ucn 5.2.2 → 5.3.0

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.
@@ -0,0 +1,259 @@
1
+ /**
2
+ * core/output/lines.js - grep-shaped and raw output modes (fix #341).
3
+ *
4
+ * `--lines`: one `path:line:text` record per line — the `grep -n` shape — so
5
+ * `ucn` composes with head/cut/xargs and reads like the tool agents already
6
+ * reach for in a shell. Everything that is not a record (ACCOUNT/CONTRACT
7
+ * lines, disambiguation, notes) follows the records as `# ` comment lines:
8
+ * the CLI routes those to stderr, MCP keeps them in its single text block.
9
+ * Records outside the confirmed tier carry a trailing `\t# tag`, so the
10
+ * `path:line:` prefix stays parseable while the tier stays visible.
11
+ *
12
+ * `--raw` (source): the code text and nothing else — no header, no gutter —
13
+ * so an agent can extract pristine text for an exact-string edit.
14
+ */
15
+ 'use strict';
16
+
17
+ const { CALLABLE_SYMBOL_KINDS } = require('../shared');
18
+ const { formatSurfaceMessage } = require('../registry');
19
+ const { formatAccountLines, formatCalleeAccountLine } = require('./analysis');
20
+
21
+ const LINES_COMMANDS = new Set(['find', 'usages', 'search', 'show', 'impact']);
22
+
23
+ function record(pathLike, line, text, tag = '') {
24
+ // One record per line is the contract: a multi-line signature or a
25
+ // wrapped call expression folds onto one line, and the source line's
26
+ // indentation is dropped (a locate result needs the text, `--raw` has
27
+ // the layout).
28
+ const body = String(text == null ? '' : text).replace(/\s*\n\s*/g, ' ').trim();
29
+ // Keep unusual filenames from becoming notes or extra physical records.
30
+ let file = String(pathLike).replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t');
31
+ if (file.startsWith('# ')) file = './' + file;
32
+ return `${file}:${line == null ? 0 : line}:${body}${tag ? `\t# ${tag}` : ''}`;
33
+ }
34
+
35
+ function commentLines(text) {
36
+ return String(text || '').split(/\r?\n/).filter(Boolean).map(line => `# ${line}`);
37
+ }
38
+
39
+ function signatureOf(symbol) {
40
+ const owner = symbol.className ? `${symbol.className}.` : '';
41
+ const callable = CALLABLE_SYMBOL_KINDS.has(symbol.type) || symbol.params != null;
42
+ // A multi-line parameter list folds onto one line and drops the trailing
43
+ // comma the source may carry before its closing paren.
44
+ const params = String(symbol.params || '').replace(/\s*\n\s*/g, ' ').replace(/,\s*$/, '');
45
+ return callable ? `${owner}${symbol.name}(${params})` : `${owner}${symbol.name}`;
46
+ }
47
+
48
+ function pathOf(entry) {
49
+ return entry.relativePath || entry.file || '';
50
+ }
51
+
52
+ function unverifiedTag(entry) {
53
+ const reason = entry.reason || 'unverified';
54
+ const via = entry.dispatchVia ? ` via ${entry.dispatchVia}` : '';
55
+ return `unverified: ${reason}${via}`;
56
+ }
57
+
58
+ function accountComments(account) {
59
+ if (!account) return [];
60
+ return [].concat(formatAccountLines(account) || [])
61
+ .join('\n').split('\n').filter(Boolean).map(line => `# ${line}`);
62
+ }
63
+
64
+ function findRecords(result) {
65
+ const out = [];
66
+ if (Array.isArray(result)) {
67
+ for (const symbol of result) out.push(record(pathOf(symbol), symbol.startLine, signatureOf(symbol), symbol.type));
68
+ } else if (result && Array.isArray(result.types)) {
69
+ for (const type of result.types) {
70
+ out.push(record(pathOf(type), type.startLine ?? type.line, type.name, type.type || type.kind));
71
+ }
72
+ }
73
+ return { records: out, notes: [] };
74
+ }
75
+
76
+ function usagesRecords(result) {
77
+ const out = [];
78
+ const notes = [];
79
+ for (const usage of Array.isArray(result) ? result : []) {
80
+ const kind = usage.isDefinition ? 'definition' : (usage.usageType || 'reference');
81
+ out.push(record(pathOf(usage), usage.line, usage.content, kind === 'call' ? '' : kind));
82
+ }
83
+ const counts = result && result.summaryCounts;
84
+ if (counts && counts.hiddenTestUsages > 0) {
85
+ notes.push(`# ${counts.hiddenTestUsages} test-file usage(s) hidden by default (--include-tests)`);
86
+ }
87
+ return { records: out, notes };
88
+ }
89
+
90
+ function searchRecords(result) {
91
+ const out = [];
92
+ const notes = [];
93
+ // Structural search (--type=...) returns { meta, results: [{file, line,
94
+ // name, kind, receiver, params?}] }; text search returns file groups.
95
+ if (result && !Array.isArray(result) && Array.isArray(result.results)) {
96
+ for (const item of result.results) {
97
+ 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
+ }
100
+ const meta = result.meta;
101
+ if (meta && meta.totalMatched > meta.shown) {
102
+ notes.push(`# ${meta.totalMatched - meta.shown} more match(es) (--limit=N / --all)`);
103
+ }
104
+ return { records: out, notes };
105
+ }
106
+ for (const item of Array.isArray(result) ? result : []) {
107
+ if (Array.isArray(item.matches)) {
108
+ for (const match of item.matches) out.push(record(item.file, match.line, match.content));
109
+ } else if (item.file != null && item.line != null) {
110
+ const text = item.content != null ? item.content : signatureOf(item);
111
+ out.push(record(pathOf(item), item.line, text, item.type || item.kind));
112
+ }
113
+ }
114
+ const meta = result && result.meta;
115
+ if (meta && meta.filesSkipped > 0) {
116
+ notes.push(`# ${meta.filesSkipped} test file(s) hidden by default (--include-tests)`);
117
+ }
118
+ if (meta && meta.truncatedMatches > 0) {
119
+ notes.push(`# ${meta.truncatedMatches} more match(es) omitted by --top/--limit`);
120
+ }
121
+ return { records: out, notes };
122
+ }
123
+
124
+ function callerRecords(context) {
125
+ const out = [];
126
+ for (const caller of context.callers || []) {
127
+ const tag = caller.tier && caller.tier !== 'confirmed' ? unverifiedTag(caller) : '';
128
+ out.push(record(pathOf(caller), caller.line, caller.content, tag));
129
+ }
130
+ for (const caller of context.unverifiedCallers || []) {
131
+ out.push(record(pathOf(caller), caller.line, caller.content, unverifiedTag(caller)));
132
+ }
133
+ return out;
134
+ }
135
+
136
+ function showRecords(result, params = {}) {
137
+ const out = [];
138
+ const notes = [];
139
+ const context = result && result.context;
140
+ // Only an EXPLICIT --sections selects the band: the resolved defaults
141
+ // (summary, callers, callees) would mix callee records into a caller
142
+ // listing and skew `cut -d: -f1 | sort | uniq -c`.
143
+ const explicit = (Array.isArray(params.sections) ? params.sections : String(params.sections || '').split(','))
144
+ .map(s => String(s).trim().toLowerCase()).filter(Boolean);
145
+ const selected = new Set(explicit.length > 0 ? explicit : ['callers']);
146
+ if (context) {
147
+ if (selected.has('callers')) out.push(...callerRecords(context));
148
+ if (selected.has('callees')) {
149
+ for (const callee of context.callees || []) {
150
+ const count = callee.callCount > 1 ? ` x${callee.callCount}` : '';
151
+ out.push(record(pathOf(callee), callee.startLine, signatureOf(callee), `callee${count}`));
152
+ }
153
+ for (const callee of context.unverifiedCallees || []) {
154
+ for (const site of callee.sites || []) {
155
+ out.push(record(pathOf(context), site, callee.name, `${unverifiedTag(callee)}; callee`));
156
+ }
157
+ }
158
+ }
159
+ notes.push(...accountComments(context.meta && context.meta.account));
160
+ if (context.meta && context.meta.calleeAccount && selected.has('callees')) {
161
+ notes.push(`# ${formatCalleeAccountLine(context.meta.calleeAccount)}`);
162
+ }
163
+ }
164
+ if (result && result.target && result.target.alternatives && result.target.alternatives.length > 0) {
165
+ notes.push(`# ${result.target.alternatives.length + 1} definitions; using ${result.target.handle || result.target.file}. Pass a file:line:name handle to pin another.`);
166
+ }
167
+ return { records: out, notes };
168
+ }
169
+
170
+ function impactRecords(result) {
171
+ const out = [];
172
+ const notes = [];
173
+ if (!result) return { records: out, notes };
174
+ if (Array.isArray(result.functions)) {
175
+ for (const fn of result.functions) {
176
+ out.push(...callerRecords(fn));
177
+ notes.push(...accountComments(fn.account));
178
+ }
179
+ for (const fn of result.deletedFunctions || []) {
180
+ for (const site of fn.remainingCallSites || []) {
181
+ out.push(record(pathOf(site), site.line, site.content, 'unverified: deleted-target-name-match'));
182
+ }
183
+ }
184
+ const summary = result.summary || {};
185
+ 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.`);
186
+ if (result.nonSourcePaths) notes.push(`# ${result.nonSourcePaths} changed path(s) outside supported source files not analyzed.`);
187
+ return { records: [...new Set(out)], notes };
188
+ }
189
+ for (const group of result.byFile || []) {
190
+ for (const site of group.sites || []) {
191
+ const tag = site.tier && site.tier !== 'confirmed' ? unverifiedTag(site) : '';
192
+ out.push(record(group.file, site.line, site.expression, tag));
193
+ }
194
+ }
195
+ for (const site of result.unverifiedSites || []) {
196
+ out.push(record(pathOf(site), site.line, site.content || site.expression, unverifiedTag(site)));
197
+ }
198
+ if (result.propertyAccesses) {
199
+ const accesses = result.propertyAccesses;
200
+ for (const group of accesses.byFile || []) {
201
+ for (const access of group.sites || []) {
202
+ out.push(record(group.file, access.line, access.expression, 'property-access'));
203
+ }
204
+ }
205
+ for (const access of accesses.unverifiedSites || []) {
206
+ out.push(record(pathOf(access), access.line, access.expression, `${unverifiedTag(access)}; property-access`));
207
+ }
208
+ notes.push(`# PROPERTY ACCESS SITES: ${accesses.confirmedCount} confirmed, ${accesses.unverifiedCount} unverified, ${accesses.excluded?.total || 0} other-target (separate from caller ACCOUNT).`);
209
+ }
210
+ notes.push(...accountComments(result.account));
211
+ for (const warning of result.warnings || []) notes.push(...commentLines(warning.message));
212
+ if (result.scopeWarning?.hint) notes.push(...commentLines(result.scopeWarning.hint));
213
+ if (result.shownCallSites < result.totalCallSites) {
214
+ notes.push(`# ${result.totalCallSites - result.shownCallSites} more call site(s) omitted by --top/--limit`);
215
+ }
216
+ return { records: out, notes };
217
+ }
218
+
219
+ /**
220
+ * Render a public command result as grep-shaped records followed by `# `
221
+ * comment lines. Returns '' when the command has nothing to list.
222
+ */
223
+ function formatPublicLines(command, result, params = {}, execution = {}) {
224
+ let shaped;
225
+ switch (command) {
226
+ case 'find': shaped = findRecords(result); break;
227
+ case 'usages': shaped = usagesRecords(result); break;
228
+ case 'search': shaped = searchRecords(result); break;
229
+ case 'show': shaped = showRecords(result, params); break;
230
+ case 'impact': shaped = impactRecords(result); break;
231
+ default: return null;
232
+ }
233
+ const lines = [...shaped.records, ...shaped.notes];
234
+ if (execution.note) lines.push(...commentLines(execution.note));
235
+ return lines.map(line => line.startsWith('# ')
236
+ ? commentLines(formatSurfaceMessage(line.slice(2), execution.surface)).join('\n') : line).join('\n');
237
+ }
238
+
239
+ /**
240
+ * Render a `source` result as the code text alone.
241
+ */
242
+ function formatPublicRaw(result, execution = {}) {
243
+ let code = '';
244
+ if (result && Array.isArray(result.lines)) code = result.lines.join('\n');
245
+ else if (result && Array.isArray(result.entries)) {
246
+ code = result.entries.map(entry => entry.code == null ? '' : String(entry.code)).join('\n\n');
247
+ } else if (result && typeof result.code === 'string') code = result.code;
248
+ // A note (the same-name disambiguation, a hidden-section warning) must
249
+ // not vanish in raw mode. Code lines are never reinterpreted, so it
250
+ // cannot ride inside the text: the CLI prints `execution.note` to stderr
251
+ // itself (see emitCliText); the single-block surfaces get it appended as
252
+ // one trailing `# ` line after the code.
253
+ if (execution.note && execution.surface !== 'cli') {
254
+ return `${code.replace(/\n$/, '')}\n${commentLines(execution.note).join('\n')}`;
255
+ }
256
+ return code;
257
+ }
258
+
259
+ module.exports = { LINES_COMMANDS, formatPublicLines, formatPublicRaw, commentLines };
@@ -11,6 +11,7 @@
11
11
  const { COMMAND_CONTRACTS } = require('../command-contracts');
12
12
  const { COMMAND_TRUST_MATRIX } = require('../trust-matrix');
13
13
  const { toCliName, toMcpName, formatSurfaceMessage } = require('../registry');
14
+ const { LINES_COMMANDS, formatPublicLines, formatPublicRaw } = require('./lines');
14
15
 
15
16
  const legacy = {
16
17
  ...require('./analysis'),
@@ -274,6 +275,20 @@ function formatRepo(result, params = {}, hints = presentationHints()) {
274
275
  }
275
276
 
276
277
  function formatPublicText(command, result, params = {}, execution = {}) {
278
+ // Grep-shaped and raw modes (fix #341): the agent-in-a-shell surface.
279
+ if (params.raw && command === 'source') {
280
+ return formatPublicRaw(result, {
281
+ ...execution,
282
+ note: execution.note ? formatSurfaceMessage(execution.note, execution.surface) : undefined,
283
+ });
284
+ }
285
+ if (params.lines && LINES_COMMANDS.has(command)) {
286
+ const shaped = formatPublicLines(command, result, params, {
287
+ ...execution,
288
+ note: execution.note ? formatSurfaceMessage(execution.note, execution.surface) : undefined,
289
+ });
290
+ if (shaped != null) return shaped;
291
+ }
277
292
  const hints = presentationHints(execution.surface);
278
293
  if (result?.scopeWarning?.hint) {
279
294
  result.scopeWarning = {
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * core/output/reporting.js - Stats/TOC/deadcode/entrypoints formatters
3
3
  */
4
+ const path = require('path');
4
5
 
5
6
  const {
6
7
  lineRange,
@@ -457,7 +458,10 @@ function formatEntrypointsJson(results) {
457
458
  */
458
459
  function formatOrient(result, options = {}) {
459
460
  const lines = [];
460
- lines.push(`PROJECT ORIENTATION — ${result.root}${result.scope ? ` (scoped to ${result.scope})` : ''}`);
461
+ // The title names the project (fix #343); the absolute root is the
462
+ // caller's own argument and `--sections=stats` prints it in full.
463
+ const projectName = path.basename(String(result.root || '')) || String(result.root || '');
464
+ lines.push(`PROJECT ORIENTATION — ${projectName}${result.scope ? ` (scoped to ${result.scope})` : ''}`);
461
465
  lines.push('═'.repeat(60));
462
466
 
463
467
  // Size + language mix (percent by symbols, largest first)
@@ -486,7 +490,10 @@ function formatOrient(result, options = {}) {
486
490
  const population = result.hot.totalKind === 'raw-call-candidates'
487
491
  ? `${result.hot.total} raw candidates`
488
492
  : result.hot.total;
489
- lines.push(`HOT (most-called ${scope}, top ${result.hot.items.length} of ${population}):`);
493
+ const budgetNote = result.hot.budgetExhausted
494
+ ? `; refinement budget ${result.hot.maxRefine} reached — ranking approximate, exact list: ucn repo --sections=stats --hot`
495
+ : '';
496
+ lines.push(`HOT (most-called ${scope}, top ${result.hot.items.length} of ${population}${budgetNote}):`);
490
497
  for (const h of result.hot.items) {
491
498
  const label = h.className ? `${h.className}.${h.name}` : h.name;
492
499
  lines.push(` ${label} — ${h.callCount} call(s) · ${h.file}:${h.line}`);
@@ -33,18 +33,21 @@ function preservedContractMetadata(fullText, visibleText, options = {}) {
33
33
  // one physical line. Preserve the actionable test-scope contract as
34
34
  // its own sentence so a later parse-failure note cannot make the
35
35
  // whole metadata item too large for a small transport budget.
36
- const firstSentenceEnd = /^\s*\d+ test-file usage\(s\) hidden\b/.test(rawLine)
36
+ const firstSentenceEnd = /^\s*(?:# )?\d+ test-file usage\(s\) hidden\b/.test(rawLine)
37
37
  ? rawLine.indexOf('. ')
38
38
  : -1;
39
39
  const contractLine = firstSentenceEnd >= 0
40
40
  ? rawLine.slice(0, firstSentenceEnd + 1)
41
41
  : rawLine;
42
- if (!CONTRACT_LINE_RE.test(contractLine)) continue;
42
+ // Shell-shaped MCP/interactive results prefix disclosures with '# '.
43
+ // Match their contents but retain the prefix in the preserved text.
44
+ const evidenceLine = contractLine.replace(/^\s*# /, '').trim();
45
+ if (!CONTRACT_LINE_RE.test(evidenceLine)) continue;
43
46
  const line = contractLine.trim();
44
47
  if (!line || visible.has(line)) continue;
45
- const priority = /^(?:ACCOUNT|CONTRACT|WARNING|FILTERED|CALLEE ACCOUNT|TREE ACCOUNT):/.test(line)
48
+ const priority = /^(?:ACCOUNT|CONTRACT|WARNING|FILTERED|CALLEE ACCOUNT|TREE ACCOUNT):/.test(evidenceLine)
46
49
  ? 0
47
- : /^\d+ test-file usage\(s\) hidden\b/.test(line) ? 1 : 2;
50
+ : /^\d+ test-file usage\(s\) hidden\b/.test(evidenceLine) ? 1 : 2;
48
51
  candidates.push({ line, priority, sourceIndex });
49
52
  }
50
53
 
package/core/project.js CHANGED
@@ -149,6 +149,7 @@ class ProjectIndex {
149
149
  this._opCppPathReceiverTypeCache = new Map();
150
150
  this._opDerefPairs = undefined;
151
151
  this._opAliasPairs = undefined;
152
+ this._opFindCallersCaches = null; // fix #340: findCallers per-file derivations
152
153
  this._opDepth = 0;
153
154
  }
154
155
  this._opDepth++;
@@ -178,6 +179,7 @@ class ProjectIndex {
178
179
  this._opCppPathReceiverTypeCache = null;
179
180
  this._opDerefPairs = null;
180
181
  this._opAliasPairs = null;
182
+ this._opFindCallersCaches = null;
181
183
  // Free cached file content from callsCache entries (retained during
182
184
  // operation for _readFile caching, not needed between operations)
183
185
  for (const entry of this.callsCache.values()) {
@@ -1249,7 +1251,7 @@ class ProjectIndex {
1249
1251
  const typeOrder = new Set([
1250
1252
  'class', 'struct', 'interface', 'type', 'impl', 'enum', 'record',
1251
1253
  ]);
1252
- const { isTestPath } = require('./shared');
1254
+ const { isTestPath, CALLABLE_SYMBOL_KINDS } = require('./shared');
1253
1255
  const scored = definitions.map(d => {
1254
1256
  let score = 0;
1255
1257
  const rp = d.relativePath || '';
@@ -1280,10 +1282,21 @@ class ProjectIndex {
1280
1282
  // Deprioritize type-only overload signatures (TypeScript function_signature)
1281
1283
  if (d.isSignature) score -= 200;
1282
1284
  // Prefer larger function bodies (implementation over overload signature)
1283
- // Only for functions/methods — not for class-level types (struct vs impl)
1285
+ // Only for functions — not for class-level types (struct vs impl).
1286
+ // Same-named METHODS keep tying here on purpose: widening this to
1287
+ // every callable kind reshuffled method-vs-method picks project-wide
1288
+ // (httpx `send`), which is not the fix #343 defect.
1284
1289
  if (d.startLine && d.endLine && d.type === 'function') {
1285
1290
  score += Math.min(d.endLine - d.startLine, 100);
1286
1291
  }
1292
+ // Fix #343: a bare name denotes the CALLABLE when a field shares it.
1293
+ // A Rust/Go/Java method used to tie with a same-named field (the
1294
+ // builder idiom: `heap_limit` field + `heap_limit(&mut self)` setter)
1295
+ // and lose on file order, so `impact heap_limit` answered 0 call
1296
+ // sites against the field while the setter had 10 confirmed callers.
1297
+ // Callable kinds tie among themselves as before; only the
1298
+ // callable-vs-field tie is decided (a zero-line Java getter too).
1299
+ if (CALLABLE_SYMBOL_KINDS.has(d.type)) score += 25;
1287
1300
  // Prefer shallower paths (fewer directory levels = more central to project)
1288
1301
  // Max bonus 50 for root-level files, decreasing with depth
1289
1302
  const depth = (rp.match(/\//g) || []).length;
package/core/registry.js CHANGED
@@ -133,13 +133,13 @@ const FLAG_APPLICABILITY = {
133
133
  // Understand one symbol. `sections` is a comma-separated projection:
134
134
  // summary, callers, callees, source, dependencies, tests, types, example,
135
135
  // related. Caller-bearing projections always preserve ACCOUNT/CONTRACT.
136
- show: ['name', 'file', 'exclude', 'className', 'line', 'sections', 'includeMethods', 'includeTests', 'top', 'all', 'withTypes', 'minConfidence', 'showConfidence', 'unreachableOnly', 'compact', 'git', 'diverse'],
137
- find: ['name', 'file', 'exclude', 'className', 'includeTests', 'limit', 'exact', 'in', 'compact', 'type', 'withSource'],
138
- usages: ['name', 'file', 'exclude', 'className', 'includeTests', 'limit', 'codeOnly', 'context', 'in', 'compact', 'all'],
139
- search: ['term', 'file', 'exclude', 'includeTests', 'top', 'limit', 'codeOnly', 'caseSensitive', 'context', 'regex', 'in', 'type', 'param', 'receiver', 'returns', 'decorator', 'exported', 'unused'],
140
- source: ['name', 'file', 'className', 'line', 'range', 'all', 'maxLines'],
136
+ show: ['name', 'file', 'exclude', 'className', 'line', 'sections', 'includeMethods', 'includeTests', 'top', 'all', 'withTypes', 'minConfidence', 'showConfidence', 'unreachableOnly', 'compact', 'git', 'diverse', 'lines'],
137
+ find: ['name', 'file', 'exclude', 'className', 'includeTests', 'limit', 'exact', 'in', 'compact', 'type', 'withSource', 'lines'],
138
+ usages: ['name', 'file', 'exclude', 'className', 'includeTests', 'limit', 'codeOnly', 'context', 'in', 'compact', 'all', 'lines'],
139
+ search: ['term', 'file', 'exclude', 'includeTests', 'top', 'limit', 'codeOnly', 'caseSensitive', 'context', 'regex', 'in', 'type', 'param', 'receiver', 'returns', 'decorator', 'exported', 'unused', 'lines'],
140
+ source: ['name', 'file', 'className', 'line', 'range', 'all', 'maxLines', 'raw'],
141
141
  trace: ['name', 'file', 'exclude', 'className', 'line', 'direction', 'to', 'includeMethods', 'depth', 'all', 'expandUnverified'],
142
- impact: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'top', 'unreachableOnly', 'compact', 'base', 'staged', 'limit', 'all'],
142
+ impact: ['name', 'file', 'exclude', 'className', 'line', 'includeMethods', 'top', 'unreachableOnly', 'compact', 'base', 'staged', 'limit', 'all', 'lines'],
143
143
  tests: ['name', 'file', 'exclude', 'className', 'line', 'callsOnly', 'depth', 'includeMethods', 'all'],
144
144
  deps: ['file', 'exclude', 'depth', 'direction', 'all', 'detailed', 'cycles'],
145
145
  api: ['file', 'in', 'limit'],
@@ -308,6 +308,7 @@ function formatSurfaceMessage(message, surface = 'cli') {
308
308
  'expandUnverified', 'withSource', 'all', 'compact', 'exact',
309
309
  'regex', 'exported', 'unused', 'staged', 'detailed', 'functions',
310
310
  'hot', 'deep', 'cycles', 'bridge', 'unmatched', 'diverse', 'git',
311
+ 'raw', 'lines',
311
312
  ]);
312
313
  if (surface === 'mcp') {
313
314
  rendered = rendered.replace(/--([a-z][a-z0-9-]*)(?:=([^\s,.)]+))?/g,