ucn 4.2.3 → 5.0.2

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 (72) hide show
  1. package/.claude/skills/ucn/SKILL.md +89 -77
  2. package/.claude/skills/ucn/references/commands.md +62 -68
  3. package/.claude/skills/ucn/references/trust-contract.md +31 -6
  4. package/README.md +438 -305
  5. package/assets/demo.svg +31 -0
  6. package/cli/index.js +430 -1385
  7. package/core/account.js +144 -34
  8. package/core/analysis.js +182 -72
  9. package/core/ast-analysis.js +279 -0
  10. package/core/bridge.js +205 -24
  11. package/core/brief.js +27 -58
  12. package/core/build-worker.js +21 -140
  13. package/core/cache.js +513 -11
  14. package/core/callers.js +4920 -456
  15. package/core/check.js +13 -4
  16. package/core/command-contracts.js +402 -0
  17. package/core/compilation-database.js +276 -0
  18. package/core/confidence.js +4 -1
  19. package/core/deadcode.js +397 -19
  20. package/core/discovery.js +359 -46
  21. package/core/entrypoints.js +195 -41
  22. package/core/execute.js +887 -81
  23. package/core/graph-build.js +162 -7
  24. package/core/graph.js +53 -77
  25. package/core/imports.js +65 -6
  26. package/core/index-ir.js +138 -0
  27. package/core/ir.js +195 -0
  28. package/core/output/analysis.js +212 -22
  29. package/core/output/brief.js +23 -0
  30. package/core/output/check.js +4 -0
  31. package/core/output/doctor.js +37 -6
  32. package/core/output/endpoints.js +5 -2
  33. package/core/output/extraction.js +24 -12
  34. package/core/output/find.js +141 -36
  35. package/core/output/graph.js +11 -5
  36. package/core/output/public.js +462 -0
  37. package/core/output/refactoring.js +42 -10
  38. package/core/output/reporting.js +97 -20
  39. package/core/output/search.js +24 -16
  40. package/core/output/shared.js +22 -1
  41. package/core/output/tracing.js +30 -15
  42. package/core/output-budget.js +295 -0
  43. package/core/output.js +1 -0
  44. package/core/parallel-build.js +44 -11
  45. package/core/parser.js +3 -3
  46. package/core/project.js +384 -187
  47. package/core/public-command.js +47 -0
  48. package/core/registry.js +247 -117
  49. package/core/reporting.js +312 -290
  50. package/core/search.js +317 -185
  51. package/core/semantic-provider.js +110 -0
  52. package/core/stacktrace.js +25 -0
  53. package/core/tracing.js +101 -51
  54. package/core/trust-matrix.js +19 -40
  55. package/core/verify.js +534 -37
  56. package/languages/adapter.js +218 -0
  57. package/languages/c-family.js +2791 -0
  58. package/languages/c.js +3 -0
  59. package/languages/cpp.js +3 -0
  60. package/languages/csharp.js +1402 -0
  61. package/languages/go.js +60 -21
  62. package/languages/html.js +2 -2
  63. package/languages/index.js +85 -7
  64. package/languages/java.js +396 -13
  65. package/languages/javascript.js +199 -19
  66. package/languages/python.js +964 -22
  67. package/languages/rust.js +1317 -152
  68. package/languages/utils.js +40 -3
  69. package/mcp/server.js +254 -636
  70. package/package.json +39 -22
  71. package/eslint.config.js +0 -43
  72. package/jsconfig.json +0 -10
@@ -26,7 +26,7 @@ function firstSentenceShort(text) {
26
26
  /**
27
27
  * Format find command output
28
28
  */
29
- function formatFind(symbols, query, top) {
29
+ function formatFind(symbols, query, top = undefined) {
30
30
  if (symbols.length === 0) {
31
31
  return `No symbols found for "${query}"`;
32
32
  }
@@ -55,13 +55,21 @@ function formatFind(symbols, query, top) {
55
55
  if (s.usageCounts !== undefined) {
56
56
  const c = s.usageCounts;
57
57
  const parts = [];
58
- if (c.calls > 0) parts.push(`${c.calls} calls`);
58
+ if (c.calls > 0 && c.confirmedCalls !== undefined) {
59
+ const tiers = [];
60
+ if (c.confirmedCalls > 0) tiers.push(`${c.confirmedCalls} confirmed`);
61
+ if (c.unverifiedCalls > 0) tiers.push(`${c.unverifiedCalls} unverified`);
62
+ parts.push(`${c.calls} target call candidate(s) (${tiers.join(', ')})`);
63
+ } else if (c.calls > 0) parts.push(`${c.calls} calls`);
59
64
  if (c.definitions > 0) parts.push(`${c.definitions} def`);
60
65
  if (c.imports > 0) parts.push(`${c.imports} imports`);
61
66
  if (c.references > 0) parts.push(`${c.references} refs`);
67
+ const excluded = c.otherTargetCalls > 0
68
+ ? `; ${c.otherTargetCalls} same-name call(s) excluded as another target`
69
+ : '';
62
70
  lines.push(parts.length > 0
63
- ? ` (${c.total} usages: ${parts.join(', ')})`
64
- : ` (${c.total} usages)`);
71
+ ? ` (${c.total} usages: ${parts.join(', ')}${excluded})`
72
+ : ` (${c.total} usages${excluded})`);
65
73
  } else if (s.usageCount !== undefined) {
66
74
  lines.push(` (${s.usageCount} usages)`);
67
75
  }
@@ -92,12 +100,12 @@ function formatFindJson(items) {
92
100
  }
93
101
 
94
102
  /**
95
- * Format find results with depth/confidence features (detailed view).
103
+ * Format find results with confidence and optional source.
96
104
  * Returns a string. Used by CLI and interactive mode.
97
105
  *
98
106
  * @param {Array} symbols - Find result array
99
107
  * @param {string} query - Original search query
100
- * @param {object} options - { depth, top, all }
108
+ * @param {object} options - { depth, top, all, compact, withSource, limitHint }
101
109
  */
102
110
  function formatFindDetailed(symbols, query, options = {}) {
103
111
  const { top, all, compact } = options;
@@ -111,14 +119,25 @@ function formatFindDetailed(symbols, query, options = {}) {
111
119
  }
112
120
 
113
121
  const lines = [];
122
+ const sameNameDefinitionCounts = new Map();
123
+ for (const symbol of symbols) {
124
+ sameNameDefinitionCounts.set(
125
+ symbol.name,
126
+ (sameNameDefinitionCounts.get(symbol.name) || 0) + 1,
127
+ );
128
+ }
129
+ for (const [name, count] of Object.entries(symbols.findInfo?.nameWideDefinitionCounts || {})) {
130
+ sameNameDefinitionCounts.set(name, count);
131
+ }
114
132
  const limit = all ? symbols.length : (top > 0 ? top : DEFAULT_LIMIT);
115
133
  const showing = Math.min(limit, symbols.length);
116
- const hidden = symbols.length - showing;
134
+ const total = symbols.findInfo?.total ?? symbols.length;
135
+ const hidden = Math.max(0, total - showing);
117
136
 
118
137
  if (hidden > 0) {
119
- lines.push(`Found ${symbols.length} match(es) for "${query}" (showing top ${showing}):`);
138
+ lines.push(`Found ${total} match(es) for "${query}" (showing top ${showing}):`);
120
139
  } else {
121
- lines.push(`Found ${symbols.length} match(es) for "${query}":`);
140
+ lines.push(`Found ${total} match(es) for "${query}":`);
122
141
  }
123
142
  if (!compact) lines.push('─'.repeat(60));
124
143
 
@@ -144,7 +163,11 @@ function formatFindDetailed(symbols, query, options = {}) {
144
163
  // One line per result: "<handle> <sig> <usages?> <doc snippet?>"
145
164
  const parts = [`${loc} ${sig}${confStr}`];
146
165
  if (s.usageCounts !== undefined && s.usageCounts.total > 0) {
147
- parts.push(`(${s.usageCounts.total} usages)`);
166
+ const scope = sameNameDefinitionCounts.get(s.name) > 1 ? ' name-wide' : '';
167
+ const label = s.usageCounts.complete === false
168
+ ? `${scope} indexed activity; refs excluded`
169
+ : `${scope} usages`;
170
+ parts.push(`(${s.usageCounts.total}${label})`);
148
171
  } else if (s.usageCount !== undefined) {
149
172
  parts.push(`(${s.usageCount} usages)`);
150
173
  }
@@ -153,6 +176,12 @@ function formatFindDetailed(symbols, query, options = {}) {
153
176
  if (snip) parts.push(`— ${snip}`);
154
177
  }
155
178
  lines.push(parts.join(' '));
179
+ if (options.withSource && s.code) {
180
+ lines.push(' ───');
181
+ for (const codeLine of String(s.code).split('\n')) {
182
+ lines.push(` ${codeLine}`);
183
+ }
184
+ }
156
185
  continue;
157
186
  }
158
187
 
@@ -164,11 +193,24 @@ function formatFindDetailed(symbols, query, options = {}) {
164
193
  if (s.usageCounts !== undefined) {
165
194
  const c = s.usageCounts;
166
195
  const parts = [];
167
- if (c.calls > 0) parts.push(`${c.calls} calls`);
196
+ if (c.calls > 0 && c.confirmedCalls !== undefined) {
197
+ const tiers = [];
198
+ if (c.confirmedCalls > 0) tiers.push(`${c.confirmedCalls} confirmed`);
199
+ if (c.unverifiedCalls > 0) tiers.push(`${c.unverifiedCalls} unverified`);
200
+ parts.push(`${c.calls} target call candidate(s) (${tiers.join(', ')})`);
201
+ } else if (c.calls > 0) parts.push(`${c.calls} calls`);
168
202
  if (c.definitions > 0) parts.push(`${c.definitions} def`);
169
203
  if (c.imports > 0) parts.push(`${c.imports} imports`);
170
204
  if (c.references > 0) parts.push(`${c.references} refs`);
171
- lines.push(` (${c.total} usages: ${parts.join(', ')})`);
205
+ const scope = sameNameDefinitionCounts.get(s.name) > 1 ? 'name-wide ' : '';
206
+ const label = c.complete === false ? 'indexed activity' : 'usages';
207
+ const boundary = c.complete === false
208
+ ? '; references not counted — use usages for the full inventory'
209
+ : '';
210
+ const excluded = c.otherTargetCalls > 0
211
+ ? `; ${c.otherTargetCalls} same-name call(s) excluded as another target`
212
+ : '';
213
+ lines.push(` (${c.total} ${scope}${label}: ${parts.join(', ')}${boundary}${excluded})`);
172
214
  } else if (s.usageCount !== undefined) {
173
215
  lines.push(` (${s.usageCount} usages)`);
174
216
  }
@@ -177,8 +219,14 @@ function formatFindDetailed(symbols, query, options = {}) {
177
219
  lines.push(` ⚠ ${confidence.reasons.join(', ')}`);
178
220
  }
179
221
 
180
- // Depth 2: + first 10 lines of code
181
- if (depth === '2' || depth === 'full') {
222
+ if (options.withSource && s.code) {
223
+ lines.push(' ───');
224
+ for (const codeLine of String(s.code).split('\n')) {
225
+ lines.push(` ${codeLine}`);
226
+ }
227
+ // Legacy internal formatter mode. The v5 public command uses the
228
+ // explicit withSource contract instead of overloading graph depth.
229
+ } else if (depth === '2' || depth === 'full') {
182
230
  try {
183
231
  const content = fs.readFileSync(s.file, 'utf-8');
184
232
  const fileLines = content.split('\n');
@@ -199,7 +247,8 @@ function formatFindDetailed(symbols, query, options = {}) {
199
247
  }
200
248
 
201
249
  if (hidden > 0) {
202
- lines.push(`... ${hidden} more result(s). Use --all to see all, or --top=N to see more.`);
250
+ lines.push(`... ${hidden} more result(s). ${options.limitHint ||
251
+ 'Use --all to see all, or --top=N to see more.'}`);
203
252
  }
204
253
 
205
254
  return lines.join('\n');
@@ -245,11 +294,14 @@ function formatUsagesJson(usages, name) {
245
294
 
246
295
  const calls = refs.filter(u => u.usageType === 'call');
247
296
  const imports = refs.filter(u => u.usageType === 'import');
297
+ const textOccurrences = refs.filter(u => u.usageType === 'text');
298
+ const otherDefinitions = refs.filter(u => u.usageType === 'definition');
248
299
  // Exhaustive complement (fix #241): a non-definition record that is
249
300
  // neither call nor import lands in references — same-name definer sites
250
301
  // (usageType 'definition', isDefinition false: shadowing locals, other
251
302
  // defs of the name) used to inflate totals while rendering in NO band.
252
- const references = refs.filter(u => u.usageType !== 'call' && u.usageType !== 'import');
303
+ const references = refs.filter(u =>
304
+ !['call', 'import', 'text', 'definition'].includes(u.usageType));
253
305
 
254
306
  // Each usage record points at a call site. We emit a per-occurrence handle
255
307
  // pointing at the SITE itself in the form "relativePath:line:callerName"
@@ -285,7 +337,11 @@ function formatUsagesJson(usages, name) {
285
337
  callCount: sc ? sc.calls : calls.length,
286
338
  importCount: sc ? sc.imports : imports.length,
287
339
  referenceCount: sc ? sc.references : references.length,
288
- totalUsages: sc ? (sc.calls + sc.imports + sc.references) : refs.length,
340
+ otherDefinitionCount: sc ? (sc.otherDefinitions || 0) : otherDefinitions.length,
341
+ textCount: sc ? (sc.text || 0) : textOccurrences.length,
342
+ totalUsages: sc
343
+ ? (sc.calls + sc.imports + sc.references + (sc.text || 0))
344
+ : refs.length,
289
345
  definitions: definitions.map(d => {
290
346
  const handle = formatSymbolHandle({ ...d, name: d.name || name });
291
347
  return {
@@ -302,7 +358,12 @@ function formatUsagesJson(usages, name) {
302
358
  }),
303
359
  calls: calls.map(formatUsage),
304
360
  imports: imports.map(formatUsage),
305
- references: references.map(formatUsage)
361
+ otherDefinitions: otherDefinitions.map(formatUsage),
362
+ references: references.map(formatUsage),
363
+ textOccurrences: textOccurrences.map(u => ({
364
+ ...formatUsage(u),
365
+ textKind: u.textKind || 'other-text',
366
+ })),
306
367
  }
307
368
  });
308
369
  }
@@ -315,15 +376,21 @@ function formatUsages(usages, name, options = {}) {
315
376
  const defs = usages.filter(u => u.isDefinition);
316
377
  const calls = usages.filter(u => u.usageType === 'call');
317
378
  const imports = usages.filter(u => u.usageType === 'import');
318
- // Exhaustive complement (fix #241) — see formatUsagesJson.
319
- const refs = usages.filter(u => !u.isDefinition && u.usageType !== 'call' && u.usageType !== 'import');
379
+ const textOccurrences = usages.filter(u => u.usageType === 'text');
380
+ const otherDefinitions = usages.filter(u => !u.isDefinition && u.usageType === 'definition');
381
+ const refs = usages.filter(u => !u.isDefinition &&
382
+ !['call', 'import', 'text', 'definition'].includes(u.usageType));
320
383
 
321
384
  // Under --limit the listed entries are truncated but the summary must
322
385
  // describe the FULL result set (fix #237) — the handler attaches the
323
386
  // full counts as a non-enumerable property.
324
387
  const sc = usages.summaryCounts;
325
388
  const lines = [];
326
- lines.push(`Usages of "${name}": ${sc ? sc.definitions : defs.length} definitions, ${sc ? sc.calls : calls.length} calls, ${sc ? sc.imports : imports.length} imports, ${sc ? sc.references : refs.length} references`);
389
+ // "syntax-classified": classification is by syntax alone — a method
390
+ // reference is a `reference` here even when the engine confirms it as a
391
+ // caller. ACCOUNT lines in show/impact are engine-adjudicated, so the
392
+ // two breakdowns can legitimately differ; both count lines.
393
+ lines.push(`Usages of "${name}" (syntax-classified): ${sc ? sc.definitions : defs.length} definitions, ${sc ? (sc.otherDefinitions || 0) : otherDefinitions.length} other definitions, ${sc ? sc.calls : calls.length} calls, ${sc ? sc.imports : imports.length} imports, ${sc ? sc.references : refs.length} references, ${sc ? (sc.text || 0) : textOccurrences.length} other-text`);
327
394
  if (!compact) lines.push('═'.repeat(60));
328
395
 
329
396
  function renderContextLines(usage) {
@@ -343,8 +410,12 @@ function formatUsages(usages, name, options = {}) {
343
410
  }
344
411
 
345
412
  if (defs.length > 0) {
413
+ // Many same-name definitions (jsoup has 25 `parse` overloads) can
414
+ // consume the whole output budget before the rarer sections render —
415
+ // cap the section display; the header count stays complete.
416
+ const defLimit = !options.all && defs.length > 10 ? 10 : defs.length;
346
417
  lines.push(`${compact ? '' : '\n'}DEFINITIONS:`);
347
- for (const d of defs) {
418
+ for (const d of defs.slice(0, defLimit)) {
348
419
  if (compact) {
349
420
  lines.push(` ${d.relativePath}:${d.line || d.startLine}${d.signature ? ' ' + d.signature : ''}`);
350
421
  } else {
@@ -352,23 +423,16 @@ function formatUsages(usages, name, options = {}) {
352
423
  if (d.signature) lines.push(` ${d.signature}`);
353
424
  }
354
425
  }
355
- }
356
-
357
- if (calls.length > 0) {
358
- lines.push(`${compact ? '' : '\n'}CALLS:`);
359
- for (const c of calls) {
360
- if (compact) {
361
- const expr = c.content ? c.content.trim().replace(/\s+/g, ' ').slice(0, 100) : '';
362
- lines.push(` ${c.relativePath}:${c.line}: ${expr}`);
363
- } else {
364
- lines.push(` ${c.relativePath}:${c.line}`);
365
- renderContextLines(c);
366
- lines.push(` ${c.content.trim()}`);
367
- renderAfterLines(c);
368
- }
426
+ if (defLimit < defs.length) {
427
+ lines.push(` ... and ${defs.length - defLimit} more definitions (${options.allHint || 'use --all'})`);
369
428
  }
370
429
  }
371
430
 
431
+ // Section order is bulk-LAST: imports, references, and other-text are
432
+ // small, high-signal sections the ACCOUNT explicitly routes agents to;
433
+ // CALLS is the homogeneous bulk that dominates large outputs. Rendering
434
+ // CALLS last means the output budget truncates redundant bulk instead of
435
+ // silently swallowing the rare sections (the header still promised them).
372
436
  if (imports.length > 0) {
373
437
  lines.push(`${compact ? '' : '\n'}IMPORTS:`);
374
438
  for (const i of imports) {
@@ -382,6 +446,14 @@ function formatUsages(usages, name, options = {}) {
382
446
  }
383
447
  }
384
448
 
449
+ if (otherDefinitions.length > 0) {
450
+ lines.push(`${compact ? '' : '\n'}OTHER DEFINITIONS (same spelling; not references to the selected target):`);
451
+ for (const d of otherDefinitions) {
452
+ const expression = d.content ? d.content.trim() : '';
453
+ lines.push(` ${d.relativePath}:${d.line}${expression ? `\n ${expression}` : ''}`);
454
+ }
455
+ }
456
+
385
457
  if (refs.length > 0) {
386
458
  lines.push(`${compact ? '' : '\n'}REFERENCES:`);
387
459
  for (const r of refs) {
@@ -397,6 +469,39 @@ function formatUsages(usages, name, options = {}) {
397
469
  }
398
470
  }
399
471
 
472
+ if (textOccurrences.length > 0) {
473
+ lines.push(`${compact ? '' : '\n'}OTHER TEXT (comments, strings, docstrings, or unclassified literal lines):`);
474
+ for (const occurrence of textOccurrences) {
475
+ const kind = occurrence.textKind || 'other-text';
476
+ if (compact) {
477
+ const expr = occurrence.content
478
+ ? occurrence.content.trim().replace(/\s+/g, ' ').slice(0, 100)
479
+ : '';
480
+ lines.push(` ${occurrence.relativePath}:${occurrence.line} [${kind}]: ${expr}`);
481
+ } else {
482
+ lines.push(` ${occurrence.relativePath}:${occurrence.line} [${kind}]`);
483
+ renderContextLines(occurrence);
484
+ lines.push(` ${occurrence.content.trim()}`);
485
+ renderAfterLines(occurrence);
486
+ }
487
+ }
488
+ }
489
+
490
+ if (calls.length > 0) {
491
+ lines.push(`${compact ? '' : '\n'}CALLS:`);
492
+ for (const c of calls) {
493
+ if (compact) {
494
+ const expr = c.content ? c.content.trim().replace(/\s+/g, ' ').slice(0, 100) : '';
495
+ lines.push(` ${c.relativePath}:${c.line}: ${expr}`);
496
+ } else {
497
+ lines.push(` ${c.relativePath}:${c.line}`);
498
+ renderContextLines(c);
499
+ lines.push(` ${c.content.trim()}`);
500
+ renderAfterLines(c);
501
+ }
502
+ }
503
+ }
504
+
400
505
  return lines.join('\n');
401
506
  }
402
507
 
@@ -23,7 +23,9 @@ function formatImports(imports, filePath) {
23
23
  for (const imp of internal) {
24
24
  lines.push(` ${imp.module}`);
25
25
  if (imp.resolved) {
26
- lines.push(` -> ${imp.resolved}`);
26
+ lines.push(` -> ${imp.resolved}${imp.indexed === false
27
+ ? ' (not indexed; absent from dependency graph)'
28
+ : ''}`);
27
29
  }
28
30
  if (imp.names && imp.names.length > 0 && imp.names[0] !== '*') {
29
31
  lines.push(` ${imp.names.join(', ')}`);
@@ -73,6 +75,7 @@ function formatImportsJson(imports, filePath) {
73
75
  names: i.names,
74
76
  type: i.type,
75
77
  resolved: i.resolved || null,
78
+ indexed: i.resolved ? i.indexed !== false : false,
76
79
  isDynamic: !!i.isDynamic,
77
80
  line: i.line ?? null
78
81
  }))
@@ -170,10 +173,6 @@ function formatApi(symbols, filePath) {
170
173
 
171
174
  if (symbols.length === 0) {
172
175
  lines.push(' (none found)');
173
- if (filePath && filePath.endsWith('.py')) {
174
- lines.push('');
175
- lines.push('Note: Python requires __all__ for export detection. Use \'toc\' command to see all functions/classes.');
176
- }
177
176
  } else {
178
177
  // Group by file, then dedup overloads within each file group.
179
178
  const byFile = new Map();
@@ -194,6 +193,10 @@ function formatApi(symbols, filePath) {
194
193
  lines.push('');
195
194
  }
196
195
  }
196
+ if (symbols.apiInfo?.pythonImplicitFiles > 0) {
197
+ lines.push('');
198
+ lines.push('Note: Python files without __all__ use the standard top-level non-underscore public-name convention; explicit `from X import Y as Y` re-exports are included.');
199
+ }
197
200
 
198
201
  return lines.join('\n');
199
202
  }
@@ -212,6 +215,9 @@ function formatApiJson(symbols, filePath) {
212
215
  command: 'api',
213
216
  count: deduped.length,
214
217
  ...(li && { total: li.total, truncated: true }),
218
+ ...(symbols.apiInfo?.pythonImplicitFiles > 0 && {
219
+ note: 'Python files without __all__ use the top-level non-underscore public-name convention; explicit redundant-alias re-exports are included.',
220
+ }),
215
221
  },
216
222
  ...(filePath && { file: filePath }),
217
223
  data: {