ucn 5.3.7 → 5.3.8

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.
@@ -126,7 +126,7 @@ answers:
126
126
  ucn find handleRequest --lines # path:line:signature # kind
127
127
  ucn show handleRequest --lines # callers as path:line:text; unverified ones end in "\t# unverified: <reason>"
128
128
  ucn show handleRequest --lines --sections=callees
129
- ucn usages handleRequest --lines # every literal-name line; non-call kinds tagged "# import" / "# definition"
129
+ ucn usages handleRequest --lines # every literal-name occurrence; non-call kinds tagged "# import" / "# definition"
130
130
  ucn search 'retry(' --lines # grep -n output, code-aware scope
131
131
  ucn impact handleRequest --lines
132
132
  ucn source handleRequest --raw # the code and nothing else, ready for an exact-string edit
@@ -136,6 +136,12 @@ ucn source src/server.js:40-80 --raw
136
136
  Records go to stdout; the `ACCOUNT` / `CONTRACT` lines, notes, and the
137
137
  same-name disambiguation go to stderr prefixed `# ` (MCP keeps them in the one
138
138
  text block, and `--raw` appends its note as one trailing `# ` line there).
139
+ `usages` emits one record per occurrence, so a source line may repeat; deduplicate
140
+ `path:line` values for line counts. Definition handles start at decorators when
141
+ present, while usages point at token lines (`nameLine` identifies the declaration
142
+ token when it differs from `startLine`). Structural `search --unused` keeps its
143
+ safety note and decorator tags in shell output; runtime registrations can appear
144
+ and zero call edges do not prove a symbol is safe to delete.
139
145
  `--lines` lists the whole band without default row/character caps, so pipe through
140
146
  `grep -v '# unverified'` for the confirmed tier or `cut -d: -f1 | sort | uniq -c`
141
147
  for callers per file. Nothing to list prints nothing and exits 1, grep's
@@ -33,7 +33,7 @@ structural or code-only search.
33
33
  |---|---|
34
34
  | `repo` | Repository orientation. Select `summary,files,stats,health` with `--sections`; `--deep` includes readiness evidence. Skipped unsupported source is listed with a grep/language-tool handoff. |
35
35
  | `deps <file>` | File dependency graph. Use `--direction=imports\|importers\|both`, `--detailed`, or `--cycles`. Cycles distinguish eager edges from function-local, Python typing-guarded, and TypeScript type-only edges. Complete cycle groups remain visible when enumeration is capped. |
36
- | `api [file]` | Static exported/public surface for a project or file. |
36
+ | `api [file]` | Static exported/public surface for a project or file. An exact file includes tests; broader scans exclude tests with a count. Use `--include-tests` to include them. |
37
37
  | `entrypoints` | Framework, route, task, test, and runtime entry points. |
38
38
  | `endpoints` | Server/client HTTP surface; `--bridge` adds advisory matching. |
39
39
 
@@ -49,6 +49,14 @@ structural or code-only search.
49
49
 
50
50
  Symbol-listing commands emit handles such as `src/api.ts:42:handler`. Pass the full handle to symbol commands. `path:line` also works. Handles prevent same-named definitions from being silently combined.
51
51
 
52
+ Definition handles and source spans start at the first decorator or annotation when present; literal usages point at the actual token line. Use a symbol's `nameLine` (when present, otherwise `startLine`) to compare declaration tokens with usages.
53
+
54
+ Structural `search --param` matches parameter names, types, and defaults; `--returns` matches return annotations. Both exclude AST comments and preserve string contents. `--unused` lists callable symbols without call edges, not safe-delete candidates; decorated runtime registrations may still appear. Its safety note and decorator tags are retained in `--lines` output. Use `deadcode` and `usages` before deletion.
55
+
56
+ `repo` summary/stats `buildTime` is the duration of the last index build (discovery, parsing, and graphs), retained in the cache. It excludes cache loading/saving and query execution, so it is not command wall time; `buildTimeNote` states this boundary.
57
+
58
+ `--lines` writes one record per output line. `usages` records occurrences, so multiple tokens on the same source line can produce repeated `path:line` values. Deduplicate those values when counting source lines.
59
+
52
60
  ## Common flags
53
61
 
54
62
  | Flag | Meaning |
package/cli/index.js CHANGED
@@ -917,7 +917,8 @@ Commands:
917
917
  deps <file> File graph; --direction=imports|importers|both
918
918
  --detailed Include import declarations
919
919
  deps --cycles Report circular dependencies (no file target)
920
- api [file] Project or file public API
920
+ api [file] Project or file public API (exact files include tests;
921
+ broader scans exclude tests unless --include-tests)
921
922
  check [symbol] Signature check; without symbol, precommit check
922
923
  plan <symbol> Preview rename or parameter edits
923
924
  entrypoints Runtime and framework entry points
@@ -933,7 +934,8 @@ Common flags:
933
934
  --base=REF --staged --no-cache --clear-cache [--all] --max-files=N --workers=N
934
935
  --max-chars=N (text output; default 10K targeted / 3K broad, ceiling 100K)
935
936
  --lines find/usages/search/show/impact: grep -n shape, one path:line:text
936
- record per line (tags after a tab: # unverified: <reason>, # import,
937
+ record per output line; usages may repeat a source line per occurrence
938
+ (tags after a tab: # unverified: <reason>, # import,
937
939
  # callee); accounting and notes go to stderr as "# " lines; exit 1
938
940
  when nothing matched; exit 2 on errors. No default result cap.
939
941
  show defaults to callers; --sections=callers,callees selects bands.
package/core/cache.js CHANGED
@@ -712,7 +712,8 @@ function clearAllCaches() {
712
712
  // copied bindings and qualified macro receivers; TS indexed array evidence.
713
713
  // v225 (fix #357): Rust `use path::name as local` bindings record the original name with a paired `renames` alias.
714
714
  // v226: bundled/minified filename exclusions are disclosed in discoveryIssues.
715
- const CACHE_FORMAT_VERSION = 226;
715
+ // v227: signature parameter/return text excludes AST comments in every language.
716
+ const CACHE_FORMAT_VERSION = 227;
716
717
  const USAGE_CACHE_FILE = 'usage-results.json';
717
718
 
718
719
  /**
package/core/execute.js CHANGED
@@ -2330,7 +2330,7 @@ const HANDLERS = {
2330
2330
  },
2331
2331
 
2332
2332
  api: (index, p) => {
2333
- if (p.file) {
2333
+ if (p.file && typeof index.resolveFilePathForQuery(p.file) !== 'string') {
2334
2334
  const fileErr = checkFilePatternMatch(index, p.file);
2335
2335
  if (fileErr) return { ok: false, error: fileErr };
2336
2336
  }
@@ -2346,7 +2346,7 @@ const HANDLERS = {
2346
2346
  return { ok: false, error: `No files matched the 'in' directory filter '${p.in}'.` };
2347
2347
  }
2348
2348
  }
2349
- let result = index.api(p.file, { in: p.in });
2349
+ let result = index.api(p.file, { in: p.in, includeTests: p.includeTests });
2350
2350
  if (p.file) {
2351
2351
  const fileErr = checkFileError(result, p.file, index);
2352
2352
  if (fileErr) return { ok: false, error: fileErr };
@@ -2374,6 +2374,10 @@ const HANDLERS = {
2374
2374
  }
2375
2375
  result = items;
2376
2376
  }
2377
+ if (result.apiInfo?.excludedTestFiles > 0) {
2378
+ const excluded = `${result.apiInfo.excludedTestFiles} test file(s) excluded from API; use --include-tests to include them, or name an exact file.`;
2379
+ note = note ? `${note}\n${excluded}` : excluded;
2380
+ }
2377
2381
  return { ok: true, result, note };
2378
2382
  },
2379
2383
 
package/core/graph.js CHANGED
@@ -564,6 +564,8 @@ function api(index, filePath, options = {}) {
564
564
  const results = [];
565
565
  let scopedFiles = 0;
566
566
  let pythonImplicitFiles = 0;
567
+ let excludedTestFiles = 0;
568
+ let explicitFile = false;
567
569
 
568
570
  let fileIterator;
569
571
  if (filePath) {
@@ -573,6 +575,7 @@ function api(index, filePath, options = {}) {
573
575
  const fileEntry = index.files.get(resolved);
574
576
  if (!fileEntry) return { error: 'file-not-found', filePath };
575
577
  fileIterator = [[resolved, fileEntry]];
578
+ explicitFile = true;
576
579
  } else {
577
580
  // Fall back to pattern filter (substring match on relative path)
578
581
  const matches = [];
@@ -596,7 +599,8 @@ function api(index, filePath, options = {}) {
596
599
  }
597
600
 
598
601
  // Skip test files by default (test classes aren't part of public API)
599
- if (!options.includeTests && isTestFile(fileEntry.relativePath, fileEntry.language)) {
602
+ if (!explicitFile && !options.includeTests && isTestFile(fileEntry.relativePath, fileEntry.language)) {
603
+ excludedTestFiles++;
600
604
  continue;
601
605
  }
602
606
  scopedFiles++;
@@ -612,7 +616,7 @@ function api(index, filePath, options = {}) {
612
616
  results.sort((a, b) => codeUnitCompare(a.file, b.file) ||
613
617
  (a.startLine - b.startLine) || codeUnitCompare(a.name, b.name));
614
618
  Object.defineProperty(results, 'apiInfo', {
615
- value: { scopedFiles, pythonImplicitFiles },
619
+ value: { scopedFiles, pythonImplicitFiles, excludedTestFiles },
616
620
  enumerable: false, writable: true, configurable: true,
617
621
  });
618
622
  return results;
@@ -33,7 +33,7 @@ function formatDoctor(result, options = {}) {
33
33
  // Cache state
34
34
  if (result.cache) {
35
35
  const state = result.cache.fresh === true ? 'fresh' : result.cache.fresh === false ? 'stale' : 'unknown';
36
- const buildHint = result.cache.buildMs ? `, ${result.cache.buildMs}ms build` : '';
36
+ const buildHint = result.cache.buildMs ? `, ${result.cache.buildMs}ms last index build (excludes cache I/O and query execution)` : '';
37
37
  lines.push(`Cache: ${state}${buildHint}`);
38
38
  }
39
39
  if (result.commandTrust) {
@@ -29,7 +29,8 @@ function record(pathLike, line, text, tag = '') {
29
29
  // Keep unusual filenames from becoming notes or extra physical records.
30
30
  let file = String(pathLike).replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t');
31
31
  if (file.startsWith('# ')) file = './' + file;
32
- return `${file}:${line == null ? 0 : line}:${body}${tag ? `\t# ${tag}` : ''}`;
32
+ const tagText = String(tag || '').replace(/\s+/g, ' ').trim();
33
+ return `${file}:${line == null ? 0 : line}:${body}${tagText ? `\t# ${tagText}` : ''}`;
33
34
  }
34
35
 
35
36
  function commentLines(text) {
@@ -95,9 +96,14 @@ function searchRecords(result) {
95
96
  if (result && !Array.isArray(result) && Array.isArray(result.results)) {
96
97
  for (const item of result.results) {
97
98
  const text = item.params != null ? `${item.name}(${item.params})` : item.name;
98
- out.push(record(item.file, item.line, text, item.kind || item.type));
99
+ const decorators = (item.decorators || []).map(d => `@${String(d).replace(/^@/, '')}`).join(', ');
100
+ const tag = [item.kind || item.type, decorators].filter(Boolean).join('; ');
101
+ out.push(record(item.file, item.line, text, tag));
99
102
  }
100
103
  const meta = result.meta;
104
+ if (meta?.query?.unused) {
105
+ notes.push(...commentLines(require('./search').unusedSearchNote()));
106
+ }
101
107
  if (meta && meta.totalMatched > meta.shown) {
102
108
  notes.push(`# ${meta.totalMatched - meta.shown} more match(es) (--limit=N / --all)`);
103
109
  }
@@ -424,6 +424,9 @@ function formatPublicJson(command, result, params = {}, execution = {}) {
424
424
  if (result.meta.truncatedMatches > 0) commandMeta.truncated = true;
425
425
  if (result.unsupportedMatches) commandMeta.unsupportedMatches = result.unsupportedMatches;
426
426
  }
427
+ if (command === 'api' && result?.apiInfo) {
428
+ commandMeta.apiInfo = result.apiInfo;
429
+ }
427
430
  if (command === 'entrypoints' && result?.filterInfo) {
428
431
  commandMeta.hiddenTestEntrypoints = result.filterInfo.hiddenTests;
429
432
  commandMeta.testsIncluded = result.filterInfo.testsIncluded;
@@ -125,7 +125,7 @@ function formatStats(stats, options = {}) {
125
125
  lines.push(`Files: ${stats.files}`);
126
126
  }
127
127
  lines.push(`Symbols: ${stats.symbols}`);
128
- lines.push(`Build time: ${stats.buildTime}ms`);
128
+ lines.push(`Last index build: ${stats.buildTime}ms (excludes cache I/O and query execution; reused from cache)`);
129
129
 
130
130
  lines.push('\nBy Language:');
131
131
  for (const [lang, info] of Object.entries(stats.byLanguage)) {
@@ -87,6 +87,10 @@ function formatSearchJson(results, term) {
87
87
  /**
88
88
  * Format structural search results (index-based queries)
89
89
  */
90
+ function unusedSearchNote(flag = '--unused') {
91
+ return `${flag} lists callable symbols with no resolved call edge; it does not assess type/field/reference liveness and is not safe-delete proof. Confirm with deadcode and usages.`;
92
+ }
93
+
90
94
  function formatStructuralSearch(result, options = {}) {
91
95
  const { results, meta } = result;
92
96
  const lines = [];
@@ -106,7 +110,7 @@ function formatStructuralSearch(result, options = {}) {
106
110
  lines.push(`Structural search: ${queryStr}`);
107
111
  lines.push('═'.repeat(60));
108
112
  if (meta.query.unused) {
109
- lines.push(`NOTE: ${options.unusedFlag || '--unused'} lists callable symbols with no resolved call edge; it does not assess type/field/reference liveness and is not safe-delete proof. Confirm with deadcode and usages.`);
113
+ lines.push(`NOTE: ${unusedSearchNote(options.unusedFlag)}`);
110
114
  lines.push('');
111
115
  }
112
116
 
@@ -447,6 +451,7 @@ function formatTestsJson(tests, name) {
447
451
  }
448
452
 
449
453
  module.exports = {
454
+ unusedSearchNote,
450
455
  formatSearch,
451
456
  formatSearchJson,
452
457
  formatStructuralSearch,
package/core/registry.js CHANGED
@@ -143,7 +143,7 @@ const FLAG_APPLICABILITY = {
143
143
  impact: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'top', 'unreachableOnly', 'compact', 'base', 'staged', 'limit', 'all', 'lines'],
144
144
  tests: ['name', 'file', 'exclude', 'className', 'line', 'callsOnly', 'depth', 'includeMethods', 'all'],
145
145
  deps: ['file', 'exclude', 'depth', 'direction', 'all', 'detailed', 'cycles'],
146
- api: ['file', 'in', 'limit'],
146
+ api: ['file', 'in', 'limit', 'includeTests'],
147
147
  check: ['name', 'file', 'className', 'line', 'includeMethods', 'base', 'staged', 'limit'],
148
148
  plan: ['name', 'file', 'className', 'line', 'addParam', 'removeParam', 'renameTo', 'defaultValue'],
149
149
  repo: ['file', 'exclude', 'top', 'limit', 'all', 'detailed', 'topLevel', 'in', 'functions', 'hot', 'deep', 'sections'],
package/core/reporting.js CHANGED
@@ -48,6 +48,7 @@ function getStats(index, options = {}) {
48
48
  files: scopedFiles.length,
49
49
  symbols: totalSymbols, // Total symbol count, not unique names
50
50
  buildTime: index.buildTime,
51
+ buildTimeNote: 'Last index build, including discovery and graphs; excludes cache I/O and query execution. Reused when loading a cached index.',
51
52
  byLanguage: {},
52
53
  byType: {},
53
54
  ...(index.truncated && { truncated: index.truncated })
@@ -1044,6 +1045,7 @@ function orient(index, options = {}) {
1044
1045
  files: stats.files,
1045
1046
  symbols: stats.symbols,
1046
1047
  buildTime: stats.buildTime,
1048
+ buildTimeNote: stats.buildTimeNote,
1047
1049
  byLanguage: stats.byLanguage,
1048
1050
  dirs,
1049
1051
  hot: {
package/core/search.js CHANGED
@@ -883,7 +883,10 @@ function structuralSearch(index, options = {}) {
883
883
  // expression position — never "unused" (the deadcode
884
884
  // twin of the bodyScopedName audit skip).
885
885
  if (def.bodyScopedName) continue;
886
- index.buildCalleeIndex();
886
+ // buildCalleeIndex rebuilds the whole project. Reuse
887
+ // the eagerly built (or cache-loaded) index, including
888
+ // across every candidate in this operation.
889
+ if (!index.calleeIndex) index.buildCalleeIndex();
887
890
  // A name whose every call site is its own recursion
888
891
  // has zero callers (fix #253c — the deadcode
889
892
  // carve-out, applied here). Class-kind names are
@@ -13,6 +13,7 @@ const { typeOrigin } = require('./type-evidence');
13
13
 
14
14
  const {
15
15
  traverseTree,
16
+ nodeTextWithoutComments,
16
17
  traverseTreeCached,
17
18
  nodeToLocation,
18
19
  extractJSDocstring,
@@ -989,7 +990,7 @@ function paramTypeText(param, identity) {
989
990
  const defaultValue = param.childForFieldName('default_value');
990
991
  const end = defaultValue ? defaultValue.startIndex : param.endIndex;
991
992
  if (identity.nameNode.startIndex < base || identity.nameNode.endIndex > end) return null;
992
- const text = param.text.slice(0, end - base);
993
+ const text = nodeTextWithoutComments(param).slice(0, end - base);
993
994
  const typeText = (text.slice(0, identity.nameNode.startIndex - base) +
994
995
  text.slice(identity.nameNode.endIndex - base))
995
996
  .replace(/\s+/g, ' ')
@@ -1014,10 +1015,10 @@ function structuredParams(paramsNode) {
1014
1015
  // their type text alone — the type must not double as both name and
1015
1016
  // annotation, and `void *` must not collapse into the `(void)` form.
1016
1017
  const info = {
1017
- name: identity.name || param.text.replace(/\s+/g, ' ').trim(),
1018
+ name: identity.name || nodeTextWithoutComments(param).replace(/\s+/g, ' ').trim(),
1018
1019
  };
1019
1020
  if (typeNode && identity.name) {
1020
- info.type = paramTypeText(param, identity) || typeNode.text;
1021
+ info.type = paramTypeText(param, identity) || nodeTextWithoutComments(typeNode);
1021
1022
  }
1022
1023
  if (param.type === 'optional_parameter_declaration') info.optional = true;
1023
1024
  let declaratorCursor = declarator;
@@ -1164,7 +1165,7 @@ function returnTypeOf(node) {
1164
1165
  const descriptor = (current.namedChildren || []).find(child =>
1165
1166
  child.type === 'type_descriptor') || current.namedChild(0);
1166
1167
  const type = descriptor?.childForFieldName('type') || descriptor;
1167
- return type?.text || null;
1168
+ return nodeTextWithoutComments(type) || null;
1168
1169
  }
1169
1170
  for (const child of current.namedChildren || []) {
1170
1171
  const found = findTrailing(child);
@@ -1193,7 +1194,7 @@ function returnTypeOf(node) {
1193
1194
  current = current.childForFieldName('declarator') ||
1194
1195
  (current.namedChildren || []).find(child => child.type.endsWith('_declarator'));
1195
1196
  }
1196
- return stars > 0 ? `${typeNode.text} ${'*'.repeat(stars)}` : typeNode.text;
1197
+ return stars > 0 ? `${nodeTextWithoutComments(typeNode)} ${'*'.repeat(stars)}` : nodeTextWithoutComments(typeNode);
1197
1198
  }
1198
1199
 
1199
1200
  function memberFromNode(node, className, access, lines, mode) {
@@ -1209,7 +1210,7 @@ function memberFromNode(node, className, access, lines, mode) {
1209
1210
  if (isConstructor && identity.name.startsWith('~')) modifiers.push('destructor');
1210
1211
  return {
1211
1212
  name: identity.name,
1212
- params: paramsNode ? paramsNode.text.replace(/^\(|\)$/g, '').trim() : '...',
1213
+ params: paramsNode ? nodeTextWithoutComments(paramsNode).replace(/^\(|\)$/g, '').trim() : '...',
1213
1214
  paramsStructured: structuredParams(paramsNode),
1214
1215
  returnType: isConstructor ? null :
1215
1216
  (identity.conversionType || returnTypeOf(node)),
@@ -1510,7 +1511,7 @@ function findFunctionsInTree(code, tree, mode, sourceLines = null) {
1510
1511
  : null;
1511
1512
  functions.push({
1512
1513
  name: identity.name,
1513
- params: paramsNode ? paramsNode.text.replace(/^\(|\)$/g, '').trim() : '...',
1514
+ params: paramsNode ? nodeTextWithoutComments(paramsNode).replace(/^\(|\)$/g, '').trim() : '...',
1514
1515
  paramsStructured: structuredParams(paramsNode),
1515
1516
  returnType: isConstructor ? null :
1516
1517
  (identity.conversionType || returnTypeOf(node)),
@@ -1800,7 +1801,7 @@ function findMacrosInTree(tree, lines, parser) {
1800
1801
  startLine,
1801
1802
  endLine,
1802
1803
  indent,
1803
- params: paramsNode ? paramsNode.text.replace(/^\(|\)$/g, '').trim() : undefined,
1804
+ params: paramsNode ? nodeTextWithoutComments(paramsNode).replace(/^\(|\)$/g, '').trim() : undefined,
1804
1805
  paramsStructured: paramsNode
1805
1806
  ? (paramsNode.namedChildren || [])
1806
1807
  .filter(child => child.type === 'identifier')
@@ -2,6 +2,7 @@
2
2
 
3
3
  const {
4
4
  traverseTree,
5
+ nodeTextWithoutComments,
5
6
  traverseTreeCached,
6
7
  nodeToLocation,
7
8
  extractJSDocstring,
@@ -127,7 +128,7 @@ function structuredParams(paramsNode) {
127
128
  if (nameNode) {
128
129
  recoveredParams.push({
129
130
  name: nameNode.text,
130
- ...(typeNode && { type: typeNode.text }),
131
+ ...(typeNode && { type: nodeTextWithoutComments(typeNode) }),
131
132
  rest: true,
132
133
  });
133
134
  }
@@ -138,14 +139,14 @@ function structuredParams(paramsNode) {
138
139
  const typeNode = param.childForFieldName('type');
139
140
  if (!nameNode) continue;
140
141
  const info = { name: nameNode.text };
141
- if (typeNode) info.type = typeNode.text;
142
+ if (typeNode) info.type = nodeTextWithoutComments(typeNode);
142
143
  if (modifiersOf(param).includes('this')) info.extensionReceiver = true;
143
144
  if (param.type === 'parameter_array') info.rest = true;
144
145
  const value = param.childForFieldName('value') ||
145
146
  param.namedChildren.find(child => child !== nameNode && child !== typeNode &&
146
- !['attribute_list', 'modifier'].includes(child.type));
147
+ !['attribute_list', 'modifier', 'comment'].includes(child.type));
147
148
  if (value) {
148
- info.default = value.text;
149
+ info.default = nodeTextWithoutComments(value);
149
150
  info.optional = true;
150
151
  }
151
152
  params.push(info);
@@ -208,9 +209,9 @@ function memberFromNode(node, className, lines) {
208
209
  const paramsStructured = structuredParams(paramsNode);
209
210
  return {
210
211
  name,
211
- params: paramsNode ? paramsNode.text.replace(/^\(|\)$/g, '').trim() : '...',
212
+ params: paramsNode ? nodeTextWithoutComments(paramsNode).replace(/^\(|\)$/g, '').trim() : '...',
212
213
  paramsStructured,
213
- returnType: isConstructor ? null : returnNode?.text || null,
214
+ returnType: isConstructor ? null : nodeTextWithoutComments(returnNode).trim() || null,
214
215
  startLine,
215
216
  endLine,
216
217
  indent,
@@ -263,9 +264,9 @@ function indexerMember(node, className, lines) {
263
264
  const { startLine, endLine, indent } = nodeToLocation(node, lines);
264
265
  return {
265
266
  name: 'this[]',
266
- params: paramsNode ? paramsNode.text.replace(/^\[|\]$/g, '').trim() : '...',
267
+ params: paramsNode ? nodeTextWithoutComments(paramsNode).replace(/^\[|\]$/g, '').trim() : '...',
267
268
  paramsStructured: structuredParams(paramsNode),
268
- returnType: typeNode?.text || null,
269
+ returnType: nodeTextWithoutComments(typeNode).trim() || null,
269
270
  startLine,
270
271
  endLine,
271
272
  indent,
@@ -495,9 +496,9 @@ function findFunctions(code, parser) {
495
496
  const modifiers = modifiersOf(node);
496
497
  functions.push({
497
498
  name: nameNode.text,
498
- params: paramsNode ? paramsNode.text.replace(/^\(|\)$/g, '').trim() : '...',
499
+ params: paramsNode ? nodeTextWithoutComments(paramsNode).replace(/^\(|\)$/g, '').trim() : '...',
499
500
  paramsStructured: structuredParams(paramsNode),
500
- returnType: returnNode?.text || null,
501
+ returnType: nodeTextWithoutComments(returnNode).trim() || null,
501
502
  startLine,
502
503
  endLine,
503
504
  indent,
package/languages/go.js CHANGED
@@ -10,6 +10,7 @@ const { ReceiverTypeMap, typeOrigin } = require('./type-evidence');
10
10
 
11
11
  const {
12
12
  traverseTree,
13
+ nodeTextWithoutComments,
13
14
  traverseTreeCached,
14
15
  nodeToLocation,
15
16
  parseStructuredParams,
@@ -29,7 +30,7 @@ function parseTree(parser, code) {
29
30
  function extractReturnType(node) {
30
31
  const resultNode = node.childForFieldName('result');
31
32
  if (resultNode) {
32
- return resultNode.text.trim() || null;
33
+ return nodeTextWithoutComments(resultNode).trim() || null;
33
34
  }
34
35
  return null;
35
36
  }
@@ -39,7 +40,7 @@ function extractReturnedFunctionResult(node) {
39
40
  const resultNode = node.childForFieldName('result');
40
41
  if (resultNode?.type !== 'function_type') return null;
41
42
  const innerResult = resultNode.childForFieldName('result');
42
- return innerResult?.text.trim() || null;
43
+ return nodeTextWithoutComments(innerResult).trim() || null;
43
44
  }
44
45
 
45
46
  /**
@@ -51,7 +52,7 @@ function extractGoParams(paramsNode) {
51
52
  // unknown signatures in JSON output (fix #238; the shared
52
53
  // utils.extractParams already had this fix).
53
54
  if (!paramsNode) return '...';
54
- const text = paramsNode.text;
55
+ const text = nodeTextWithoutComments(paramsNode);
55
56
  return text.replace(/^\(|\)$/g, '').trim();
56
57
  }
57
58
 
@@ -115,7 +116,7 @@ function _processFunction(node, functions, processedRanges, lines) {
115
116
  indent,
116
117
  modifiers: isExported ? ['export'] : [],
117
118
  isFunctionVariable: true,
118
- ...(resultNode?.text.trim() && { returnType: resultNode.text.trim() }),
119
+ ...(resultNode && { returnType: nodeTextWithoutComments(resultNode).trim() || null }),
119
120
  });
120
121
  }
121
122
  return true;
@@ -491,11 +492,11 @@ function extractInterfaceMembers(interfaceNode, codeOrLines) {
491
492
  } else if (sub.type === 'parameter_list') {
492
493
  hasParams = true;
493
494
  if (!paramsText) {
494
- paramsText = sub.text.slice(1, -1); // strip parens
495
+ paramsText = nodeTextWithoutComments(sub).slice(1, -1); // strip parens
495
496
  paramsNode = sub;
496
497
  } else {
497
498
  // Second parameter_list is the return type tuple
498
- returnType = sub.text;
499
+ returnType = nodeTextWithoutComments(sub);
499
500
  }
500
501
  }
501
502
  }
@@ -514,7 +515,7 @@ function extractInterfaceMembers(interfaceNode, codeOrLines) {
514
515
  for (let j = 0; j < child.namedChildCount; j++) {
515
516
  const sub = child.namedChild(j);
516
517
  if (returnTypeNodes.has(sub.type) && sub.text !== nameText) {
517
- returnType = sub.text;
518
+ returnType = nodeTextWithoutComments(sub);
518
519
  }
519
520
  }
520
521
  }
package/languages/java.js CHANGED
@@ -10,6 +10,7 @@ const { ReceiverTypeMap, typeOrigin } = require('./type-evidence');
10
10
 
11
11
  const {
12
12
  traverseTree,
13
+ nodeTextWithoutComments,
13
14
  traverseTreeCached,
14
15
  nodeToLocation,
15
16
  parseStructuredParams,
@@ -32,7 +33,7 @@ function extractJavaParams(paramsNode) {
32
33
  // unknown signatures in JSON output (fix #241; go/rust got this in #238,
33
34
  // the shared utils.extractParams already had it).
34
35
  if (!paramsNode) return '...';
35
- const text = paramsNode.text;
36
+ const text = nodeTextWithoutComments(paramsNode);
36
37
  let params = text.replace(/^\(|\)$/g, '').trim();
37
38
  return params;
38
39
  }
@@ -201,7 +202,7 @@ function stripJavaString(text) {
201
202
  function extractReturnType(node) {
202
203
  const typeNode = node.childForFieldName('type');
203
204
  if (typeNode) {
204
- return typeNode.text;
205
+ return nodeTextWithoutComments(typeNode);
205
206
  }
206
207
  return null;
207
208
  }
@@ -10,6 +10,7 @@ const { ReceiverTypeMap, typeOrigin } = require('./type-evidence');
10
10
 
11
11
  const {
12
12
  traverseTree,
13
+ nodeTextWithoutComments,
13
14
  traverseTreeCached,
14
15
  nodeToLocation,
15
16
  extractParams,
@@ -34,7 +35,7 @@ function parseTree(parser, code) {
34
35
  function extractReturnType(node) {
35
36
  const returnTypeNode = node.childForFieldName('return_type');
36
37
  if (returnTypeNode) {
37
- let text = returnTypeNode.text.trim();
38
+ let text = nodeTextWithoutComments(returnTypeNode).trim();
38
39
  if (text.startsWith(':')) {
39
40
  text = text.slice(1).trim();
40
41
  }
@@ -10,6 +10,7 @@ const { ReceiverTypeMap, typeOrigin } = require('./type-evidence');
10
10
 
11
11
  const {
12
12
  traverseTree,
13
+ nodeTextWithoutComments,
13
14
  traverseTreeCached,
14
15
  nodeToLocation,
15
16
  parseStructuredParams,
@@ -32,7 +33,7 @@ function parseTree(parser, code) {
32
33
  function extractReturnType(node) {
33
34
  const returnTypeNode = node.childForFieldName('return_type');
34
35
  if (returnTypeNode) {
35
- let text = returnTypeNode.text.trim();
36
+ let text = nodeTextWithoutComments(returnTypeNode).trim();
36
37
  if (text.startsWith('->')) {
37
38
  text = text.slice(2).trim();
38
39
  }
@@ -129,7 +130,7 @@ function extractPythonParams(paramsNode) {
129
130
  // unknown signatures in JSON output (fix #241; go/rust got this in #238,
130
131
  // the shared utils.extractParams already had it).
131
132
  if (!paramsNode) return '...';
132
- const text = paramsNode.text;
133
+ const text = nodeTextWithoutComments(paramsNode);
133
134
  let params = text.replace(/^\(|\)$/g, '').trim();
134
135
  return params;
135
136
  }
package/languages/rust.js CHANGED
@@ -10,6 +10,7 @@ const { ReceiverTypeMap, typeOrigin } = require('./type-evidence');
10
10
 
11
11
  const {
12
12
  traverseTree,
13
+ nodeTextWithoutComments,
13
14
  traverseTreeCached,
14
15
  nodeToLocation,
15
16
  parseStructuredParams,
@@ -128,7 +129,7 @@ function declarationTrees(code, parser) {
128
129
  function extractReturnType(node) {
129
130
  const returnTypeNode = node.childForFieldName('return_type');
130
131
  if (returnTypeNode) {
131
- let text = returnTypeNode.text.trim();
132
+ let text = nodeTextWithoutComments(returnTypeNode).trim();
132
133
  if (text.startsWith('->')) {
133
134
  text = text.slice(2).trim();
134
135
  }
@@ -198,7 +199,7 @@ function extractRustParams(paramsNode) {
198
199
  // unknown signatures in JSON output (fix #238; the shared
199
200
  // utils.extractParams already had this fix).
200
201
  if (!paramsNode) return '...';
201
- const text = paramsNode.text;
202
+ const text = nodeTextWithoutComments(paramsNode);
202
203
  return text.replace(/^\(|\)$/g, '').trim();
203
204
  }
204
205
 
@@ -54,11 +54,32 @@ function extractParams(paramsNode) {
54
54
  // functions that actually take zero arguments. Empty → '' so callers can
55
55
  // render `main()` cleanly.
56
56
  if (!paramsNode) return '...';
57
- const text = paramsNode.text;
57
+ const text = nodeTextWithoutComments(paramsNode);
58
58
  const stripped = text.replace(/^\(|\)$/g, '').trim();
59
59
  return stripped; // '' for empty params, '...' only when paramsNode missing
60
60
  }
61
61
 
62
+ /**
63
+ * Signature text without AST comment nodes. Keep source offsets/line breaks
64
+ * intact for declarator slicing, and preserve strings (including forward
65
+ * annotations and defaults containing comment-like text) verbatim.
66
+ */
67
+ function nodeTextWithoutComments(node) {
68
+ if (!node) return '';
69
+ const text = node.text;
70
+ const parts = [];
71
+ let offset = 0;
72
+ traverseTree(node, child => {
73
+ if (child.type !== 'comment' && !child.type.endsWith('_comment')) return true;
74
+ const start = child.startIndex - node.startIndex;
75
+ const end = child.endIndex - node.startIndex;
76
+ parts.push(text.slice(offset, start), text.slice(start, end).replace(/[^\r\n]/g, ' '));
77
+ offset = end;
78
+ return false;
79
+ });
80
+ return parts.length ? parts.join('') + text.slice(offset) : text;
81
+ }
82
+
62
83
  /**
63
84
  * Parse parameters into structured format
64
85
  * @param {object} paramsNode - Tree-sitter parameters node
@@ -140,7 +161,7 @@ function parseStructuredParams(paramsNode, language) {
140
161
 
141
162
  function parseJSParam(param, info) {
142
163
  if (param.type === 'identifier') {
143
- info.name = param.text;
164
+ info.name = nodeTextWithoutComments(param);
144
165
  } else if (param.type === 'required_parameter' || param.type === 'optional_parameter') {
145
166
  const patternNode = param.childForFieldName('pattern');
146
167
  const typeNode = param.childForFieldName('type');
@@ -148,18 +169,18 @@ function parseJSParam(param, info) {
148
169
  // Check if pattern is a rest_pattern (e.g., ...args inside required_parameter)
149
170
  if (patternNode.type === 'rest_pattern') {
150
171
  const innerName = patternNode.namedChild(0);
151
- info.name = innerName ? innerName.text : patternNode.text.replace(/^\.\.\./, '');
172
+ info.name = innerName ? nodeTextWithoutComments(innerName) : nodeTextWithoutComments(patternNode).replace(/^\.\.\./, '');
152
173
  info.rest = true;
153
174
  } else {
154
- info.name = patternNode.text;
175
+ info.name = nodeTextWithoutComments(patternNode);
155
176
  }
156
177
  }
157
- if (typeNode) info.type = typeNode.text.replace(/^:\s*/, '');
178
+ if (typeNode) info.type = nodeTextWithoutComments(typeNode).replace(/^:\s*/, '');
158
179
  if (param.type === 'optional_parameter') info.optional = true;
159
180
  // Check for default value (e.g., priority: number = 1)
160
181
  const valueNode = param.childForFieldName('value');
161
182
  if (valueNode) {
162
- info.default = valueNode.text;
183
+ info.default = nodeTextWithoutComments(valueNode);
163
184
  info.optional = true;
164
185
  } else if (!info.rest) {
165
186
  // Also check for bare number/string/etc. children as defaults.
@@ -170,14 +191,14 @@ function parseJSParam(param, info) {
170
191
  // wrecking expectedArgs.min and the signature display).
171
192
  const NON_DEFAULT_PARAM_CHILDREN = new Set([
172
193
  'identifier', 'type_annotation', 'rest_pattern',
173
- 'accessibility_modifier', 'override_modifier', 'readonly', 'decorator',
194
+ 'accessibility_modifier', 'override_modifier', 'readonly', 'decorator', 'comment',
174
195
  ]);
175
196
  for (let i = 0; i < param.namedChildCount; i++) {
176
197
  const child = param.namedChild(i);
177
198
  if (child !== patternNode && child !== (typeNode && typeNode.parent === param ? typeNode : null) &&
178
199
  !NON_DEFAULT_PARAM_CHILDREN.has(child.type)) {
179
200
  // This is likely a default value node
180
- info.default = child.text;
201
+ info.default = nodeTextWithoutComments(child);
181
202
  info.optional = true;
182
203
  break;
183
204
  }
@@ -186,27 +207,27 @@ function parseJSParam(param, info) {
186
207
  } else if (param.type === 'rest_parameter' || param.type === 'rest_pattern') {
187
208
  // rest_parameter = TypeScript, rest_pattern = JavaScript
188
209
  const patternNode = param.childForFieldName('pattern') || param.namedChild(0);
189
- if (patternNode) info.name = patternNode.text;
210
+ if (patternNode) info.name = nodeTextWithoutComments(patternNode);
190
211
  info.rest = true;
191
212
  } else if (param.type === 'assignment_pattern') {
192
213
  const leftNode = param.childForFieldName('left');
193
214
  const rightNode = param.childForFieldName('right');
194
- if (leftNode) info.name = leftNode.text;
195
- if (rightNode) info.default = rightNode.text;
215
+ if (leftNode) info.name = nodeTextWithoutComments(leftNode);
216
+ if (rightNode) info.default = nodeTextWithoutComments(rightNode);
196
217
  } else if (param.type === 'object_pattern' || param.type === 'array_pattern') {
197
218
  // Destructured params: { name, value } or [a, b]
198
- info.name = param.text;
219
+ info.name = nodeTextWithoutComments(param);
199
220
  }
200
221
  }
201
222
 
202
223
  function parsePythonParam(param, info) {
203
224
  if (param.type === 'identifier') {
204
- info.name = param.text;
225
+ info.name = nodeTextWithoutComments(param);
205
226
  } else if (param.type === 'typed_parameter') {
206
227
  const nameNode = param.namedChild(0);
207
228
  const typeNode = param.childForFieldName('type');
208
- if (nameNode) info.name = nameNode.text;
209
- if (typeNode) info.type = typeNode.text;
229
+ if (nameNode) info.name = nodeTextWithoutComments(nameNode);
230
+ if (typeNode) info.type = nodeTextWithoutComments(typeNode);
210
231
  // Python wraps annotated splats in typed_parameter, with the actual
211
232
  // `*args` / `**kwargs` node as its first named child. Treating those
212
233
  // as ordinary required parameters makes every short call look broken.
@@ -218,12 +239,12 @@ function parsePythonParam(param, info) {
218
239
  const nameNode = param.childForFieldName('name');
219
240
  const valueNode = param.childForFieldName('value');
220
241
  const typeNode = param.childForFieldName('type');
221
- if (nameNode) info.name = nameNode.text;
222
- if (valueNode) info.default = valueNode.text;
223
- if (typeNode) info.type = typeNode.text;
242
+ if (nameNode) info.name = nodeTextWithoutComments(nameNode);
243
+ if (valueNode) info.default = nodeTextWithoutComments(valueNode);
244
+ if (typeNode) info.type = nodeTextWithoutComments(typeNode);
224
245
  info.optional = true;
225
246
  } else if (param.type === 'list_splat_pattern' || param.type === 'dictionary_splat_pattern') {
226
- info.name = param.text;
247
+ info.name = nodeTextWithoutComments(param);
227
248
  info.rest = true;
228
249
  }
229
250
  }
@@ -237,11 +258,11 @@ function parseGoParam(param, info) {
237
258
  for (let i = 0; i < param.namedChildCount; i++) {
238
259
  const child = param.namedChild(i);
239
260
  if (child && child.type === 'identifier') {
240
- names.push(child.text);
261
+ names.push(nodeTextWithoutComments(child));
241
262
  }
242
263
  }
243
264
  if (names.length > 0) info.name = names[0];
244
- if (typeNode) info.type = typeNode.text;
265
+ if (typeNode) info.type = nodeTextWithoutComments(typeNode);
245
266
  // Interface method declarations commonly omit parameter names:
246
267
  // `Match(*http.Request, *RouteMatch) bool`. These are still two real
247
268
  // signature slots. Dropping them made verify/plan see zero arguments
@@ -249,7 +270,7 @@ function parseGoParam(param, info) {
249
270
  // slot. Preserve the authored type as the display token while marking
250
271
  // it unnamed so signature consumers can distinguish it from a name.
251
272
  if (names.length === 0 && typeNode) {
252
- info.name = typeNode.text;
273
+ info.name = nodeTextWithoutComments(typeNode);
253
274
  info.unnamed = true;
254
275
  delete info.type;
255
276
  }
@@ -261,9 +282,9 @@ function parseGoParam(param, info) {
261
282
  // Go variadic: `args ...int`
262
283
  const nameNode = param.childForFieldName('name');
263
284
  const typeNode = param.childForFieldName('type');
264
- if (nameNode) info.name = nameNode.text;
285
+ if (nameNode) info.name = nodeTextWithoutComments(nameNode);
265
286
  else info.name = '...';
266
- if (typeNode) info.type = '...' + typeNode.text;
287
+ if (typeNode) info.type = '...' + nodeTextWithoutComments(typeNode);
267
288
  info.rest = true;
268
289
  }
269
290
  }
@@ -272,10 +293,10 @@ function parseRustParam(param, info) {
272
293
  if (param.type === 'parameter') {
273
294
  const patternNode = param.childForFieldName('pattern');
274
295
  const typeNode = param.childForFieldName('type');
275
- if (patternNode) info.name = patternNode.text;
276
- if (typeNode) info.type = typeNode.text;
296
+ if (patternNode) info.name = nodeTextWithoutComments(patternNode);
297
+ if (typeNode) info.type = nodeTextWithoutComments(typeNode);
277
298
  } else if (param.type === 'self_parameter') {
278
- info.name = param.text;
299
+ info.name = nodeTextWithoutComments(param);
279
300
  }
280
301
  }
281
302
 
@@ -294,8 +315,8 @@ function parseJavaParam(param, info) {
294
315
  }
295
316
  }
296
317
  }
297
- if (nameNode) info.name = nameNode.text;
298
- if (typeNode) info.type = typeNode.text;
318
+ if (nameNode) info.name = nodeTextWithoutComments(nameNode);
319
+ if (typeNode) info.type = nodeTextWithoutComments(typeNode);
299
320
  if (param.type === 'spread_parameter') info.rest = true;
300
321
  }
301
322
  }
@@ -1051,6 +1072,7 @@ function sameNode(a, b) {
1051
1072
  }
1052
1073
 
1053
1074
  module.exports = {
1075
+ nodeTextWithoutComments,
1054
1076
  sameNode,
1055
1077
  traverseTree,
1056
1078
  traverseTreeCached,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "5.3.7",
3
+ "version": "5.3.8",
4
4
  "mcpName": "io.github.mleoca/ucn",
5
5
  "description": "Auditable AST code intelligence for AI agents: 18 task-oriented commands through one MCP tool, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java, C, C++, C#, and HTML.",
6
6
  "main": "index.js",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "scripts": {
12
12
  "version": "node scripts/sync-server-version.js && git add server.json",
13
- "test": "node --test test/audit-5.3.6.test.js test/evidence-provenance.test.js test/provenance-unit.test.js test/shell-output.test.js test/parser-unit.test.js test/integration.test.js test/cache.test.js test/formatter.test.js test/interactive.test.js test/feature.test.js test/regression-js.test.js test/regression-py.test.js test/regression-go.test.js test/regression-java.test.js test/regression-rust.test.js test/regression-c-family.test.js test/regression-cross.test.js test/regression-mcp.test.js test/mcp-protocol.test.js test/mcp-sdk-compat.test.js test/dependency-security.test.js test/regression-parser.test.js test/regression-commands.test.js test/regression-fixes.test.js test/regression-bugfixes.test.js test/release-surface-regressions.test.js test/prerelease-audit.test.js test/release-readiness-audit.test.js test/cross-language.test.js test/accuracy.test.js test/command-coverage.test.js test/perf-optimizations.test.js test/performance-gate-policy.test.js test/oracle-gate-policy.test.js test/outcome-policy.test.js test/consistency-eval.test.js test/agent-public-surface-benchmark.test.js test/command-contracts.test.js test/language-adapter.test.js test/systematic-test.js test/mcp-edge-cases.js test/conservation.test.js test/parity-test.js test/trust-matrix.test.js",
13
+ "test": "node --test test/audit-5.3.7.test.js test/audit-5.3.6.test.js test/evidence-provenance.test.js test/provenance-unit.test.js test/shell-output.test.js test/parser-unit.test.js test/integration.test.js test/cache.test.js test/formatter.test.js test/interactive.test.js test/feature.test.js test/regression-js.test.js test/regression-py.test.js test/regression-go.test.js test/regression-java.test.js test/regression-rust.test.js test/regression-c-family.test.js test/regression-cross.test.js test/regression-mcp.test.js test/mcp-protocol.test.js test/mcp-sdk-compat.test.js test/dependency-security.test.js test/regression-parser.test.js test/regression-commands.test.js test/regression-fixes.test.js test/regression-bugfixes.test.js test/release-surface-regressions.test.js test/prerelease-audit.test.js test/release-readiness-audit.test.js test/cross-language.test.js test/accuracy.test.js test/command-coverage.test.js test/perf-optimizations.test.js test/performance-gate-policy.test.js test/oracle-gate-policy.test.js test/outcome-policy.test.js test/consistency-eval.test.js test/agent-public-surface-benchmark.test.js test/command-contracts.test.js test/language-adapter.test.js test/systematic-test.js test/mcp-edge-cases.js test/conservation.test.js test/parity-test.js test/trust-matrix.test.js",
14
14
  "benchmark:agent": "node test/agent-public-surface-benchmark.js",
15
15
  "benchmark:agent:gate": "node test/agent-public-surface-benchmark.js --gate",
16
16
  "benchmark:agent:legacy": "node test/agent-understanding-benchmark.js",