ucn 3.8.13 → 3.8.15

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.
Files changed (44) hide show
  1. package/.claude/skills/ucn/SKILL.md +3 -1
  2. package/.github/workflows/ci.yml +13 -1
  3. package/README.md +1 -0
  4. package/cli/index.js +165 -246
  5. package/core/analysis.js +1400 -0
  6. package/core/build-worker.js +194 -0
  7. package/core/cache.js +105 -7
  8. package/core/callers.js +194 -64
  9. package/core/deadcode.js +22 -66
  10. package/core/discovery.js +9 -54
  11. package/core/execute.js +139 -54
  12. package/core/graph.js +615 -0
  13. package/core/imports.js +50 -16
  14. package/core/output/analysis-ext.js +271 -0
  15. package/core/output/analysis.js +491 -0
  16. package/core/output/extraction.js +188 -0
  17. package/core/output/find.js +355 -0
  18. package/core/output/graph.js +399 -0
  19. package/core/output/refactoring.js +293 -0
  20. package/core/output/reporting.js +331 -0
  21. package/core/output/search.js +307 -0
  22. package/core/output/shared.js +271 -0
  23. package/core/output/tracing.js +416 -0
  24. package/core/output.js +15 -3293
  25. package/core/parallel-build.js +165 -0
  26. package/core/project.js +299 -3633
  27. package/core/registry.js +59 -0
  28. package/core/reporting.js +258 -0
  29. package/core/search.js +890 -0
  30. package/core/stacktrace.js +1 -1
  31. package/core/tracing.js +631 -0
  32. package/core/verify.js +10 -13
  33. package/eslint.config.js +43 -0
  34. package/jsconfig.json +10 -0
  35. package/languages/go.js +21 -2
  36. package/languages/html.js +8 -0
  37. package/languages/index.js +102 -40
  38. package/languages/java.js +13 -0
  39. package/languages/javascript.js +17 -1
  40. package/languages/python.js +14 -0
  41. package/languages/rust.js +13 -0
  42. package/languages/utils.js +1 -1
  43. package/mcp/server.js +45 -28
  44. package/package.json +8 -3
package/core/discovery.js CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
+ const { langTraits } = require('../languages');
9
10
 
10
11
  // Always ignore - unambiguous, never user code
11
12
  const DEFAULT_IGNORES = [
@@ -504,46 +505,10 @@ function findTestFileFor(sourceFile, language) {
504
505
  const ext = path.extname(sourceFile);
505
506
  const base = path.basename(sourceFile, ext);
506
507
 
507
- const candidates = [];
508
-
509
- switch (language) {
510
- case 'javascript':
511
- case 'typescript':
512
- case 'tsx':
513
- candidates.push(
514
- `${base}.test${ext}`,
515
- `${base}.spec${ext}`,
516
- `${base}.test.ts`,
517
- `${base}.test.js`,
518
- `${base}.spec.ts`,
519
- `${base}.spec.js`
520
- );
521
- break;
522
- case 'python':
523
- candidates.push(
524
- `test_${base}.py`,
525
- `${base}_test.py`
526
- );
527
- break;
528
- case 'go':
529
- candidates.push(`${base}_test.go`);
530
- break;
531
- case 'java':
532
- candidates.push(
533
- `${base}Test.java`,
534
- `${base}Tests.java`,
535
- `${base}TestCase.java`
536
- );
537
- break;
538
- case 'rust':
539
- candidates.push(`${base}_test.rs`);
540
- break;
541
- default:
542
- candidates.push(
543
- `${base}.test${ext}`,
544
- `${base}.spec${ext}`
545
- );
546
- }
508
+ const traits = langTraits(language);
509
+ const candidates = traits?.testFileCandidates
510
+ ? traits.testFileCandidates(base, ext)
511
+ : [`${base}.test${ext}`, `${base}.spec${ext}`];
547
512
 
548
513
  // Check in same directory
549
514
  for (const candidate of candidates) {
@@ -553,20 +518,10 @@ function findTestFileFor(sourceFile, language) {
553
518
  }
554
519
  }
555
520
 
556
- // Check in __tests__ subdirectory
557
- if (language === 'javascript' || language === 'typescript' || language === 'tsx') {
558
- const testsDir = path.join(dir, '__tests__');
559
- for (const candidate of candidates) {
560
- const testPath = path.join(testsDir, candidate);
561
- if (fs.existsSync(testPath)) {
562
- return testPath;
563
- }
564
- }
565
- }
566
-
567
- // Check in tests subdirectory
568
- if (language === 'python' || language === 'rust') {
569
- const testsDir = path.join(dir, 'tests');
521
+ // Check in language-specific test directories
522
+ const testDirs = traits?.testDirs || [];
523
+ for (const testDir of testDirs) {
524
+ const testsDir = path.join(dir, testDir);
570
525
  for (const candidate of candidates) {
571
526
  const testPath = path.join(testsDir, candidate);
572
527
  if (fs.existsSync(testPath)) {
package/core/execute.js CHANGED
@@ -86,6 +86,21 @@ function applyTestExclusions(exclude, includeTests) {
86
86
  return includeTests ? arr : addTestExclusions(arr);
87
87
  }
88
88
 
89
+ /**
90
+ * Build common caller/callee analysis options from handler params.
91
+ * Used by about, context, blast, reverseTrace, smart, trace, affectedTests.
92
+ */
93
+ function buildCallerOptions(p) {
94
+ return {
95
+ file: p.file,
96
+ className: p.className,
97
+ includeMethods: p.includeMethods,
98
+ includeUncertain: p.includeUncertain || false,
99
+ exclude: toExcludeArray(p.exclude),
100
+ minConfidence: num(p.minConfidence, 0),
101
+ };
102
+ }
103
+
89
104
  /** Check if a file-based result has a file error. */
90
105
  function checkFileError(result, file) {
91
106
  if (!result) return null;
@@ -140,6 +155,27 @@ function limitNote(limit, total) {
140
155
  return `Showing ${limit} of ${total} results. Use --limit N to see more.`;
141
156
  }
142
157
 
158
+ /** Build a truncation warning when index is incomplete */
159
+ function truncationNote(index) {
160
+ if (!index.truncated) return null;
161
+ return `Index limited to ${index.truncated.indexed} files (max ${index.truncated.maxFiles}). Results may be incomplete. Use --max-files N to increase.`;
162
+ }
163
+
164
+ /** Build notes for tree-based results (blast, trace, reverseTrace, affectedTests). */
165
+ function treeNote(result) {
166
+ const parts = [];
167
+ if (result?.warnings?.length > 0) {
168
+ for (const w of result.warnings) parts.push(w.message || w);
169
+ }
170
+ if (result?.tree?.truncatedChildren > 0) {
171
+ parts.push(`${result.tree.truncatedChildren} children truncated. Use --depth=N or --all to expand.`);
172
+ }
173
+ if (result?.truncatedCallers > 0) {
174
+ parts.push(`${result.truncatedCallers} callers truncated. Use --all to expand.`);
175
+ }
176
+ return parts.length > 0 ? parts.join('\n') : null;
177
+ }
178
+
143
179
  /**
144
180
  * Check if a --file pattern matches any files in the index.
145
181
  * Returns error string if no files match, null otherwise.
@@ -190,16 +226,11 @@ const HANDLERS = {
190
226
  if (err) return { ok: false, error: err };
191
227
  applyClassMethodSyntax(p);
192
228
  const result = index.about(p.name, {
229
+ ...buildCallerOptions(p),
193
230
  withTypes: p.withTypes || false,
194
- file: p.file,
195
- className: p.className,
196
231
  all: p.all,
197
- includeMethods: p.includeMethods,
198
- includeUncertain: p.includeUncertain || false,
199
- exclude: toExcludeArray(p.exclude),
200
232
  maxCallers: num(p.top, undefined),
201
233
  maxCallees: num(p.top, undefined),
202
- minConfidence: num(p.minConfidence, 0),
203
234
  });
204
235
  if (!result) {
205
236
  // Give better error if file/className filter is the problem
@@ -218,7 +249,8 @@ const HANDLERS = {
218
249
  }
219
250
  return { ok: false, error: `Symbol "${p.name}" not found.` };
220
251
  }
221
- return { ok: true, result, showConfidence: !!p.showConfidence };
252
+ const tNote = truncationNote(index);
253
+ return { ok: true, result, showConfidence: !!p.showConfidence, ...(tNote && { note: tNote }) };
222
254
  },
223
255
 
224
256
  context: (index, p) => {
@@ -230,15 +262,11 @@ const HANDLERS = {
230
262
  const classErr = validateClassName(index, p.name, p.className);
231
263
  if (classErr) return { ok: false, error: classErr };
232
264
  const result = index.context(p.name, {
233
- includeMethods: p.includeMethods,
234
- includeUncertain: p.includeUncertain || false,
235
- file: p.file,
236
- className: p.className,
237
- exclude: toExcludeArray(p.exclude),
238
- minConfidence: num(p.minConfidence, 0),
265
+ ...buildCallerOptions(p),
239
266
  });
240
267
  if (!result) return { ok: false, error: `Symbol "${p.name}" not found.` };
241
- return { ok: true, result, showConfidence: !!p.showConfidence };
268
+ const tNote = truncationNote(index);
269
+ return { ok: true, result, showConfidence: !!p.showConfidence, ...(tNote && { note: tNote }) };
242
270
  },
243
271
 
244
272
  impact: (index, p) => {
@@ -256,7 +284,8 @@ const HANDLERS = {
256
284
  top: num(p.top, undefined),
257
285
  });
258
286
  if (!result) return { ok: false, error: `Function "${p.name}" not found.` };
259
- return { ok: true, result };
287
+ const tNote = truncationNote(index);
288
+ return { ok: true, result, ...(tNote && { note: tNote }) };
260
289
  },
261
290
 
262
291
  blast: (index, p) => {
@@ -269,16 +298,15 @@ const HANDLERS = {
269
298
  if (classErr) return { ok: false, error: classErr };
270
299
  const depthVal = num(p.depth, undefined);
271
300
  const result = index.blast(p.name, {
301
+ ...buildCallerOptions(p),
272
302
  depth: depthVal ?? 3,
273
- file: p.file,
274
- className: p.className,
275
303
  all: p.all || depthVal !== undefined,
276
- exclude: toExcludeArray(p.exclude),
277
- includeMethods: p.includeMethods,
278
- includeUncertain: p.includeUncertain || false,
279
304
  });
280
305
  if (!result) return { ok: false, error: `Function "${p.name}" not found.` };
281
- return { ok: true, result };
306
+ const note = treeNote(result);
307
+ const tNote = truncationNote(index);
308
+ const combined = [note, tNote].filter(Boolean).join('\n') || undefined;
309
+ return { ok: true, result, ...(combined && { note: combined }) };
282
310
  },
283
311
 
284
312
  reverseTrace: (index, p) => {
@@ -291,16 +319,15 @@ const HANDLERS = {
291
319
  if (classErr) return { ok: false, error: classErr };
292
320
  const depthVal = num(p.depth, undefined);
293
321
  const result = index.reverseTrace(p.name, {
322
+ ...buildCallerOptions(p),
294
323
  depth: depthVal ?? 5,
295
- file: p.file,
296
- className: p.className,
297
324
  all: p.all || depthVal !== undefined,
298
- exclude: toExcludeArray(p.exclude),
299
- includeMethods: p.includeMethods,
300
- includeUncertain: p.includeUncertain || false,
301
325
  });
302
326
  if (!result) return { ok: false, error: `Function "${p.name}" not found.` };
303
- return { ok: true, result };
327
+ const note = treeNote(result);
328
+ const tNote = truncationNote(index);
329
+ const combined = [note, tNote].filter(Boolean).join('\n') || undefined;
330
+ return { ok: true, result, ...(combined && { note: combined }) };
304
331
  },
305
332
 
306
333
  smart: (index, p) => {
@@ -309,12 +336,11 @@ const HANDLERS = {
309
336
  applyClassMethodSyntax(p);
310
337
  const fileErr = checkFilePatternMatch(index, p.file);
311
338
  if (fileErr) return { ok: false, error: fileErr };
339
+ const classErr = validateClassName(index, p.name, p.className);
340
+ if (classErr) return { ok: false, error: classErr };
312
341
  const result = index.smart(p.name, {
313
- file: p.file,
314
- className: p.className,
342
+ ...buildCallerOptions(p),
315
343
  withTypes: p.withTypes || false,
316
- includeMethods: p.includeMethods,
317
- includeUncertain: p.includeUncertain || false,
318
344
  });
319
345
  if (!result) return { ok: false, error: `Function "${p.name}" not found.` };
320
346
  return { ok: true, result };
@@ -326,17 +352,19 @@ const HANDLERS = {
326
352
  applyClassMethodSyntax(p);
327
353
  const fileErr = checkFilePatternMatch(index, p.file);
328
354
  if (fileErr) return { ok: false, error: fileErr };
355
+ const classErr = validateClassName(index, p.name, p.className);
356
+ if (classErr) return { ok: false, error: classErr };
329
357
  const depthVal = num(p.depth, undefined);
330
358
  const result = index.trace(p.name, {
359
+ ...buildCallerOptions(p),
331
360
  depth: depthVal ?? 3,
332
- file: p.file,
333
- className: p.className,
334
361
  all: p.all || depthVal !== undefined,
335
- includeMethods: p.includeMethods,
336
- includeUncertain: p.includeUncertain || false,
337
362
  });
338
363
  if (!result) return { ok: false, error: `Function "${p.name}" not found.` };
339
- return { ok: true, result };
364
+ const note = treeNote(result);
365
+ const tNote = truncationNote(index);
366
+ const combined = [note, tNote].filter(Boolean).join('\n') || undefined;
367
+ return { ok: true, result, ...(combined && { note: combined }) };
340
368
  },
341
369
 
342
370
  example: (index, p) => {
@@ -345,6 +373,8 @@ const HANDLERS = {
345
373
  applyClassMethodSyntax(p);
346
374
  const fileErr = checkFilePatternMatch(index, p.file);
347
375
  if (fileErr) return { ok: false, error: fileErr };
376
+ const classErr = validateClassName(index, p.name, p.className);
377
+ if (classErr) return { ok: false, error: classErr };
348
378
  const result = index.example(p.name, { file: p.file, className: p.className });
349
379
  if (!result) return { ok: false, error: `No examples found for "${p.name}".` };
350
380
  return { ok: true, result };
@@ -363,7 +393,17 @@ const HANDLERS = {
363
393
  all: p.all,
364
394
  });
365
395
  if (!result) return { ok: false, error: `Function "${p.name}" not found.` };
366
- return { ok: true, result };
396
+ const parts = [];
397
+ if (result.similarNamesTotal > result.similarNames.length)
398
+ parts.push(`similar names: showing ${result.similarNames.length} of ${result.similarNamesTotal}`);
399
+ if (result.sharedCallersTotal > result.sharedCallers.length)
400
+ parts.push(`shared callers: showing ${result.sharedCallers.length} of ${result.sharedCallersTotal}`);
401
+ if (result.sharedCalleesTotal > result.sharedCallees.length)
402
+ parts.push(`shared callees: showing ${result.sharedCallees.length} of ${result.sharedCalleesTotal}`);
403
+ const relatedNote = parts.length ? `Truncated: ${parts.join(', ')}. Use --all to show all.` : null;
404
+ const tNote = truncationNote(index);
405
+ const combined = [relatedNote, tNote].filter(Boolean).join('\n') || undefined;
406
+ return { ok: true, result, ...(combined && { note: combined }) };
367
407
  },
368
408
 
369
409
  // ── Finding Code ────────────────────────────────────────────────────
@@ -375,6 +415,10 @@ const HANDLERS = {
375
415
  // Check if --file pattern matches any files
376
416
  const fileErr = checkFilePatternMatch(index, p.file);
377
417
  if (fileErr) return { ok: false, error: fileErr };
418
+ if (p.className) {
419
+ const classErr = validateClassName(index, p.name, p.className);
420
+ if (classErr) return { ok: false, error: classErr };
421
+ }
378
422
  // Auto-include tests when pattern clearly targets test functions
379
423
  // But only if the user didn't explicitly set include_tests=false
380
424
  let includeTests = p.includeTests;
@@ -401,6 +445,8 @@ const HANDLERS = {
401
445
  if (limited) notes.push(limitNote(limit, total));
402
446
  result = items;
403
447
  }
448
+ const tNote = truncationNote(index);
449
+ if (tNote) notes.push(tNote);
404
450
  return { ok: true, result, note: notes.length ? notes.join('\n') : undefined };
405
451
  },
406
452
 
@@ -411,6 +457,10 @@ const HANDLERS = {
411
457
  const exclude = applyTestExclusions(p.exclude, p.includeTests);
412
458
  const fileErr = checkFilePatternMatch(index, p.file);
413
459
  if (fileErr) return { ok: false, error: fileErr };
460
+ if (p.className) {
461
+ const classErr = validateClassName(index, p.name, p.className);
462
+ if (classErr) return { ok: false, error: classErr };
463
+ }
414
464
  const result = index.usages(p.name, {
415
465
  codeOnly: p.codeOnly || false,
416
466
  context: num(p.context, 0),
@@ -431,6 +481,8 @@ const HANDLERS = {
431
481
  },
432
482
 
433
483
  toc: (index, p) => {
484
+ const fileErr = checkFilePatternMatch(index, p.file);
485
+ if (fileErr) return { ok: false, error: fileErr };
434
486
  const result = index.getToc({
435
487
  detailed: p.detailed,
436
488
  topLevel: p.topLevel,
@@ -482,10 +534,14 @@ const HANDLERS = {
482
534
  note = limitNote(limit, totalEntries);
483
535
  }
484
536
  }
537
+ const tNote = truncationNote(index);
538
+ if (tNote) note = note ? `${note}\n${tNote}` : tNote;
485
539
  return { ok: true, result, note };
486
540
  },
487
541
 
488
542
  search: (index, p) => {
543
+ const fileErr = checkFilePatternMatch(index, p.file);
544
+ if (fileErr) return { ok: false, error: fileErr };
489
545
  // Detect structural search mode: any of these flags triggers index-based search
490
546
  const isStructural = p.type || p.param || p.receiver || p.returns || p.decorator || p.exported || p.unused;
491
547
  if (isStructural) {
@@ -527,13 +583,16 @@ const HANDLERS = {
527
583
  file: p.file,
528
584
  });
529
585
  if (result.meta) result.meta.testsExcluded = testsExcluded;
530
- return { ok: true, result };
586
+ const tNote = truncationNote(index);
587
+ return { ok: true, result, ...(tNote && { note: tNote }) };
531
588
  },
532
589
 
533
590
  tests: (index, p) => {
534
591
  const err = requireName(p.name);
535
592
  if (err) return { ok: false, error: err };
536
593
  applyClassMethodSyntax(p);
594
+ const classErr = validateClassName(index, p.name, p.className);
595
+ if (classErr) return { ok: false, error: classErr };
537
596
  const result = index.tests(p.name, {
538
597
  callsOnly: p.callsOnly || false,
539
598
  className: p.className,
@@ -551,15 +610,14 @@ const HANDLERS = {
551
610
  if (classErr) return { ok: false, error: classErr };
552
611
  const depthVal = num(p.depth, undefined);
553
612
  const result = index.affectedTests(p.name, {
613
+ ...buildCallerOptions(p),
554
614
  depth: depthVal ?? 3,
555
- file: p.file,
556
- className: p.className,
557
- exclude: toExcludeArray(p.exclude),
558
- includeMethods: p.includeMethods,
559
- includeUncertain: p.includeUncertain || false,
560
615
  });
561
616
  if (!result) return { ok: false, error: `Function "${p.name}" not found.` };
562
- return { ok: true, result };
617
+ const note = treeNote(result);
618
+ const tNote = truncationNote(index);
619
+ const combined = [note, tNote].filter(Boolean).join('\n') || undefined;
620
+ return { ok: true, result, ...(combined && { note: combined }) };
563
621
  },
564
622
 
565
623
  deadcode: (index, p) => {
@@ -584,6 +642,8 @@ const HANDLERS = {
584
642
  if (result.excludedDecorated != null) sliced.excludedDecorated = result.excludedDecorated;
585
643
  result = sliced;
586
644
  }
645
+ const tNote = truncationNote(index);
646
+ if (tNote) note = note ? `${note}\n${tNote}` : tNote;
587
647
  return { ok: true, result, note };
588
648
  },
589
649
 
@@ -591,13 +651,20 @@ const HANDLERS = {
591
651
  const fileErr = checkFilePatternMatch(index, p.file);
592
652
  if (fileErr) return { ok: false, error: fileErr };
593
653
  const { detectEntrypoints } = require('./entrypoints');
594
- const result = detectEntrypoints(index, {
654
+ const exclude = applyTestExclusions(p.exclude, p.includeTests);
655
+ let result = detectEntrypoints(index, {
595
656
  type: p.type,
596
657
  framework: p.framework,
597
658
  file: p.file,
598
- exclude: p.exclude,
659
+ exclude,
599
660
  });
600
- return { ok: true, result };
661
+ const limit = num(p.limit, undefined);
662
+ let note;
663
+ if (limit && limit > 0 && Array.isArray(result) && result.length > limit) {
664
+ note = limitNote(limit, result.length);
665
+ result = result.slice(0, limit);
666
+ }
667
+ return { ok: true, result, note };
601
668
  },
602
669
 
603
670
  // ── Extracting Code ─────────────────────────────────────────────────
@@ -608,6 +675,10 @@ const HANDLERS = {
608
675
  applyClassMethodSyntax(p);
609
676
  const fileErr = checkFilePatternMatch(index, p.file);
610
677
  if (fileErr) return { ok: false, error: fileErr };
678
+ if (p.className) {
679
+ const classErr = validateClassName(index, p.name, p.className);
680
+ if (classErr) return { ok: false, error: classErr };
681
+ }
611
682
 
612
683
  const fnNames = p.name.includes(',')
613
684
  ? p.name.split(',').map(n => n.trim()).filter(Boolean)
@@ -662,7 +733,7 @@ const HANDLERS = {
662
733
  if (entries.length === 0 && notes.length > 0) {
663
734
  return { ok: false, error: notes.join('\n') };
664
735
  }
665
- return { ok: true, result: { entries, notes } };
736
+ return { ok: true, result: { entries }, note: notes.length ? notes.map(n => 'Note: ' + n).join('\n') : undefined };
666
737
  },
667
738
 
668
739
  class: (index, p) => {
@@ -693,7 +764,7 @@ const HANDLERS = {
693
764
  const totalLines = m.endLine - m.startLine + 1;
694
765
  entries.push({ match: m, code, totalLines, summaryMode: false, truncated: false });
695
766
  }
696
- return { ok: true, result: { entries, notes } };
767
+ return { ok: true, result: { entries }, note: notes.length ? notes.map(n => 'Note: ' + n).join('\n') : undefined };
697
768
  }
698
769
 
699
770
  const match = matches.length > 1 && !p.file
@@ -712,7 +783,7 @@ const HANDLERS = {
712
783
  if (totalLines > 200 && !maxLines) {
713
784
  const methods = index.findMethodsForType(match.name);
714
785
  entries.push({ match, code: null, methods, totalLines, summaryMode: true, truncated: false });
715
- return { ok: true, result: { entries, notes } };
786
+ return { ok: true, result: { entries }, note: notes.length ? notes.map(n => 'Note: ' + n).join('\n') : undefined };
716
787
  }
717
788
 
718
789
  // Truncated mode (maxLines specified and class exceeds it)
@@ -722,13 +793,13 @@ const HANDLERS = {
722
793
  const truncated = fileLines.slice(match.startLine - 1, match.startLine - 1 + maxLines);
723
794
  const code = cleanHtmlScriptTags(truncated, detectLanguage(match.file)).join('\n');
724
795
  entries.push({ match, code, totalLines, summaryMode: false, truncated: true, maxLines });
725
- return { ok: true, result: { entries, notes } };
796
+ return { ok: true, result: { entries }, note: notes.length ? notes.map(n => 'Note: ' + n).join('\n') : undefined };
726
797
  }
727
798
 
728
799
  // Full extraction
729
800
  const code = readAndExtract(match);
730
801
  entries.push({ match, code, totalLines, summaryMode: false, truncated: false });
731
- return { ok: true, result: { entries, notes } };
802
+ return { ok: true, result: { entries }, note: notes.length ? notes.map(n => 'Note: ' + n).join('\n') : undefined };
732
803
  },
733
804
 
734
805
  lines: (index, p) => {
@@ -870,12 +941,18 @@ const HANDLERS = {
870
941
  },
871
942
 
872
943
  diffImpact: (index, p) => {
873
- const result = index.diffImpact({
944
+ let result = index.diffImpact({
874
945
  base: p.base || 'HEAD',
875
946
  staged: p.staged || false,
876
947
  file: p.file,
877
948
  });
878
- return { ok: true, result };
949
+ const limit = num(p.limit, undefined);
950
+ let note;
951
+ if (limit && limit > 0 && result && result.changed && result.changed.length > limit) {
952
+ note = limitNote(limit, result.changed.length);
953
+ result = { ...result, changed: result.changed.slice(0, limit) };
954
+ }
955
+ return { ok: true, result, note };
879
956
  },
880
957
 
881
958
  // ── Other ───────────────────────────────────────────────────────────
@@ -884,6 +961,10 @@ const HANDLERS = {
884
961
  const err = requireName(p.name);
885
962
  if (err) return { ok: false, error: err };
886
963
  applyClassMethodSyntax(p);
964
+ const fileErr = checkFilePatternMatch(index, p.file);
965
+ if (fileErr) return { ok: false, error: fileErr };
966
+ const classErr = validateClassName(index, p.name, p.className);
967
+ if (classErr) return { ok: false, error: classErr };
887
968
  const result = index.typedef(p.name, { exact: p.exact || false, className: p.className, file: p.file });
888
969
  return { ok: true, result };
889
970
  },
@@ -897,6 +978,10 @@ const HANDLERS = {
897
978
  },
898
979
 
899
980
  api: (index, p) => {
981
+ if (p.file) {
982
+ const fileErr = checkFilePatternMatch(index, p.file);
983
+ if (fileErr) return { ok: false, error: fileErr };
984
+ }
900
985
  let result = index.api(p.file);
901
986
  if (p.file) {
902
987
  const fileErr = checkFileError(result, p.file);