ucn 5.4.0 → 5.4.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.
package/core/check.js CHANGED
@@ -73,6 +73,11 @@ function check(index, options = {}) {
73
73
  const modified = (dr && Array.isArray(dr.functions)) ? dr.functions : [];
74
74
  const added = (dr && Array.isArray(dr.newFunctions)) ? dr.newFunctions : [];
75
75
  const deleted = (dr && Array.isArray(dr.deletedFunctions)) ? dr.deletedFunctions : [];
76
+ const declarations = [
77
+ ...(dr?.symbols || []).map(s => ({ ...s, _kind: 'modified', _nonCallable: true })),
78
+ ...(dr?.newSymbols || []).map(s => ({ ...s, _kind: 'added', _nonCallable: true })),
79
+ ...(dr?.deletedSymbols || []).map(s => ({ ...s, _kind: 'deleted', _nonCallable: true })),
80
+ ];
76
81
  const pathCounts = {
77
82
  changedPaths: dr?.changedPaths || 0,
78
83
  nonSourcePaths: dr?.nonSourcePaths || 0,
@@ -82,9 +87,10 @@ function check(index, options = {}) {
82
87
  const allChanged = [
83
88
  ...modified.map(f => ({ ...f, _kind: 'modified' })),
84
89
  ...added.map(f => ({ ...f, _kind: 'added' })),
90
+ ...declarations,
85
91
  ];
86
92
 
87
- if (!dr || (modified.length === 0 && added.length === 0 && deleted.length === 0)) {
93
+ if (!dr || (allChanged.length === 0 && deleted.length === 0)) {
88
94
  // fix #283: say what the diff actually contained. "no changes
89
95
  // detected" with three changed files reads as a false negative in a
90
96
  // pre-commit hook — the truth is the changes are outside what the
@@ -95,7 +101,7 @@ function check(index, options = {}) {
95
101
  if (changedPaths > 0 && nonSourcePaths === changedPaths) {
96
102
  reason = `${changedPaths} changed path(s), all outside supported source files; untracked source files are included`;
97
103
  } else if (changedPaths > 0) {
98
- reason = 'no callable-symbol changes in the diff or untracked source files';
104
+ reason = 'no indexed-symbol changes in the diff or untracked source files';
99
105
  }
100
106
  return {
101
107
  base: options.base || 'HEAD',
@@ -116,9 +122,23 @@ function check(index, options = {}) {
116
122
  // For each changed function, run verify and gather caller summary
117
123
  for (const fn of changed) {
118
124
  const filePath = fn.relativePath || fn.file || '';
125
+ if (fn._nonCallable) {
126
+ const dependency = fn.impact?.typeReferences || fn.impact?.propertyAccesses;
127
+ items.push({
128
+ name: fn.name, file: filePath, line: fn.startLine, kind: fn._kind,
129
+ symbolType: fn.type, requiresToolchainValidation: true,
130
+ callerCount: fn.impact?.totalCallSites || 0,
131
+ unverifiedCallerCount: fn.impact?.unverifiedSites?.length || 0,
132
+ dependencyCount: dependency?.confirmedCount || 0,
133
+ unverifiedDependencyCount: (dependency?.unverifiedCount || 0) + (fn.remainingReferences?.length || 0),
134
+ signatureMismatches: 0,
135
+ account: summarizeAccount(fn.impact?.account),
136
+ });
137
+ continue;
138
+ }
119
139
  let verifyResult;
120
140
  try {
121
- verifyResult = index.verify(fn.name, { file: filePath });
141
+ verifyResult = index.verify(fn.name, { file: filePath, line: fn.startLine });
122
142
  } catch (e) {
123
143
  verifyResult = null;
124
144
  }
@@ -225,6 +245,13 @@ function check(index, options = {}) {
225
245
  // Action items
226
246
  const actions = [];
227
247
  for (const it of items) {
248
+ if (it.requiresToolchainValidation) {
249
+ actions.push({
250
+ severity: it.kind === 'added' ? 'warn' : 'error',
251
+ kind: 'declaration_change',
252
+ message: `${it.name}: ${it.kind} ${it.symbolType}; shape/inheritance compatibility is not checked by arity analysis — review dependency sites and run the compiler/type checker and tests`,
253
+ });
254
+ }
228
255
  if (!it.account || !it.account.textComplete) {
229
256
  actions.push({
230
257
  severity: 'error',
@@ -272,7 +299,7 @@ function check(index, options = {}) {
272
299
  actions.push({
273
300
  severity: 'error',
274
301
  kind: 'truncated_change_set',
275
- message: `${allChanged.length - limit} changed function(s) were not checked; rerun without --limit`,
302
+ message: `${allChanged.length - limit} changed symbol(s) were not checked; rerun without --limit`,
276
303
  });
277
304
  }
278
305
  if (testFiles.length > 0) {
@@ -289,9 +316,10 @@ function check(index, options = {}) {
289
316
  const signatureMismatches = items.reduce((sum, it) => sum + (it.signatureMismatches || 0), 0);
290
317
  const filteredEdges = items.reduce((sum, it) => sum + (it.account ? it.account.filtered || 0 : 0), 0);
291
318
  const usageReviewSymbols = items.filter(it => it.account && it.account.requiresUsageReview).length;
319
+ const unvalidatedDeclarations = items.filter(it => it.requiresToolchainValidation && it.kind !== 'added').length;
292
320
  const reviewRequired = incompleteAccounts > 0 || unverifiedCallSites > 0 ||
293
321
  signatureMismatches > 0 || filteredEdges > 0 || usageReviewSymbols > 0 ||
294
- actions.some(a => a.kind === 'orphan_new') ||
322
+ actions.some(a => a.kind === 'orphan_new' || a.kind === 'declaration_change') ||
295
323
  !!(limit && allChanged.length > limit);
296
324
 
297
325
  return {
@@ -308,7 +336,7 @@ function check(index, options = {}) {
308
336
  totalTests: testCount,
309
337
  actions,
310
338
  trust: {
311
- status: incompleteAccounts > 0 || signatureMismatches > 0
339
+ status: incompleteAccounts > 0 || signatureMismatches > 0 || unvalidatedDeclarations > 0
312
340
  ? 'BLOCKED'
313
341
  : reviewRequired ? 'REVIEW_REQUIRED' : 'READY_FOR_TOOLCHAIN',
314
342
  accountsChecked: items.filter(it => it.account && it.account.available).length,
@@ -317,6 +345,7 @@ function check(index, options = {}) {
317
345
  signatureMismatches,
318
346
  filteredEdges,
319
347
  usageReviewSymbols,
348
+ unvalidatedDeclarations,
320
349
  semanticComplete: false,
321
350
  safeToDelete: false,
322
351
  requiresCompilerAndTests: true,
@@ -301,7 +301,7 @@ const COMMAND_CONTRACTS = Object.freeze({
301
301
  { name: 'bridge', when: '`bridge=true`.', answer: 'Exact/parameterized route-request matches plus unmatched sides.' },
302
302
  { name: 'unmatched', when: '`unmatched=true`.', answer: 'Only unmatched supported boundaries.' },
303
303
  ],
304
- defaults: ['Framework-specific static extraction.', 'Interpolated-path uncertainty remains visible unless hidden explicitly.'],
304
+ defaults: ['Framework-specific static extraction.', 'Test routes/requests are labeled; excludeTests=true hides them and in scopes a directory.', 'Interpolated-path uncertainty remains visible unless hidden explicitly.'],
305
305
  truth: 'Findings are static matches for supported framework call/decorator shapes; bridge confidence is match quality, not runtime probability.',
306
306
  nonGoals: ['Network discovery.', 'Frameworks not represented by an endpoint adapter.'],
307
307
  invalidCombinations: ['`serverOnly` and `clientOnly` cannot both describe a useful result.'],
package/core/execute.js CHANGED
@@ -1796,6 +1796,8 @@ const HANDLERS = {
1796
1796
  method: normMethod,
1797
1797
  prefix: p.prefix || null,
1798
1798
  showUncertain: !p.hideUncertain,
1799
+ in: p.in,
1800
+ excludeTests: !!p.excludeTests,
1799
1801
  });
1800
1802
  if (p.framework != null && String(p.framework).trim() !== '') {
1801
1803
  const framework = String(p.framework).trim().toLowerCase();
@@ -1859,6 +1861,7 @@ const HANDLERS = {
1859
1861
  // Recompute meta after filtering
1860
1862
  result.meta = {
1861
1863
  totalRoutes: result.routes.length,
1864
+ testRoutes: result.routes.filter(r => r.isTest).length,
1862
1865
  totalRequests: result.requests.length,
1863
1866
  uncertainRequests: (result.uncertainRequests || []).length,
1864
1867
  totalBridges: result.bridges.length,
@@ -2286,7 +2289,7 @@ const HANDLERS = {
2286
2289
  const limit = num(p.limit, undefined);
2287
2290
  let note;
2288
2291
  if (limit && limit > 0 && result) {
2289
- const groups = ['functions', 'moduleLevelChanges', 'newFunctions', 'deletedFunctions'];
2292
+ const groups = ['functions', 'symbols', 'newSymbols', 'deletedSymbols', 'moduleLevelChanges', 'newFunctions', 'deletedFunctions'];
2290
2293
  const total = groups.reduce((sum, key) => sum + (result[key]?.length || 0), 0);
2291
2294
  if (total > limit) {
2292
2295
  let remaining = limit;
@@ -204,12 +204,18 @@ function formatDiffImpact(result, options = {}) {
204
204
 
205
205
  const s = result.summary || {};
206
206
  const parts = [];
207
- if (s.modifiedFunctions > 0) parts.push(`${s.modifiedFunctions} modified`);
208
- if (s.deletedFunctions > 0) parts.push(`${s.deletedFunctions} deleted`);
209
- if (s.newFunctions > 0) parts.push(`${s.newFunctions} new`);
207
+ for (const [label, suffix] of [['Functions', 'Functions'], ['Declarations', 'Symbols']]) {
208
+ const counts = ['modified', 'new', 'deleted']
209
+ .filter(kind => s[kind + suffix] > 0)
210
+ .map(kind => `${s[kind + suffix]} ${kind}`);
211
+ if (counts.length) parts.push(`${label}: ${counts.join(', ')}`);
212
+ }
210
213
  parts.push(`${s.totalCallSites || 0} call sites across ${s.affectedFiles || 0} files`);
211
214
  if (s.unverifiedCallSites > 0) parts.push(`${s.unverifiedCallSites} unverified`);
212
- lines.push(parts.join(', '));
215
+ if (s.totalDependencySites || s.unverifiedDependencySites) {
216
+ parts.push(`${s.totalDependencySites || 0} confirmed + ${s.unverifiedDependencySites || 0} unverified non-call dependency sites across ${s.dependencyFiles || 0} files`);
217
+ }
218
+ lines.push(parts.join('; '));
213
219
  // fix #283: changed paths outside supported source are invisible to the
214
220
  // symbol analysis — disclose instead of silently narrowing the diff.
215
221
  if (result.nonSourcePaths > 0) {
@@ -229,10 +235,10 @@ function formatDiffImpact(result, options = {}) {
229
235
  lines.push(` ${fn.relativePath}:${fn.startLine}`);
230
236
  lines.push(` ${fn.signature}`);
231
237
  if (fn.addedLines.length > 0) {
232
- lines.push(` Lines added: ${formatLineRanges(fn.addedLines)}`);
238
+ lines.push(` Added at lines: ${formatLineRanges(fn.addedLines)}`);
233
239
  }
234
240
  if (fn.deletedLines.length > 0) {
235
- lines.push(` Lines deleted: ${formatLineRanges(fn.deletedLines)}`);
241
+ lines.push(` Deleted at old lines: ${formatLineRanges(fn.deletedLines)}`);
236
242
  }
237
243
 
238
244
  if (fn.callers.length > 0) {
@@ -302,6 +308,24 @@ function formatDiffImpact(result, options = {}) {
302
308
  }
303
309
  }
304
310
 
311
+ for (const [key, title] of [['symbols', 'MODIFIED'], ['newSymbols', 'NEW'], ['deletedSymbols', 'DELETED']]) {
312
+ if (!result[key]?.length) continue;
313
+ lines.push(`\n${title} DECLARATIONS:`);
314
+ for (const symbol of result[key]) {
315
+ lines.push(` ${symbol.type} ${symbol.name} — ${symbol.relativePath}:${symbol.startLine}`);
316
+ if (symbol.impact) {
317
+ lines.push(require('./analysis').formatImpact(symbol.impact, { compact: true }));
318
+ } else {
319
+ const refs = symbol.remainingReferences || [];
320
+ lines.push(` Remaining name references: ${refs.length} unverified (deleted target)`);
321
+ for (const site of refs.slice(0, MAX_CALLERS_PER_FN)) {
322
+ lines.push(` ${site.file}:${site.line}: ${site.expression}`);
323
+ }
324
+ if (refs.length > MAX_CALLERS_PER_FN) lines.push(` (+${refs.length - MAX_CALLERS_PER_FN} more)`);
325
+ }
326
+ }
327
+ }
328
+
305
329
  // Module-level changes
306
330
  if (result.moduleLevelChanges.length > 0) {
307
331
  lines.push('\nMODULE-LEVEL CHANGES:');
@@ -837,8 +837,10 @@ function formatImpact(impact, options = {}) {
837
837
  }
838
838
 
839
839
  // By file (confirmed tier)
840
- if (!compact) lines.push('');
841
- lines.push('BY FILE:');
840
+ if (impact.byFile.length > 0) {
841
+ if (!compact) lines.push('');
842
+ lines.push('BY FILE:');
843
+ }
842
844
 
843
845
  // Evidence aggregate over ALL sites (replaces per-edge confidence lines)
844
846
  const allSites = impact.byFile.flatMap(g => g.sites);
@@ -881,7 +883,7 @@ function formatImpact(impact, options = {}) {
881
883
  for (const site of group.sites) {
882
884
  const caller = site.callerName ? ` [${site.callerName}]` : '';
883
885
  const expr = site.expression ? `: ${site.expression.replace(/\s+/g, ' ').slice(0, 100)}` : '';
884
- lines.push(` ${group.file}:${site.line}${caller}${expr}`);
886
+ lines.push(` ${group.file}:${site.line}${caller} [${site.accessKind || 'access'}${Number.isInteger(site.column) ? `, column ${site.column + 1}` : ''}]${expr}`);
885
887
  }
886
888
  }
887
889
  if (access.unverifiedSites.length > 0) {
@@ -889,7 +891,7 @@ function formatImpact(impact, options = {}) {
889
891
  for (const site of access.unverifiedSites.slice(0, 10)) {
890
892
  const caller = site.callerName ? ` [${site.callerName}]` : '';
891
893
  const expr = site.expression ? `: ${site.expression.replace(/\s+/g, ' ').slice(0, 100)}` : '';
892
- lines.push(` ${site.file}:${site.line}${caller}${expr}`);
894
+ lines.push(` ${site.file}:${site.line}${caller} [${site.accessKind || 'access'}${Number.isInteger(site.column) ? `, column ${site.column + 1}` : ''}]${expr}`);
893
895
  }
894
896
  if (access.unverifiedSites.length > 10) {
895
897
  lines.push(` (+${access.unverifiedSites.length - 10} more unverified)`);
@@ -29,15 +29,17 @@ function formatCheck(result) {
29
29
  if (result.trust.signatureMismatches) trustDetails.push(`${result.trust.signatureMismatches} signature mismatch(es)`);
30
30
  if (result.trust.filteredEdges) trustDetails.push(`${result.trust.filteredEdges} filtered edge(s)`);
31
31
  if (result.trust.usageReviewSymbols) trustDetails.push(`${result.trust.usageReviewSymbols} symbol(s) need usages review`);
32
+ if (result.trust.unvalidatedDeclarations) trustDetails.push(`${result.trust.unvalidatedDeclarations} declaration change(s) need toolchain validation`);
32
33
  if (trustDetails.length > 0) lines.push(` ${trustDetails.join(' · ')}`);
33
34
  }
34
35
 
35
36
  // Changed functions section
36
37
  const items = result.changed || [];
38
+ const noun = items.some(it => it.symbolType) ? 'symbol' : 'function';
37
39
  if (result.truncated) {
38
- lines.push(`Changed: ${items.length} of ${result.totalChanged} functions`);
40
+ lines.push(`Changed: ${items.length} of ${result.totalChanged} ${noun}s`);
39
41
  } else {
40
- lines.push(`Changed: ${items.length} function${items.length === 1 ? '' : 's'}`);
42
+ lines.push(`Changed: ${items.length} ${noun}${items.length === 1 ? '' : 's'}`);
41
43
  }
42
44
  if (items.length === 0) {
43
45
  lines.push(' (none — only non-function changes)');
@@ -47,6 +49,7 @@ function formatCheck(result) {
47
49
  if (it.kind && it.kind !== 'changed') tags.push(it.kind.toUpperCase());
48
50
  if (it.signatureMismatches > 0) tags.push(`SIG-DRIFT(${it.signatureMismatches})`);
49
51
  if (it.orphan) tags.push('ORPHAN');
52
+ if (it.symbolType) tags.push(it.symbolType);
50
53
  if (it.account && !it.account.textComplete) tags.push('ACCOUNT-INCOMPLETE');
51
54
  const tagStr = tags.length ? ' [' + tags.join(', ') + ']' : '';
52
55
  let callers = it.callerCount != null ? `${it.callerCount} caller${it.callerCount === 1 ? '' : 's'}` : '';
@@ -54,6 +57,7 @@ function formatCheck(result) {
54
57
  callers += ` (+${it.unverifiedCallerCount} unverified)`;
55
58
  }
56
59
  lines.push(` ${it.name} (${it.file}:${it.line})${tagStr} ${callers}`);
60
+ if (it.symbolType) lines.push(` Dependencies: ${it.dependencyCount || 0} confirmed, ${it.unverifiedDependencyCount || 0} unverified`);
57
61
  if (it.mismatches && it.mismatches.length > 0) {
58
62
  for (const m of it.mismatches.slice(0, 3)) {
59
63
  const where = m.file ? ` at ${m.file}:${m.line}` : '';
@@ -55,7 +55,7 @@ function formatUncertainRequests(result, options) {
55
55
  lines.push(`Possible client requests (${list.length}) — request-shaped call with a path literal, receiver not recognized as an HTTP client:`);
56
56
  const cap = options.all ? Infinity : 10;
57
57
  for (const r of list.slice(0, cap)) {
58
- lines.push(` ${r.file}:${r.line} ${r.receiver}.${r.method}(${JSON.stringify(r.path)}) in ${r.callerName}`);
58
+ lines.push(` ${r.file}:${r.line} ${r.receiver}.${r.method}(${JSON.stringify(r.path)}) in ${r.callerName}${r.isTest ? ' [test]' : ''}`);
59
59
  }
60
60
  if (list.length > cap) lines.push(` (+${list.length - cap} more — use --all)`);
61
61
  return lines;
@@ -88,7 +88,7 @@ function formatRoutesAndRequests(routes, requests, meta, options, advisory = nul
88
88
  for (const r of list) {
89
89
  const handler = r.handler || '<anonymous>';
90
90
  const fw = r.framework ? `[${r.framework}]` : '';
91
- lines.push(` ${pad(r.method, 7)} ${pad(r.path, 40)} → ${handler} ${fw} :${r.line}`);
91
+ lines.push(` ${pad(r.method, 7)} ${pad(r.path, 40)} → ${handler} ${fw}${r.isTest ? ' [test]' : ''} :${r.line}`);
92
92
  }
93
93
  lines.push('');
94
94
  }
@@ -115,7 +115,7 @@ function formatRoutesAndRequests(routes, requests, meta, options, advisory = nul
115
115
  const inferred = r.methodInferred ? '?' : '';
116
116
  const interp = r.interp ? ' (interp)' : '';
117
117
  const fw = r.framework ? `[${r.framework}]` : '';
118
- lines.push(` ${pad(r.method + inferred, 7)} ${pad(r.path + interp, 40)} from ${r.callerName} ${fw} :${r.line}`);
118
+ lines.push(` ${pad(r.method + inferred, 7)} ${pad(r.path + interp, 40)} from ${r.callerName} ${fw}${r.isTest ? ' [test]' : ''} :${r.line}`);
119
119
  }
120
120
  lines.push('');
121
121
  }
@@ -166,12 +166,12 @@ function formatBridges(bridges, unmatchedRoutes, unmatchedRequests, meta, option
166
166
 
167
167
  lines.push(`Matched (${sorted.length} routes):`);
168
168
  for (const { route, clients } of sorted) {
169
- lines.push(` ${pad(route.method, 7)} ${route.path} [${route.framework}] ${route.file}:${route.line}`);
169
+ lines.push(` ${pad(route.method, 7)} ${route.path} [${route.framework}]${route.isTest ? ' [test]' : ''} ${route.file}:${route.line}`);
170
170
  for (const b of clients) {
171
171
  const conf = b.confidence.toFixed(2);
172
172
  const tier = b.matchType.toUpperCase();
173
173
  const inf = b.methodInferred ? ' method?' : '';
174
- lines.push(` ↔ ${pad(b.request.method + inf, 9)} ${pad(b.request.path, 30)} ${tier} (${conf}) from ${b.request.callerName} ${b.request.file}:${b.request.line}`);
174
+ lines.push(` ↔ ${pad(b.request.method + inf, 9)} ${pad(b.request.path, 30)} ${tier} (${conf}) from ${b.request.callerName}${b.request.isTest ? ' [test]' : ''} ${b.request.file}:${b.request.line}`);
175
175
  }
176
176
  lines.push('');
177
177
  }
@@ -180,7 +180,7 @@ function formatBridges(bridges, unmatchedRoutes, unmatchedRequests, meta, option
180
180
  if (unmatchedRoutes.length > 0) {
181
181
  lines.push(`Unmatched server routes (${unmatchedRoutes.length}):`);
182
182
  for (const r of unmatchedRoutes) {
183
- lines.push(` ${pad(r.method, 7)} ${pad(r.path, 40)} → ${r.handler} [${r.framework}] ${r.file}:${r.line}`);
183
+ lines.push(` ${pad(r.method, 7)} ${pad(r.path, 40)} → ${r.handler} [${r.framework}]${r.isTest ? ' [test]' : ''} ${r.file}:${r.line}`);
184
184
  }
185
185
  lines.push('');
186
186
  }
@@ -190,7 +190,7 @@ function formatBridges(bridges, unmatchedRoutes, unmatchedRequests, meta, option
190
190
  for (const r of unmatchedRequests) {
191
191
  const inferred = r.methodInferred ? '?' : '';
192
192
  const interp = r.interp ? ' (interp)' : '';
193
- lines.push(` ${pad(r.method + inferred, 7)} ${pad(r.path + interp, 40)} from ${r.callerName} [${r.framework}] ${r.file}:${r.line}`);
193
+ lines.push(` ${pad(r.method + inferred, 7)} ${pad(r.path + interp, 40)} from ${r.callerName} [${r.framework}]${r.isTest ? ' [test]' : ''} ${r.file}:${r.line}`);
194
194
  }
195
195
  }
196
196
 
@@ -213,6 +213,7 @@ function formatEndpointsJson(result, options = {}) {
213
213
  file: r.file,
214
214
  line: r.line,
215
215
  framework: r.framework,
216
+ isTest: !!r.isTest,
216
217
  ...(r.classPrefix && { classPrefix: r.classPrefix }),
217
218
  });
218
219
  const trimReq = (r) => ({
@@ -225,6 +226,7 @@ function formatEndpointsJson(result, options = {}) {
225
226
  callerName: r.callerName,
226
227
  ...(r.callerStartLine && { callerStartLine: r.callerStartLine }),
227
228
  framework: r.framework,
229
+ isTest: !!r.isTest,
228
230
  ...(r.methodInferred && { methodInferred: true }),
229
231
  });
230
232
  const trimBridge = (b) => ({
@@ -250,6 +252,7 @@ function formatEndpointsJson(result, options = {}) {
250
252
  uncertainRequests: (result.uncertainRequests || []).map(r => ({
251
253
  receiver: r.receiver, method: r.method, path: r.path,
252
254
  file: r.file, line: r.line, callerName: r.callerName, reason: r.reason,
255
+ isTest: !!r.isTest,
253
256
  })),
254
257
  // In unmatched-only mode, the matched bridges array is suppressed
255
258
  // — consumers that want both should not pass --unmatched.
@@ -158,10 +158,13 @@ function formatFindDetailed(symbols, query, options = {}) {
158
158
  const confStr = confidence.level !== 'high' ? ` [${confidence.level}]` : '';
159
159
  const handle = formatSymbolHandle(s);
160
160
  const loc = handle || (s.relativePath + ':' + s.startLine);
161
+ const nameLocation = s.nameLine && s.nameLine !== s.startLine
162
+ ? `name token at ${s.relativePath || s.file}:${s.nameLine}` : '';
161
163
 
162
164
  if (compact) {
163
165
  // One line per result: "<handle> <sig> <usages?> <doc snippet?>"
164
166
  const parts = [`${loc} ${sig}${confStr}`];
167
+ if (nameLocation) parts.push(`[${nameLocation}]`);
165
168
  if (s.usageCounts !== undefined && s.usageCounts.total > 0) {
166
169
  const scope = sameNameDefinitionCounts.get(s.name) > 1 ? ' name-wide' : '';
167
170
  const label = s.usageCounts.complete === false
@@ -186,6 +189,7 @@ function formatFindDetailed(symbols, query, options = {}) {
186
189
  }
187
190
 
188
191
  lines.push(`${loc} ${sig}${confStr}`);
192
+ if (nameLocation) lines.push(` ${nameLocation} (handle starts at declaration line ${s.startLine})`);
189
193
  if (s.docstring) {
190
194
  const snip = firstSentenceShort(s.docstring);
191
195
  if (snip) lines.push(` "${snip}"`);
@@ -64,22 +64,39 @@ function accountComments(account) {
64
64
 
65
65
  function findRecords(result) {
66
66
  const out = [];
67
+ const notes = [];
67
68
  if (Array.isArray(result)) {
68
- for (const symbol of result) out.push(record(pathOf(symbol), symbol.startLine, signatureOf(symbol), symbol.type));
69
+ for (const symbol of result) {
70
+ out.push(record(pathOf(symbol), symbol.startLine, signatureOf(symbol), symbol.type));
71
+ if (symbol.nameLine && symbol.nameLine !== symbol.startLine) {
72
+ notes.push(`# ${record(pathOf(symbol), symbol.startLine, symbol.name)} starts at the declaration; name token at line ${symbol.nameLine} (usages reports the token line).`);
73
+ }
74
+ }
69
75
  } else if (result && Array.isArray(result.types)) {
70
76
  for (const type of result.types) {
71
77
  out.push(record(pathOf(type), type.startLine ?? type.line, type.name, type.type || type.kind));
72
78
  }
73
79
  }
74
- return { records: out, notes: [] };
80
+ return { records: out, notes };
75
81
  }
76
82
 
77
83
  function usagesRecords(result) {
78
84
  const out = [];
79
85
  const notes = [];
86
+ const byLine = new Map();
80
87
  for (const usage of Array.isArray(result) ? result : []) {
81
88
  const kind = usage.isDefinition ? 'definition' : (usage.usageType || 'reference');
82
- out.push(record(pathOf(usage), usage.line, usage.content, kind === 'call' ? '' : kind));
89
+ const key = `${pathOf(usage)}\0${usage.line}`;
90
+ if (!byLine.has(key)) byLine.set(key, { usage, kinds: new Set(), count: 0 });
91
+ const row = byLine.get(key);
92
+ row.kinds.add(kind);
93
+ row.count++;
94
+ }
95
+ for (const { usage, kinds, count } of byLine.values()) {
96
+ const tags = [...kinds].filter(kind => kind !== 'call');
97
+ if (kinds.has('call') && tags.length) tags.unshift('call');
98
+ if (count > 1) tags.push(`${count} occurrences`);
99
+ out.push(record(pathOf(usage), usage.line, usage.content, tags.join('; ')));
83
100
  }
84
101
  const counts = result && result.summaryCounts;
85
102
  if (counts && counts.hiddenTestUsages > 0) {
@@ -187,6 +204,17 @@ function impactRecords(result) {
187
204
  out.push(record(pathOf(site), site.line, site.content, 'unverified: deleted-target-name-match'));
188
205
  }
189
206
  }
207
+ for (const symbol of [...(result.symbols || []), ...(result.newSymbols || []), ...(result.deletedSymbols || [])]) {
208
+ out.push(record(symbol.relativePath, symbol.startLine, symbol.name, `${symbol.type} declaration change`));
209
+ if (symbol.impact) {
210
+ const nested = impactRecords(symbol.impact);
211
+ out.push(...nested.records);
212
+ notes.push(...nested.notes);
213
+ }
214
+ for (const site of symbol.remainingReferences || []) {
215
+ out.push(record(site.file, site.line, site.expression, 'unverified: deleted-target-name-match'));
216
+ }
217
+ }
190
218
  const summary = result.summary || {};
191
219
  notes.push(`# Diff: ${summary.modifiedFunctions || 0} modified, ${summary.newFunctions || 0} new, ${summary.deletedFunctions || 0} deleted functions; ${(result.moduleLevelChanges || []).length} file(s) with module-level changes.`);
192
220
  if (result.nonSourcePaths) notes.push(`# ${result.nonSourcePaths} changed path(s) outside supported source files not analyzed.`);
@@ -206,11 +234,11 @@ function impactRecords(result) {
206
234
  const accesses = result.propertyAccesses;
207
235
  for (const group of accesses.byFile || []) {
208
236
  for (const access of group.sites || []) {
209
- out.push(record(group.file, access.line, access.expression, 'property-access'));
237
+ out.push(record(group.file, access.line, access.expression, `property-access: ${access.accessKind || 'access'}${Number.isInteger(access.column) ? `, column ${access.column + 1}` : ''}`));
210
238
  }
211
239
  }
212
240
  for (const access of accesses.unverifiedSites || []) {
213
- out.push(record(pathOf(access), access.line, access.expression, `${unverifiedTag(access)}; property-access`));
241
+ out.push(record(pathOf(access), access.line, access.expression, `${unverifiedTag(access)}; property-access: ${access.accessKind || 'access'}${Number.isInteger(access.column) ? `, column ${access.column + 1}` : ''}`));
214
242
  }
215
243
  notes.push(`# PROPERTY ACCESS SITES: ${accesses.confirmedCount} confirmed, ${accesses.unverifiedCount} unverified, ${accesses.excluded?.total || 0} other-target (separate from caller ACCOUNT).`);
216
244
  }
@@ -284,7 +284,7 @@ function formatDeadcode(results, options = {}) {
284
284
  lines.push(`\n${extHint}`);
285
285
  }
286
286
  if (results.excludedRuntimeContract > 0) {
287
- lines.push(`\n${results.excludedRuntimeContract} Java serialization callback(s) hidden (JVM runtime contract, not dead).`);
287
+ lines.push(`\n${results.excludedRuntimeContract} runtime callback(s) hidden (language/runtime registrations, not dead).`);
288
288
  }
289
289
  if (results.pythonImplicitExportFiles > 0) {
290
290
  lines.push(`\nPython public-surface rule active in ${results.pythonImplicitExportFiles} file(s) without __all__: top-level non-underscore names are treated as externally reachable.`);
@@ -15,7 +15,7 @@ const BROAD_COMMANDS = new Set([
15
15
  ...[...BROAD_CANONICAL].map(toMcpName),
16
16
  ]);
17
17
 
18
- const CONTRACT_LINE_RE = /^\s*(?:(?:Summary|ACCOUNT|CONTRACT|WARNING|FILTERED|CALLEE ACCOUNT|TREE ACCOUNT):|\d+ test-file usage\(s\) hidden\b|(?:Note:\s*)?Found \d+ (?:definitions|fuzzy matches)\b)/;
18
+ const CONTRACT_LINE_RE = /^\s*(?:(?:Summary|ACCOUNT|CONTRACT|WARNING|FILTERED|CALLEE ACCOUNT|TREE ACCOUNT|Note):|\d+ test-file usage\(s\) hidden\b|Found \d+ (?:definitions|fuzzy matches)\b)/;
19
19
  const MAX_PRESERVED_CONTRACT_LINES = 24;
20
20
  const MAX_PRESERVED_CONTRACT_CHARS = 8000;
21
21
 
@@ -132,29 +132,31 @@ function applyOutputBudget(text, {
132
132
  all = false,
133
133
  surface = 'cli',
134
134
  params = {},
135
+ trailingChars = 0,
135
136
  } = {}) {
137
+ const defaultLimit = BROAD_COMMANDS.has(command)
138
+ ? BROAD_OUTPUT_CHARS
139
+ : DEFAULT_OUTPUT_CHARS;
140
+ const requested = maxChars || (all ? MAX_OUTPUT_CHARS : defaultLimit);
141
+ const hardLimit = Math.min(requested, MAX_OUTPUT_CHARS);
142
+ const limit = Math.max(0, hardLimit - trailingChars);
136
143
  if (!text) {
137
144
  return {
138
- text: '(no output)',
139
- truncated: false,
145
+ text: '(no output)'.slice(0, limit),
146
+ truncated: '(no output)'.length > limit,
140
147
  fullChars: 0,
141
- requestedLimit: maxChars || null,
148
+ requestedLimit: hardLimit,
142
149
  contractMetadata: [],
143
150
  contractMetadataComplete: true,
144
151
  };
145
152
  }
146
153
 
147
- const defaultLimit = BROAD_COMMANDS.has(command)
148
- ? BROAD_OUTPUT_CHARS
149
- : DEFAULT_OUTPUT_CHARS;
150
- const requested = maxChars || (all ? MAX_OUTPUT_CHARS : defaultLimit);
151
- const limit = Math.min(requested, MAX_OUTPUT_CHARS);
152
154
  if (text.length <= limit) {
153
155
  return {
154
156
  text,
155
157
  truncated: false,
156
158
  fullChars: text.length,
157
- requestedLimit: limit,
159
+ requestedLimit: hardLimit,
158
160
  contractMetadata: [],
159
161
  contractMetadataComplete: true,
160
162
  };
@@ -178,8 +180,8 @@ function applyOutputBudget(text, {
178
180
  ? `Raise ${raiseHint}.`
179
181
  : `Narrow with ${compactScope || raiseHint}; raise ${raiseHint}.`;
180
182
  let notice = compactBudget
181
- ? `... OUTPUT TRUNCATED (${text.length}→${limit}). ${compactGuidance}`
182
- : `... OUTPUT TRUNCATED: ${text.length} chars total; hard limit ${limit}. ` +
183
+ ? `... OUTPUT TRUNCATED (${text.length}→${hardLimit}). ${compactGuidance}`
184
+ : `... OUTPUT TRUNCATED: ${text.length} chars total; hard limit ${hardLimit}. ` +
183
185
  `${narrowingHint(command, surface, params)} ${allHint}`;
184
186
  if (compactBudget && notice.length > limit) {
185
187
  const emergency = supportsAll
@@ -194,10 +196,21 @@ function applyOutputBudget(text, {
194
196
  // can consume most of the transport. Trust/account lines take precedence
195
197
  // over body detail and are appended directly after a compact notice.
196
198
  if (compactBudget) {
197
- const metadataCapacity = Math.max(0, limit - notice.length - 1);
198
- const candidate = preservedContractMetadata(text, '', {
199
- maxChars: metadataCapacity,
199
+ let candidate = preservedContractMetadata(text, '', {
200
+ maxChars: Math.max(0, limit - notice.length - 1),
200
201
  });
202
+ if (candidate.omitted > 0) {
203
+ // Spend less on generic guidance when that lets a complete scope
204
+ // or parameter warning survive even a tiny transport ceiling.
205
+ const shorterNotice = `... OUTPUT TRUNCATED. Raise ${raiseHint}.`;
206
+ const shorterCandidate = preservedContractMetadata(text, '', {
207
+ maxChars: Math.max(0, limit - shorterNotice.length - 1),
208
+ });
209
+ if (shorterNotice.length < notice.length && shorterCandidate.lines.length > candidate.lines.length) {
210
+ notice = shorterNotice;
211
+ candidate = shorterCandidate;
212
+ }
213
+ }
201
214
  const candidateText = candidate.lines.join('\n');
202
215
  const separatorChars = candidateText ? 2 : 1;
203
216
  const bodyBudget = Math.max(0,
@@ -225,7 +238,7 @@ function applyOutputBudget(text, {
225
238
  text: pieces.join('\n').slice(0, limit),
226
239
  truncated: true,
227
240
  fullChars: text.length,
228
- requestedLimit: limit,
241
+ requestedLimit: hardLimit,
229
242
  contractMetadata: contractMetadata.lines,
230
243
  contractMetadataComplete: contractMetadata.complete,
231
244
  };
@@ -283,7 +296,7 @@ function applyOutputBudget(text, {
283
296
  text: rendered,
284
297
  truncated: true,
285
298
  fullChars: text.length,
286
- requestedLimit: limit,
299
+ requestedLimit: hardLimit,
287
300
  contractMetadata: contractMetadata.lines,
288
301
  contractMetadataComplete: contractMetadata.complete,
289
302
  };
package/core/registry.js CHANGED
@@ -149,7 +149,7 @@ const FLAG_APPLICABILITY = {
149
149
  repo: ['file', 'exclude', 'top', 'limit', 'all', 'detailed', 'topLevel', 'in', 'functions', 'hot', 'deep', 'sections'],
150
150
  deadcode: ['file', 'exclude', 'includeTests', 'includeExported', 'includeDecorated', 'limit', 'in'],
151
151
  entrypoints: ['file', 'exclude', 'includeTests', 'excludeTests', 'limit', 'type', 'framework'],
152
- endpoints: ['file', 'exclude', 'limit', 'framework', 'bridge', 'serverOnly', 'clientOnly', 'unmatched', 'method', 'prefix', 'hideUncertain'],
152
+ endpoints: ['file', 'in', 'exclude', 'excludeTests', 'limit', 'framework', 'bridge', 'serverOnly', 'clientOnly', 'unmatched', 'method', 'prefix', 'hideUncertain'],
153
153
  stacktrace: ['stack'],
154
154
  auditAsync: ['file', 'exclude', 'limit'],
155
155
  };