ucn 4.0.2 → 4.1.1
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/.claude/skills/ucn/SKILL.md +31 -7
- package/README.md +90 -51
- package/cli/index.js +199 -94
- package/core/account.js +3 -1
- package/core/analysis.js +322 -304
- package/core/bridge.js +13 -8
- package/core/cache.js +109 -19
- package/core/callers.js +969 -77
- package/core/check.js +19 -8
- package/core/deadcode.js +368 -40
- package/core/discovery.js +31 -11
- package/core/entrypoints.js +149 -17
- package/core/execute.js +330 -43
- package/core/graph-build.js +61 -10
- package/core/graph.js +282 -61
- package/core/imports.js +72 -3
- package/core/output/analysis-ext.js +70 -10
- package/core/output/analysis.js +52 -33
- package/core/output/check.js +4 -1
- package/core/output/endpoints.js +8 -3
- package/core/output/extraction.js +12 -1
- package/core/output/find.js +23 -9
- package/core/output/graph.js +32 -9
- package/core/output/refactoring.js +147 -49
- package/core/output/reporting.js +104 -5
- package/core/output/search.js +30 -3
- package/core/output/shared.js +31 -4
- package/core/output/tracing.js +22 -11
- package/core/parser.js +8 -6
- package/core/project.js +167 -7
- package/core/registry.js +20 -16
- package/core/reporting.js +131 -13
- package/core/search.js +270 -55
- package/core/shared.js +240 -1
- package/core/stacktrace.js +23 -1
- package/core/tracing.js +278 -36
- package/core/verify.js +352 -349
- package/languages/go.js +29 -17
- package/languages/index.js +56 -0
- package/languages/java.js +133 -16
- package/languages/javascript.js +215 -27
- package/languages/python.js +113 -47
- package/languages/rust.js +89 -26
- package/languages/utils.js +35 -7
- package/mcp/server.js +69 -30
- package/package.json +5 -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
|
-
|
|
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: [
|
|
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)
|
|
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
|
-
|
|
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')
|
|
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
|
-
|
|
1973
|
-
grandparent.childForFieldName('function')
|
|
1974
|
-
|
|
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
|
|
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')
|
|
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')
|
|
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')
|
|
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')
|
|
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')
|
|
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')
|
|
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')
|
|
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')
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
package/languages/utils.js
CHANGED
|
@@ -11,8 +11,11 @@
|
|
|
11
11
|
*/
|
|
12
12
|
function traverseTree(node, callback, options) {
|
|
13
13
|
if (callback(node) === false) return;
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
687
|
-
|
|
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
|
|
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()
|
|
265
|
-
include_uncertain: z.boolean().optional().describe('
|
|
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
|
-
|
|
515
|
-
|
|
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 (
|
|
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
|
|