ucn 5.3.6 → 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.
@@ -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/mcp/server.js CHANGED
@@ -45,6 +45,7 @@ function getIndex(projectDir, options) {
45
45
  }
46
46
  const maxFiles = options && options.maxFiles;
47
47
  const followSymlinks = options && options.followSymlinks;
48
+ const includeBundled = options?.includeBundled === true;
48
49
  const absDir = path.resolve(projectDir);
49
50
  if (!fs.existsSync(absDir) || !fs.statSync(absDir).isDirectory()) {
50
51
  throw new Error(`Project directory not found: ${absDir}`);
@@ -54,7 +55,7 @@ function getIndex(projectDir, options) {
54
55
 
55
56
  // Always check staleness — MCP is used in iterative agent loops where
56
57
  // files change between requests, so a throttle causes stale results.
57
- if (cached && !maxFiles) {
58
+ if (cached && !maxFiles && !includeBundled) {
58
59
  if (!cached.index.isCacheStale()) {
59
60
  cached.checkedAt = Date.now();
60
61
  return cached.index;
@@ -66,12 +67,13 @@ function getIndex(projectDir, options) {
66
67
  const buildOpts = { quiet: true, forceRebuild: false };
67
68
  if (maxFiles) buildOpts.maxFiles = maxFiles;
68
69
  if (followSymlinks === false) buildOpts.followSymlinks = false;
69
- const loaded = index.loadCache();
70
- if (loaded && !maxFiles && !index.isCacheStale()) {
70
+ buildOpts.includeBundled = includeBundled;
71
+ const loaded = !includeBundled && index.loadCache();
72
+ if (loaded && !index.includeBundled && !maxFiles && !index.isCacheStale()) {
71
73
  // Disk cache is fresh (skip when maxFiles is set — cached index may have different file count)
72
74
  } else {
73
75
  buildOpts.forceRebuild = !!loaded;
74
- if (maxFiles) {
76
+ if (maxFiles || includeBundled) {
75
77
  index.build(null, buildOpts); // Don't pollute disk cache with partial indexes
76
78
  } else {
77
79
  // Cross-process build lock (fix #354): a CLI or another MCP
@@ -96,7 +98,7 @@ function getIndex(projectDir, options) {
96
98
  }
97
99
 
98
100
  // Don't cache partial indexes (maxFiles) — they'd serve wrong results for full queries
99
- if (!maxFiles) {
101
+ if (!maxFiles && !includeBundled) {
100
102
  indexCache.set(root, { index, checkedAt: Date.now() });
101
103
  }
102
104
  return index;
@@ -303,7 +305,7 @@ const INPUT_SHAPE = {
303
305
  top_level: booleanParam('repo files: show only top-level functions.'),
304
306
  class_name: stringParam('Class name to scope method analysis (e.g. "MarketDataFetcher" for close)'),
305
307
  line: integerParam('Definition line pin. Resolves the symbol defined at this exact line (the middle component of a file:line:name handle). Disambiguates same-file same-name definitions.', { exclusiveMinimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
306
- limit: integerParam('Max results to return (default: 500). Caps find, usages, search, deadcode, api, and repo files. Must be a positive integer.', { exclusiveMinimum: 0, maximum: 1000000 }),
308
+ limit: integerParam('Max results to return (default: 500; structural search: 50; usages and lines: uncapped). Caps find, usages, search, deadcode, api, and repo files. Must be a positive integer.', { exclusiveMinimum: 0, maximum: 1000000 }),
307
309
  max_files: integerParam('Max files to index (default: 10000). Use for very large codebases. Must be a positive integer.', { exclusiveMinimum: 0, maximum: 10000000 }),
308
310
  max_chars: integerParam('Max output chars before truncation. Broad sweep commands (repo, entrypoints, endpoints, deadcode, deps, check, audit_async) default to 3K; all other commands default to 10K. Maximum: 100K. all=true lifts formatter caps but keeps the 100K transport ceiling.', { exclusiveMinimum: 0, maximum: 100000 }),
309
311
  type: stringParam('Symbol type filter for structural search: function, class, call, method, type, state, field, constant, macro. Triggers index-based search.'),
@@ -315,6 +317,7 @@ const INPUT_SHAPE = {
315
317
  unused: booleanParam('Only symbols with zero callers (structural search).'),
316
318
  framework: stringParam('Filter entrypoints by framework (e.g. "express", "spring", "flask"). Comma-separated for multiple.'),
317
319
  follow_symlinks: booleanParam('Follow symlinks during file discovery (default: true)'),
320
+ include_bundled: booleanParam('Index *.min.js and *.bundle.js files (default: false). Source maps remain disclosed but unindexed. Bypasses the shared cache.'),
318
321
  bridge: booleanParam('Match server routes to client requests (endpoints command).'),
319
322
  server_only: booleanParam('Only list server routes (endpoints command).'),
320
323
  client_only: booleanParam('Only list client requests (endpoints command).'),
@@ -400,7 +403,7 @@ server.registerTool(
400
403
  if (applicable) {
401
404
  // Truly global options — apply to all commands (build/display control).
402
405
  // Command-specific params (name, term, stack, range, etc.) are in FLAG_APPLICABILITY.
403
- const coreParams = new Set(['maxChars', 'maxFiles', 'followSymlinks']);
406
+ const coreParams = new Set(['maxChars', 'maxFiles', 'followSymlinks', 'includeBundled']);
404
407
  for (const key of Object.keys(ep)) {
405
408
  if (coreParams.has(key)) continue;
406
409
  if (!applicable.includes(key) && ep[key] !== undefined &&
@@ -512,7 +515,8 @@ server.registerTool(
512
515
  // so we save here to avoid re-parsing all files on every MCP session.
513
516
  // MED-1: also persist when reachability was computed in-process so
514
517
  // long-lived MCP servers carry the BFS result forward to disk.
515
- if (index && (index.callsCacheDirty || index.reachabilityDirty || index.computedDispatchDirty)) {
518
+ if (index && !ep.includeBundled && !ep.maxFiles &&
519
+ (index.callsCacheDirty || index.reachabilityDirty || index.computedDispatchDirty)) {
516
520
  try { index.saveCache(); } catch (_) { /* best-effort */ }
517
521
  index.callsCacheDirty = false;
518
522
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "5.3.6",
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/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",