ucn 5.4.1 → 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/cli/index.js CHANGED
@@ -933,9 +933,9 @@ Common flags:
933
933
  --all --compact --no-compact --json --include-tests --class-name=X --line=N
934
934
  --range=N-M (source with --file=PATH)
935
935
  --base=REF --staged --no-cache --clear-cache [--all] --max-files=N --workers=N
936
- --max-chars=N (text output; default 10K targeted / 3K broad, ceiling 100K)
936
+ --max-chars=N (text characters, not UTF-8 bytes; default 10K targeted / 3K broad, ceiling 100K)
937
937
  --lines find/usages/search/show/impact: grep -n shape, one path:line:text
938
- record per output line; usages may repeat a source line per occurrence
938
+ record per output line; usages combines occurrences on each source line
939
939
  (tags after a tab: # unverified: <reason>, # import,
940
940
  # callee); accounting and notes go to stderr as "# " lines; exit 1
941
941
  when nothing matched; exit 2 on errors. No default result cap.
@@ -204,18 +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`);
210
- if (s.modifiedSymbols || s.newSymbols || s.deletedSymbols) {
211
- parts.push(`${s.modifiedSymbols || 0} modified, ${s.newSymbols || 0} new, ${s.deletedSymbols || 0} deleted non-callable declarations`);
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
212
  }
213
213
  parts.push(`${s.totalCallSites || 0} call sites across ${s.affectedFiles || 0} files`);
214
214
  if (s.unverifiedCallSites > 0) parts.push(`${s.unverifiedCallSites} unverified`);
215
215
  if (s.totalDependencySites || s.unverifiedDependencySites) {
216
216
  parts.push(`${s.totalDependencySites || 0} confirmed + ${s.unverifiedDependencySites || 0} unverified non-call dependency sites across ${s.dependencyFiles || 0} files`);
217
217
  }
218
- lines.push(parts.join(', '));
218
+ lines.push(parts.join('; '));
219
219
  // fix #283: changed paths outside supported source are invisible to the
220
220
  // symbol analysis — disclose instead of silently narrowing the diff.
221
221
  if (result.nonSourcePaths > 0) {
@@ -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,14 +64,20 @@ 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) {
@@ -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
 
@@ -134,23 +134,23 @@ function applyOutputBudget(text, {
134
134
  params = {},
135
135
  trailingChars = 0,
136
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);
137
143
  if (!text) {
138
144
  return {
139
- text: '(no output)',
140
- truncated: false,
145
+ text: '(no output)'.slice(0, limit),
146
+ truncated: '(no output)'.length > limit,
141
147
  fullChars: 0,
142
- requestedLimit: maxChars || null,
148
+ requestedLimit: hardLimit,
143
149
  contractMetadata: [],
144
150
  contractMetadataComplete: true,
145
151
  };
146
152
  }
147
153
 
148
- const defaultLimit = BROAD_COMMANDS.has(command)
149
- ? BROAD_OUTPUT_CHARS
150
- : DEFAULT_OUTPUT_CHARS;
151
- const requested = maxChars || (all ? MAX_OUTPUT_CHARS : defaultLimit);
152
- const hardLimit = Math.min(requested, MAX_OUTPUT_CHARS);
153
- const limit = Math.max(0, hardLimit - trailingChars);
154
154
  if (text.length <= limit) {
155
155
  return {
156
156
  text,
@@ -196,10 +196,21 @@ function applyOutputBudget(text, {
196
196
  // can consume most of the transport. Trust/account lines take precedence
197
197
  // over body detail and are appended directly after a compact notice.
198
198
  if (compactBudget) {
199
- const metadataCapacity = Math.max(0, limit - notice.length - 1);
200
- const candidate = preservedContractMetadata(text, '', {
201
- maxChars: metadataCapacity,
199
+ let candidate = preservedContractMetadata(text, '', {
200
+ maxChars: Math.max(0, limit - notice.length - 1),
202
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
+ }
203
214
  const candidateText = candidate.lines.join('\n');
204
215
  const separatorChars = candidateText ? 2 : 1;
205
216
  const bodyBudget = Math.max(0,
package/mcp/server.js CHANGED
@@ -119,7 +119,7 @@ const server = new StdioMcpServer({
119
119
 
120
120
  function toolResult(text, command, maxChars, suffixNote, params = {}) {
121
121
  const suffix = suffixNote || '';
122
- const budget = applyOutputBudget(text, {
122
+ const budget = applyOutputBudget(text + suffix, {
123
123
  command,
124
124
  maxChars,
125
125
  surface: 'mcp',
@@ -130,7 +130,7 @@ function toolResult(text, command, maxChars, suffixNote, params = {}) {
130
130
  // narrowing hints; preserved contract lines follow). A structuredContent
131
131
  // side-channel is rendered INSTEAD of content by MCP clients that prefer
132
132
  // structured results, which discards the entire answer (fix #284).
133
- return { content: [{ type: 'text', text: budget.text + suffix }] };
133
+ return { content: [{ type: 'text', text: budget.text }] };
134
134
  }
135
135
 
136
136
  function toolError(message) {
@@ -307,7 +307,7 @@ const INPUT_SHAPE = {
307
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 }),
308
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 }),
309
309
  max_files: integerParam('Max files to index (default: 10000). Use for very large codebases. Must be a positive integer.', { exclusiveMinimum: 0, maximum: 10000000 }),
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 }),
310
+ max_chars: integerParam('Max output characters, not UTF-8 bytes, including notes and preserved metadata. 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 }),
311
311
  type: stringParam('Symbol type filter for structural search: function, class, call, method, type, state, field, constant, macro. Triggers index-based search.'),
312
312
  param: stringParam('Filter by parameter name or type (structural search). E.g. "Request", "ctx".'),
313
313
  receiver: stringParam('Filter calls by receiver (structural search, type=call). E.g. "db", "http".'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucn",
3
- "version": "5.4.1",
3
+ "version": "5.4.2",
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",