ucn 4.0.1 → 4.1.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.
Files changed (47) hide show
  1. package/.claude/skills/ucn/SKILL.md +31 -7
  2. package/README.md +89 -50
  3. package/cli/index.js +199 -94
  4. package/core/account.js +3 -1
  5. package/core/analysis.js +334 -316
  6. package/core/bridge.js +13 -8
  7. package/core/cache.js +109 -19
  8. package/core/callers.js +969 -77
  9. package/core/check.js +19 -8
  10. package/core/deadcode.js +368 -40
  11. package/core/discovery.js +31 -11
  12. package/core/entrypoints.js +149 -17
  13. package/core/execute.js +330 -43
  14. package/core/graph-build.js +61 -10
  15. package/core/graph.js +282 -61
  16. package/core/imports.js +72 -3
  17. package/core/output/analysis-ext.js +70 -10
  18. package/core/output/analysis.js +67 -33
  19. package/core/output/check.js +4 -1
  20. package/core/output/doctor.js +13 -3
  21. package/core/output/endpoints.js +8 -3
  22. package/core/output/extraction.js +12 -1
  23. package/core/output/find.js +23 -9
  24. package/core/output/graph.js +32 -9
  25. package/core/output/refactoring.js +147 -49
  26. package/core/output/reporting.js +104 -5
  27. package/core/output/search.js +30 -3
  28. package/core/output/shared.js +31 -4
  29. package/core/output/tracing.js +22 -11
  30. package/core/parser.js +8 -6
  31. package/core/project.js +167 -7
  32. package/core/registry.js +20 -16
  33. package/core/reporting.js +220 -84
  34. package/core/search.js +270 -55
  35. package/core/shared.js +285 -1
  36. package/core/stacktrace.js +23 -1
  37. package/core/tracing.js +278 -36
  38. package/core/verify.js +352 -349
  39. package/languages/go.js +29 -17
  40. package/languages/index.js +56 -0
  41. package/languages/java.js +133 -16
  42. package/languages/javascript.js +215 -27
  43. package/languages/python.js +113 -47
  44. package/languages/rust.js +89 -26
  45. package/languages/utils.js +35 -7
  46. package/mcp/server.js +72 -31
  47. package/package.json +4 -1
package/languages/rust.js CHANGED
@@ -12,6 +12,7 @@ const {
12
12
  parseStructuredParams,
13
13
  extractRustDocstring,
14
14
  visitNameNodes,
15
+ sameNode,
15
16
  } = require('./utils');
16
17
  const { PARSE_OPTIONS, safeParse } = require('./index');
17
18
 
@@ -38,11 +39,13 @@ function extractReturnType(node) {
38
39
  * Extract Rust parameters
39
40
  */
40
41
  function extractRustParams(paramsNode) {
42
+ // Distinguish "we have no node" (genuinely unknown) from "node is empty".
43
+ // Returning '...' for empty parens conflated zero-param functions with
44
+ // unknown signatures in JSON output (fix #238; the shared
45
+ // utils.extractParams already had this fix).
41
46
  if (!paramsNode) return '...';
42
47
  const text = paramsNode.text;
43
- let params = text.replace(/^\(|\)$/g, '').trim();
44
- if (!params) return '...';
45
- return params;
48
+ return text.replace(/^\(|\)$/g, '').trim();
46
49
  }
47
50
 
48
51
  /**
@@ -659,11 +662,17 @@ function extractStructFields(structNode, codeOrLines) {
659
662
  const typeNode = field.childForFieldName('type');
660
663
 
661
664
  if (nameNode) {
665
+ // Record the field's own visibility (pub / pub(crate) / ...) so
666
+ // export listings can judge members per-symbol (fix #241 —
667
+ // pub fields were invisible to fileExports, and private fields
668
+ // used to leak in via name collision with file-level exports).
669
+ const visibility = extractVisibility(field.text);
662
670
  fields.push({
663
671
  name: nameNode.text,
664
672
  startLine,
665
673
  endLine,
666
674
  memberType: 'field',
675
+ ...(visibility && { modifiers: [visibility] }),
667
676
  ...(typeNode && { fieldType: typeNode.text })
668
677
  });
669
678
  }
@@ -769,13 +778,17 @@ function extractTraitMembers(traitNode, codeOrLines) {
769
778
  const returnType = extractReturnType(child);
770
779
  const hasSelf = paramsNode && paramsNode.text.includes('self');
771
780
 
781
+ // Rust vocabulary (fix #248): trait members carry the trait's
782
+ // OWN visibility — a method of a private trait is not `pub`,
783
+ // and 'public' is not a Rust modifier ('pub'/'pub(crate)'/...).
784
+ const traitVisibility = extractVisibility(traitNode.text);
772
785
  members.push({
773
786
  name: nameNode.text,
774
787
  startLine,
775
788
  endLine,
776
789
  memberType: 'method',
777
790
  isMethod: true,
778
- modifiers: ['public'], // Trait methods are implicitly public
791
+ modifiers: traitVisibility ? [traitVisibility] : [],
779
792
  ...(paramsNode && { params: extractRustParams(paramsNode) }),
780
793
  ...(paramsNode && { paramsStructured: parseStructuredParams(paramsNode, 'rust') }),
781
794
  ...(returnType && { returnType }),
@@ -822,9 +835,17 @@ function extractImplMembers(implNode, codeOrLines, typeName) {
822
835
  const inCfgTest = _isInsideCfgTestModule(child, Array.isArray(codeOrLines) ? codeOrLines : codeOrLines.split('\n'));
823
836
  const modifiers = [];
824
837
  if (visibility) modifiers.push(visibility);
838
+ // Function qualifiers, same vocabulary as free functions
839
+ // (fix #248: `pub async fn get` rendered as `pub get(...)`;
840
+ // const/unsafe methods had no machine-readable qualifier).
841
+ if (firstLine.includes('async ')) modifiers.push('async');
842
+ if (firstLine.includes('unsafe ')) modifiers.push('unsafe');
843
+ if (firstLine.includes('const fn')) modifiers.push('const');
844
+ if (firstLine.includes('extern ')) modifiers.push('extern');
825
845
  for (const attr of attributes) modifiers.push(attr);
826
846
  if (inCfgTest) modifiers.push('cfg_test_module');
827
847
 
848
+ const memberGenerics = extractGenerics(child);
828
849
  members.push({
829
850
  name: nameNode.text,
830
851
  params: extractRustParams(paramsNode),
@@ -837,7 +858,10 @@ function extractImplMembers(implNode, codeOrLines, typeName) {
837
858
  modifiers,
838
859
  ...(typeName && { receiver: typeName }), // All impl members get receiver for findMethodsForType
839
860
  ...(returnType && { returnType }),
840
- ...(docstring && { docstring })
861
+ ...(docstring && { docstring }),
862
+ // Method-level type params (fix #229): generic-param receiver
863
+ // types inside the method resolve against this declaration.
864
+ ...(memberGenerics && { generics: memberGenerics })
841
865
  });
842
866
  }
843
867
  }
@@ -1916,17 +1940,18 @@ function findExportsInCode(code, parser) {
1916
1940
  * @param {string} code - Source code
1917
1941
  * @param {string} name - Symbol name to find
1918
1942
  * @param {object} parser - Tree-sitter parser instance
1943
+ * @param {object} [tree] - Pre-parsed tree (per-operation cache); parsed here when absent
1919
1944
  * @returns {Array<{line: number, column: number, usageType: string}>}
1920
1945
  */
1921
1946
  function _indexInParent(node, parent) {
1922
1947
  for (let i = 0; i < parent.childCount; i++) {
1923
- if (parent.child(i) === node) return i;
1948
+ if (sameNode(parent.child(i), node)) return i;
1924
1949
  }
1925
1950
  return -1;
1926
1951
  }
1927
1952
 
1928
- function findUsagesInCode(code, name, parser) {
1929
- const tree = parseTree(parser, code);
1953
+ function findUsagesInCode(code, name, parser, tree) {
1954
+ tree = tree || parseTree(parser, code);
1930
1955
  const usages = [];
1931
1956
 
1932
1957
  visitNameNodes(tree, code, name, (node) => {
@@ -1961,40 +1986,69 @@ function findUsagesInCode(code, name, parser) {
1961
1986
  }
1962
1987
  // Call: name()
1963
1988
  else if (parent.type === 'call_expression' &&
1964
- parent.childForFieldName('function') === node) {
1989
+ sameNode(parent.childForFieldName('function'), node)) {
1965
1990
  usageType = 'call';
1966
1991
  }
1967
1992
  // Scoped call: Type::method() — only the LAST segment is the callee;
1968
1993
  // the path qualifier (Type in Type::method()) is a type reference,
1969
- // not a call of Type
1994
+ // not a call of Type. The qualifier IS the receiver — without it,
1995
+ // --class-name scoping could never match associated-function calls
1996
+ // (fix #244: `Kit::make()` invisible to `tests make --class-name Kit`).
1970
1997
  else if (parent.type === 'scoped_identifier') {
1971
1998
  const grandparent = parent.parent;
1972
- if (grandparent && grandparent.type === 'call_expression' &&
1973
- grandparent.childForFieldName('function') === parent &&
1974
- parent.childForFieldName('name') === node) {
1999
+ const isDirectCall = grandparent && grandparent.type === 'call_expression' &&
2000
+ sameNode(grandparent.childForFieldName('function'), parent);
2001
+ // Turbofish on a scoped path: Type::<T>::m() wraps the path in
2002
+ // generic_function before the call_expression.
2003
+ const isTurbofishCall = grandparent && grandparent.type === 'generic_function' &&
2004
+ grandparent.parent && grandparent.parent.type === 'call_expression' &&
2005
+ sameNode(grandparent.parent.childForFieldName('function'), grandparent);
2006
+ if ((isDirectCall || isTurbofishCall) &&
2007
+ sameNode(parent.childForFieldName('name'), node)) {
2008
+ usageType = 'call';
2009
+ const pathNode = parent.childForFieldName('path');
2010
+ if (pathNode) {
2011
+ const segs = pathNode.text.split('::');
2012
+ const receiver = segs[segs.length - 1];
2013
+ if (receiver) {
2014
+ usages.push({ line, column, usageType, receiver });
2015
+ return true;
2016
+ }
2017
+ }
2018
+ }
2019
+ }
2020
+ // Turbofish call on a bare name: f::<T>() — the identifier's parent
2021
+ // is generic_function, the call wraps that (fix #244: classified
2022
+ // 'reference', so the account confirmed the edge while the
2023
+ // coverage scan reported the function uncovered).
2024
+ else if (parent.type === 'generic_function' &&
2025
+ sameNode(parent.childForFieldName('function'), node)) {
2026
+ const gp = parent.parent;
2027
+ if (gp && gp.type === 'call_expression' &&
2028
+ sameNode(gp.childForFieldName('function'), parent)) {
1975
2029
  usageType = 'call';
1976
2030
  }
1977
2031
  }
1978
2032
  // Macro invocation: name!
1979
2033
  else if (parent.type === 'macro_invocation') {
1980
2034
  const macroNode = parent.childForFieldName('macro');
1981
- if (macroNode === node) {
2035
+ if (sameNode(macroNode, node)) {
1982
2036
  usageType = 'call';
1983
2037
  }
1984
2038
  }
1985
2039
  // Definition: fn name
1986
2040
  else if (parent.type === 'function_item' &&
1987
- parent.childForFieldName('name') === node) {
2041
+ sameNode(parent.childForFieldName('name'), node)) {
1988
2042
  usageType = 'definition';
1989
2043
  }
1990
2044
  // Definition: struct name
1991
2045
  else if (parent.type === 'struct_item' &&
1992
- parent.childForFieldName('name') === node) {
2046
+ sameNode(parent.childForFieldName('name'), node)) {
1993
2047
  usageType = 'definition';
1994
2048
  }
1995
2049
  // Definition: enum name
1996
2050
  else if (parent.type === 'enum_item' &&
1997
- parent.childForFieldName('name') === node) {
2051
+ sameNode(parent.childForFieldName('name'), node)) {
1998
2052
  usageType = 'definition';
1999
2053
  }
2000
2054
  // Definition: impl for Type
@@ -2003,7 +2057,7 @@ function findUsagesInCode(code, name, parser) {
2003
2057
  }
2004
2058
  // Definition: type alias
2005
2059
  else if (parent.type === 'type_item' &&
2006
- parent.childForFieldName('name') === node) {
2060
+ sameNode(parent.childForFieldName('name'), node)) {
2007
2061
  usageType = 'definition';
2008
2062
  }
2009
2063
  // Definition: let binding
@@ -2013,22 +2067,22 @@ function findUsagesInCode(code, name, parser) {
2013
2067
  }
2014
2068
  // Definition: const/static
2015
2069
  else if ((parent.type === 'const_item' || parent.type === 'static_item') &&
2016
- parent.childForFieldName('name') === node) {
2070
+ sameNode(parent.childForFieldName('name'), node)) {
2017
2071
  usageType = 'definition';
2018
2072
  }
2019
2073
  // Definition: parameter name (not the type)
2020
2074
  else if (parent.type === 'parameter' &&
2021
- parent.childForFieldName('pattern') === node) {
2075
+ sameNode(parent.childForFieldName('pattern'), node)) {
2022
2076
  usageType = 'definition';
2023
2077
  }
2024
2078
  // Struct expression: Type { field: value }
2025
2079
  else if (parent.type === 'struct_expression' &&
2026
- parent.childForFieldName('name') === node) {
2080
+ sameNode(parent.childForFieldName('name'), node)) {
2027
2081
  usageType = 'call';
2028
2082
  }
2029
2083
  // Method call: obj.name()
2030
2084
  else if (parent.type === 'field_expression' &&
2031
- parent.childForFieldName('field') === node) {
2085
+ sameNode(parent.childForFieldName('field'), node)) {
2032
2086
  const grandparent = parent.parent;
2033
2087
  if (grandparent && grandparent.type === 'call_expression') {
2034
2088
  usageType = 'call';
@@ -2079,11 +2133,17 @@ function findUsagesInCode(code, name, parser) {
2079
2133
 
2080
2134
  // Filter out enum variant references: Boundary::Grid is NOT a usage of Grid struct
2081
2135
  // If our node is the NAME (right side) of a scoped_identifier/scoped_type_identifier,
2082
- // and the PATH (left side) is a different Capitalized type, it's likely an enum variant
2083
- if (parent && (parent.type === 'scoped_identifier' || parent.type === 'scoped_type_identifier')) {
2136
+ // and the PATH (left side) is a different Capitalized type, it's likely an enum variant.
2137
+ // Never a CALL site (fix #234, campaign G2-rust BUG-1): the scoped-call
2138
+ // branch classified `DataService::with_defaults()` as a call, and this
2139
+ // filter then swallowed it — usages reported '0 calls' for every
2140
+ // path-qualified Type::method() invocation, the exact answer that
2141
+ // invites deleting a live function.
2142
+ if (usageType !== 'call' &&
2143
+ parent && (parent.type === 'scoped_identifier' || parent.type === 'scoped_type_identifier')) {
2084
2144
  const nameField = parent.childForFieldName('name');
2085
2145
  const pathField = parent.childForFieldName('path');
2086
- if (nameField === node && pathField) {
2146
+ if (sameNode(nameField, node) && pathField) {
2087
2147
  const pathText = pathField.text;
2088
2148
  // If path is a Capitalized identifier different from our target, it's Type::Variant
2089
2149
  // Skip module paths (lowercase), self/Self/super/crate keywords
@@ -2120,7 +2180,10 @@ function getEntryPointKind(symbol) {
2120
2180
  // Functions inside #[cfg(test)] mod blocks — test-only code, even if they
2121
2181
  // lack a direct #[test] attribute (e.g. shared helpers in `mod tests`).
2122
2182
  if (m.includes('cfg_test_module')) return 'test';
2123
- if (symbol.name === 'main') return 'main';
2183
+ // Only the FREE function fn main() is the binary entry — an impl method
2184
+ // named `main` is an ordinary method (fix #243; it was never audited by
2185
+ // deadcode and entrypoints listed it as runtime).
2186
+ if (symbol.name === 'main' && !symbol.className && !symbol.receiver) return 'main';
2124
2187
  // Trait-impl methods are framework entry points (invoked by trait holder).
2125
2188
  if (symbol.isMethod && symbol.className && symbol.traitImpl) return 'framework';
2126
2189
  return null;
@@ -11,8 +11,11 @@
11
11
  */
12
12
  function traverseTree(node, callback, options) {
13
13
  if (callback(node) === false) return;
14
- for (let i = 0; i < node.namedChildCount; i++) {
15
- traverseTree(node.namedChild(i), callback, options);
14
+ // Single batched native call per node namedChildCount + N × namedChild(i)
15
+ // costs N+1 native round-trips for the same children.
16
+ const children = node.namedChildren;
17
+ for (let i = 0; i < children.length; i++) {
18
+ traverseTree(children[i], callback, options);
16
19
  }
17
20
  if (options?.onLeave) {
18
21
  options.onLeave(node);
@@ -123,12 +126,20 @@ function parseJSParam(param, info) {
123
126
  info.default = valueNode.text;
124
127
  info.optional = true;
125
128
  } else if (!info.rest) {
126
- // Also check for bare number/string/etc. children as defaults
129
+ // Also check for bare number/string/etc. children as defaults.
130
+ // TS parameter-property modifiers (private/protected/public/
131
+ // readonly/override) and parameter decorators (@Inject()) are
132
+ // NOT defaults (fix #230 — `constructor(protected config:
133
+ // Config)` used to report default 'protected', optional true,
134
+ // wrecking expectedArgs.min and the signature display).
135
+ const NON_DEFAULT_PARAM_CHILDREN = new Set([
136
+ 'identifier', 'type_annotation', 'rest_pattern',
137
+ 'accessibility_modifier', 'override_modifier', 'readonly', 'decorator',
138
+ ]);
127
139
  for (let i = 0; i < param.namedChildCount; i++) {
128
140
  const child = param.namedChild(i);
129
141
  if (child !== patternNode && child !== (typeNode && typeNode.parent === param ? typeNode : null) &&
130
- child.type !== 'type_annotation' && child.type !== 'rest_pattern' &&
131
- !['identifier', 'type_annotation'].includes(child.type)) {
142
+ !NON_DEFAULT_PARAM_CHILDREN.has(child.type)) {
132
143
  // This is likely a default value node
133
144
  info.default = child.text;
134
145
  info.optional = true;
@@ -683,8 +694,10 @@ function _buildNodeList(rootNode) {
683
694
  const idx = nodes.length;
684
695
  nodes.push(node);
685
696
  subtreeEnds.push(0);
686
- for (let i = 0; i < node.namedChildCount; i++) {
687
- collect(node.namedChild(i));
697
+ // Batched children read one native call instead of N+1 (see traverseTree)
698
+ const children = node.namedChildren;
699
+ for (let i = 0; i < children.length; i++) {
700
+ collect(children[i]);
688
701
  }
689
702
  subtreeEnds[idx] = nodes.length;
690
703
  }
@@ -950,7 +963,22 @@ function extractSprintfPrefix(callNode) {
950
963
  return { value: hasFmt ? (literal + '*') : literal, interp: hasFmt };
951
964
  }
952
965
 
966
+
967
+ /**
968
+ * Stable node equality (fix #233): tree-sitter node WRAPPERS are not
969
+ * reference-stable — `parent.child(i) === node` can return false for the
970
+ * same underlying node once a tree has been walked by an earlier operation
971
+ * (the CI macro-flake root cause: assert_eq!-wrapped calls flipped
972
+ * 'call'→'reference' on the second index build in a process because
973
+ * _indexInParent returned -1). `.id` is node-tree-sitter's stable native
974
+ * node identity — compare that, never the wrapper reference.
975
+ */
976
+ function sameNode(a, b) {
977
+ return !!a && !!b && (a === b || a.id === b.id);
978
+ }
979
+
953
980
  module.exports = {
981
+ sameNode,
954
982
  traverseTree,
955
983
  traverseTreeCached,
956
984
  visitNameNodes,
package/mcp/server.js CHANGED
@@ -189,6 +189,7 @@ const TOOL_DESCRIPTION = `Code intelligence toolkit for AI agents. Extract speci
189
189
  TOP 5 (covers 90% of tasks): about, impact, trace, find, deadcode
190
190
 
191
191
  QUICK GUIDE — choosing the right command:
192
+ New/unfamiliar repo → orient (size, top dirs, hot functions, entry points, trust — run FIRST)
192
193
  Understand a symbol → about (everything), context (callers/callees only), smart (code + called functions inline)
193
194
  Before modifying → impact (all call sites with args), verify (signature check), plan (preview refactor)
194
195
  Execution flow → trace (function call tree) or graph (file imports/exports)
@@ -241,13 +242,22 @@ REFACTORING:
241
242
 
242
243
  DIAGNOSTICS:
243
244
  - doctor: Index health/coverage report — file/symbol counts, language breakdown, dynamic-import / eval / reflection blind spots, parse failures, and a verdict (HIGH/MEDIUM/LOW trust). Use deep=true to also sample resolution coverage and bucket edges by confidence. Use in= to scope to a subtree.
245
+ - orient: One-screen repo orientation for a codebase you just entered: size + language mix, densest directories, most-called functions, entry-point counts, and the trust verdict. Best FIRST command in a new repo.
244
246
 
245
247
  OTHER:
246
248
  - typedef <name>: Find type definitions matching a name: interfaces, enums, structs, traits, type aliases. See field shapes, required methods, or enum values.
247
249
  - stacktrace: Parse a stack trace, show source context per frame. Requires stack param. Handles JS, Python, Go, Rust, Java formats.
248
250
  - api: Public API surface of project or file: all exported/public symbols with signatures. Use to understand what a library exposes. Pass file to scope to one file. Python needs __all__; use toc instead.
249
251
  - stats: Quick project stats: file counts, symbol counts, lines of code by language and symbol type. Use functions=true for per-function line counts sorted by size (complexity audit). Set hot=true with top=N for the most-called functions (project orientation primitive).
250
- - audit_async: Find async calls inside async functions that are likely missing await (probable bugs). JS/TS/Python only. Filter with file/exclude/limit.` + generateMcpParamSection();
252
+ - audit_async: Find async calls inside async functions that are likely missing await (probable bugs). JS/TS/Python only. Filter with file/exclude/limit.
253
+
254
+ READING OUTPUT (trust contract):
255
+ - Caller/impact answers PARTITION every text occurrence of the symbol — nothing is silently hidden. CONFIRMED entries carry binding/receiver/import evidence (safe to act on). UNVERIFIED entries are call-syntax matches without evidence, each with a reason — treat as possible callers before a breaking change. The ACCOUNT line reconciles the arithmetic; "0 unaccounted" means the partition is complete.
256
+ - A CONFIRMED(0) + UNVERIFIED(0) answer with 0 unaccounted and no WARNING is a trustworthy zero: the symbol genuinely has no callers.
257
+ - WARNING lines list unparsed files containing the symbol — their lines were NOT analyzed; fall back to text search there.
258
+ - verify arg-checks and plan plans CONFIRMED sites only; their UNVERIFIED CALL SITES sections list candidates to review manually. check reports "N callers (+M unverified)" per changed function.
259
+ - context/smart/trace also account the callee side (CALLEE ACCOUNT line + unverified callees with reasons).
260
+ - Advisory commands (related, example, stacktrace, endpoints bridge=true) mark output "Advisory:" — ranked heuristics, not verified claims. Every other listed answer is contract-backed.` + generateMcpParamSection();
251
261
 
252
262
  server.registerTool(
253
263
  'ucn',
@@ -261,8 +271,8 @@ server.registerTool(
261
271
  exclude: z.string().optional().describe('Comma-separated patterns to exclude (e.g. "test,mock,vendor")'),
262
272
  include_tests: z.boolean().optional().describe('Include test files in results (excluded by default)'),
263
273
  exclude_tests: z.boolean().optional().describe('Exclude test files from results. Used by entrypoints (where tests are included by default).'),
264
- include_methods: z.boolean().optional().describe('Include obj.method() calls in trace/blast/smart/verify (implied for about/context/impact — method calls tiered by receiver evidence)'),
265
- include_uncertain: z.boolean().optional().describe('Include uncertain matches in smart/verify (implied for about/context/impact/trace/blast/reverse_trace/affected_tests — unverified candidates always shown, tiered)'),
274
+ include_methods: z.boolean().optional().describe('Include obj.method() callee expansion in trace/blast. No effect on about/context/impact/verify — method calls are always analyzed and tiered by receiver evidence'),
275
+ include_uncertain: z.boolean().optional().describe('No effect on tiered commands (about/context/impact/trace/blast/reverse_trace/affected_tests/verify/smart) — unverified candidates are always shown with reasons'),
266
276
  expand_unverified: z.boolean().optional().describe('blast/reverse_trace: follow unverified caller edges in the tree — downstream nodes are marked as unverified chains (possible, not confirmed, impact)'),
267
277
  min_confidence: z.number().min(0).max(1).optional().describe('Minimum confidence threshold (0.0-1.0) to filter caller/callee edges'),
268
278
  show_confidence: z.boolean().optional().describe('Show confidence scores per edge (default: true). Set false to hide.'),
@@ -302,6 +312,7 @@ server.registerTool(
302
312
  all: z.boolean().optional().describe('Show all results (expand truncated sections). Applies to about, toc, related, trace, and others.'),
303
313
  top_level: z.boolean().optional().describe('Show only top-level functions in toc (exclude nested/indented)'),
304
314
  class_name: z.string().optional().describe('Class name to scope method analysis (e.g. "MarketDataFetcher" for close)'),
315
+ line: z.number().int().positive().optional().describe('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.'),
305
316
  limit: z.number().int().positive().max(1000000).optional().describe('Max results to return (default: 500). Caps find, usages, search, deadcode, api, toc --detailed. Must be a positive integer.'),
306
317
  max_files: z.number().int().positive().max(10000000).optional().describe('Max files to index (default: 10000). Use for very large codebases. Must be a positive integer.'),
307
318
  max_chars: z.number().int().positive().max(10000000).optional().describe('Max output chars before truncation. Targeted commands (about, context, smart, etc.): 10K default. Broad commands (toc, entrypoints, deadcode, etc.): 3K default. Max: 100K. Use all=true to bypass all caps.'),
@@ -376,6 +387,19 @@ server.registerTool(
376
387
  const te = strippedNote
377
388
  ? (msg) => toolError(msg + strippedNote)
378
389
  : toolError;
390
+ // Translate CLI flag syntax in execute-layer notes to MCP param
391
+ // syntax — the deadcode exported/decorated hints were already
392
+ // param-styled, but limit/depth/truncation notes leaked
393
+ // '--limit N' / '--max-files N' / '--all' at this surface.
394
+ const mn = (note) => note && note
395
+ .replace(/--limit N\b/g, 'limit=<n>')
396
+ .replace(/--max-files N\b/g, 'max_files=<n>')
397
+ .replace(/--depth=N\b/g, 'depth=<n>')
398
+ .replace(/--detailed\b/g, 'detailed=true')
399
+ .replace(/--all\b/g, 'all=true')
400
+ .replace(/--expand-unverified\b/g, 'expand_unverified=true')
401
+ .replace(/--include-uncertain\b/g, 'include_uncertain=true')
402
+ .replace(/--(\w[\w-]*)/g, (_m, f) => f.replace(/-/g, '_'));
379
403
 
380
404
  let index = null; // Track for post-command cache save
381
405
  try {
@@ -394,8 +418,9 @@ server.registerTool(
394
418
  let aboutText = output.formatAbout(result, {
395
419
  allHint: 'Repeat with all=true to show all.',
396
420
  showConfidence: ep.showConfidence !== false,
421
+ compact: ep.compact,
397
422
  });
398
- if (note) aboutText += '\n\n' + note;
423
+ if (note) aboutText += '\n\n' + mn(note);
399
424
  return tr(aboutText);
400
425
  }
401
426
 
@@ -406,10 +431,11 @@ server.registerTool(
406
431
  const { text, expandable } = output.formatContext(ctx, {
407
432
  expandHint: 'Use expand command with item number to see code for any item.',
408
433
  showConfidence: ep.showConfidence !== false,
434
+ compact: ep.compact,
409
435
  });
410
436
  expandCacheInstance.save(index.root, ep.name, ep.file, expandable);
411
437
  let ctxText = text;
412
- if (note) ctxText += '\n\n' + note;
438
+ if (note) ctxText += '\n\n' + mn(note);
413
439
  return tr(ctxText);
414
440
  }
415
441
 
@@ -417,8 +443,8 @@ server.registerTool(
417
443
  index = getIndex(project_dir, ep);
418
444
  const { ok, result, error, note } = execute(index, 'impact', ep);
419
445
  if (!ok) return te(error);
420
- let impactText = output.formatImpact(result);
421
- if (note) impactText += '\n\n' + note;
446
+ let impactText = output.formatImpact(result, { compact: ep.compact });
447
+ if (note) impactText += '\n\n' + mn(note);
422
448
  return tr(impactText);
423
449
  }
424
450
 
@@ -429,7 +455,7 @@ server.registerTool(
429
455
  let blastText = output.formatBlast(result, {
430
456
  allHint: 'Set depth to expand all children.',
431
457
  });
432
- if (note) blastText += '\n\n' + note;
458
+ if (note) blastText += '\n\n' + mn(note);
433
459
  return tr(blastText);
434
460
  }
435
461
 
@@ -438,7 +464,7 @@ server.registerTool(
438
464
  const { ok, result, error, note } = execute(index, 'smart', ep);
439
465
  if (!ok) return te(error);
440
466
  let smartText = output.formatSmart(result);
441
- if (note) smartText += '\n\n' + note;
467
+ if (note) smartText += '\n\n' + mn(note);
442
468
  return tr(smartText);
443
469
  }
444
470
 
@@ -450,7 +476,7 @@ server.registerTool(
450
476
  allHint: 'Set depth to expand all children.',
451
477
  methodsHint: 'Note: obj.method() calls excluded. Use include_methods=true to include them.'
452
478
  });
453
- if (note) traceText += '\n\n' + note;
479
+ if (note) traceText += '\n\n' + mn(note);
454
480
  return tr(traceText);
455
481
  }
456
482
 
@@ -461,7 +487,7 @@ server.registerTool(
461
487
  let rtText = output.formatReverseTrace(result, {
462
488
  allHint: 'Set depth to expand all children.',
463
489
  });
464
- if (note) rtText += '\n\n' + note;
490
+ if (note) rtText += '\n\n' + mn(note);
465
491
  return tr(rtText);
466
492
  }
467
493
 
@@ -480,7 +506,7 @@ server.registerTool(
480
506
  all: ep.all || false, top: ep.top,
481
507
  allHint: 'Repeat with all=true to show all.'
482
508
  });
483
- if (note) relText += '\n\n' + note;
509
+ if (note) relText += '\n\n' + mn(note);
484
510
  return tr(relText);
485
511
  }
486
512
 
@@ -498,6 +524,13 @@ server.registerTool(
498
524
  return tr(output.formatDoctor(result));
499
525
  }
500
526
 
527
+ case 'orient': {
528
+ index = getIndex(project_dir, ep);
529
+ const { ok, result, error } = execute(index, 'orient', ep);
530
+ if (!ok) return te(error);
531
+ return tr(output.formatOrient(result));
532
+ }
533
+
501
534
  case 'check': {
502
535
  index = getIndex(project_dir, ep);
503
536
  const { ok, result, error } = execute(index, 'check', ep);
@@ -511,8 +544,14 @@ server.registerTool(
511
544
  index = getIndex(project_dir, ep);
512
545
  const { ok, result, error, note } = execute(index, 'find', ep);
513
546
  if (!ok) return te(error);
514
- let text = output.formatFind(result, ep.name, ep.top);
515
- if (note) text += '\n\n' + note;
547
+ // Same formatter as every other surface (fix #250 — the
548
+ // legacy formatFind had a different default limit, no stable
549
+ // file:line:name handles, no confidence markers, and
550
+ // silently ignored all/depth/compact).
551
+ let text = output.formatFindDetailed(result, ep.name, {
552
+ depth: ep.depth, top: ep.top, all: ep.all, compact: ep.compact,
553
+ });
554
+ if (note) text += '\n\n' + mn(note);
516
555
  return tr(text);
517
556
  }
518
557
 
@@ -520,8 +559,8 @@ server.registerTool(
520
559
  index = getIndex(project_dir, ep);
521
560
  const { ok, result, error, note } = execute(index, 'usages', ep);
522
561
  if (!ok) return te(error);
523
- let text = output.formatUsages(result, ep.name);
524
- if (note) text += '\n\n' + note;
562
+ let text = output.formatUsages(result, ep.name, { compact: ep.compact });
563
+ if (note) text += '\n\n' + mn(note);
525
564
  return tr(text);
526
565
  }
527
566
 
@@ -532,7 +571,7 @@ server.registerTool(
532
571
  let text = output.formatToc(result, {
533
572
  topHint: 'Set top=N or use detailed=false for compact view.'
534
573
  });
535
- if (note) text += '\n\n' + note;
574
+ if (note) text += '\n\n' + mn(note);
536
575
  return tr(text);
537
576
  }
538
577
 
@@ -546,7 +585,7 @@ server.registerTool(
546
585
  } else {
547
586
  searchText = output.formatSearch(result, ep.term);
548
587
  }
549
- if (note) searchText += '\n\n' + note;
588
+ if (note) searchText += '\n\n' + mn(note);
550
589
  return tr(searchText);
551
590
  }
552
591
 
@@ -555,7 +594,7 @@ server.registerTool(
555
594
  const { ok, result, error, note } = execute(index, 'tests', ep);
556
595
  if (!ok) return te(error);
557
596
  let testsText = output.formatTests(result, ep.name);
558
- if (note) testsText += '\n\n' + note;
597
+ if (note) testsText += '\n\n' + mn(note);
559
598
  return tr(testsText);
560
599
  }
561
600
 
@@ -564,7 +603,7 @@ server.registerTool(
564
603
  const { ok, result, error, note } = execute(index, 'affectedTests', ep);
565
604
  if (!ok) return te(error);
566
605
  let atText = output.formatAffectedTests(result, { all: ep.all });
567
- if (note) atText += '\n\n' + note;
606
+ if (note) atText += '\n\n' + mn(note);
568
607
  return tr(atText);
569
608
  }
570
609
 
@@ -576,10 +615,10 @@ server.registerTool(
576
615
  let dcText = output.formatDeadcode(result, {
577
616
  top: ep.top || 0,
578
617
  decoratedHint: !ep.includeDecorated && result.excludedDecorated > 0 ? `${result.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use include_decorated=true to include them.` : undefined,
579
- exportedHint: !ep.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded (all have callers). Use include_exported=true to audit them.` : undefined,
618
+ exportedHint: !ep.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded from the audit (public API may have external callers). Use include_exported=true to audit them.` : undefined,
580
619
  externalContractHint: !ep.includeExported && result.excludedExternalContract > 0 ? `${result.excludedExternalContract} symbol(s) hidden (override an out-of-tree base class — reachable via external contract, not dead). Use include_exported=true to include them.` : undefined
581
620
  });
582
- if (dcNote) dcText += '\n\n' + dcNote;
621
+ if (dcNote) dcText += '\n\n' + mn(dcNote);
583
622
  return tr(dcText);
584
623
  }
585
624
 
@@ -588,7 +627,7 @@ server.registerTool(
588
627
  const { ok, result, error, note } = execute(index, 'entrypoints', ep);
589
628
  if (!ok) return te(error);
590
629
  let epText = output.formatEntrypoints(result);
591
- if (note) epText += '\n\n' + note;
630
+ if (note) epText += '\n\n' + mn(note);
592
631
  return tr(epText);
593
632
  }
594
633
 
@@ -597,7 +636,7 @@ server.registerTool(
597
636
  const { ok, result, error, note } = execute(index, 'endpoints', ep);
598
637
  if (!ok) return te(error);
599
638
  let endText = output.formatEndpoints(result, { bridge: result._bridge, unmatched: result._unmatched });
600
- if (note) endText += '\n\n' + note;
639
+ if (note) endText += '\n\n' + mn(note);
601
640
  return tr(endText);
602
641
  }
603
642
 
@@ -664,7 +703,7 @@ server.registerTool(
664
703
  const { ok, result, error, note } = execute(index, 'diffImpact', ep);
665
704
  if (!ok) return te(error);
666
705
  let diText = output.formatDiffImpact(result, { all: ep.all });
667
- if (note) diText += '\n\n' + note;
706
+ if (note) diText += '\n\n' + mn(note);
668
707
  return tr(diText);
669
708
  }
670
709
 
@@ -689,7 +728,7 @@ server.registerTool(
689
728
  const { ok, result, error, note } = execute(index, 'api', ep);
690
729
  if (!ok) return te(error);
691
730
  let apiText = output.formatApi(result, ep.file || '.');
692
- if (note) apiText += '\n\n' + note;
731
+ if (note) apiText += '\n\n' + mn(note);
693
732
  return tr(apiText);
694
733
  }
695
734
 
@@ -698,7 +737,7 @@ server.registerTool(
698
737
  const { ok, result, error, note } = execute(index, 'stats', ep);
699
738
  if (!ok) return te(error);
700
739
  let statsText = output.formatStats(result, { top: ep.top || 0 });
701
- if (note) statsText += '\n\n' + note;
740
+ if (note) statsText += '\n\n' + mn(note);
702
741
  return tr(statsText);
703
742
  }
704
743
 
@@ -707,7 +746,7 @@ server.registerTool(
707
746
  const { ok, result, error, note } = execute(index, 'auditAsync', ep);
708
747
  if (!ok) return te(error);
709
748
  let text = output.formatAuditAsync(result);
710
- if (note) text += '\n\n' + note;
749
+ if (note) text += '\n\n' + mn(note);
711
750
  return tr(text);
712
751
  }
713
752
 
@@ -722,7 +761,7 @@ server.registerTool(
722
761
  const check = resolveAndValidatePath(index, entry.match.relativePath || path.relative(index.root, entry.match.file));
723
762
  if (typeof check !== 'string') return check;
724
763
  }
725
- const fnText = (note ? note + '\n\n' : '') + output.formatFnResult(result);
764
+ const fnText = (note ? mn(note) + '\n\n' : '') + output.formatFnResult(result);
726
765
  return tr(fnText);
727
766
  }
728
767
 
@@ -735,7 +774,7 @@ server.registerTool(
735
774
  const check = resolveAndValidatePath(index, entry.match.relativePath || path.relative(index.root, entry.match.file));
736
775
  if (typeof check !== 'string') return check;
737
776
  }
738
- const classText = (note ? note + '\n\n' : '') + output.formatClassResult(result);
777
+ const classText = (note ? mn(note) + '\n\n' : '') + output.formatClassResult(result);
739
778
  return tr(classText);
740
779
  }
741
780
 
@@ -791,7 +830,9 @@ server.registerTool(
791
830
  async function main() {
792
831
  const transport = new StdioServerTransport();
793
832
  await server.connect(transport);
794
- console.error('UCN MCP server running on stdio');
833
+ // Print the running version so MCP-vs-CLI drift is visible (field-report #3:
834
+ // a stale `npx -y ucn` cache can silently run an older engine than the CLI).
835
+ console.error(`UCN MCP server v${require('../package.json').version} running on stdio`);
795
836
  }
796
837
 
797
838
  main().catch(e => {