ucn 5.4.2 → 5.5.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.
- package/.claude/skills/ucn/SKILL.md +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +18 -12
- package/cli/index.js +40 -14
- package/core/accessors.js +118 -3
- package/core/account.js +30 -0
- package/core/analysis.js +508 -264
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +522 -31
- package/core/callers.js +12053 -1047
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +7 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +305 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +457 -60
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +2113 -0
- package/core/rust-modules.js +185 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1161 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2237 -115
- package/languages/utils.js +298 -4
- package/mcp/server.js +22 -8
- package/package.json +1 -1
package/languages/utils.js
CHANGED
|
@@ -10,18 +10,58 @@
|
|
|
10
10
|
* @param {function} [options.onLeave] - Called when leaving each node (after children processed)
|
|
11
11
|
*/
|
|
12
12
|
function traverseTree(node, callback, options) {
|
|
13
|
+
// The flat node list an extractor pass already built for this tree
|
|
14
|
+
// (fix #388): the same nodes in the same order, without a native
|
|
15
|
+
// children read per node. Never built here, so a walk of an uncached
|
|
16
|
+
// tree cannot evict a list another pass still uses.
|
|
17
|
+
const range = cachedNodeRange(node);
|
|
18
|
+
if (range) {
|
|
19
|
+
traverseNodeRange(range, callback, options?.onLeave);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
traverseTreeWalk(node, callback, options);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function traverseTreeWalk(node, callback, options) {
|
|
13
26
|
if (callback(node) === false) return;
|
|
14
27
|
// Single batched native call per node — namedChildCount + N × namedChild(i)
|
|
15
28
|
// costs N+1 native round-trips for the same children.
|
|
16
29
|
const children = node.namedChildren;
|
|
17
30
|
for (let i = 0; i < children.length; i++) {
|
|
18
|
-
|
|
31
|
+
traverseTreeWalk(children[i], callback, options);
|
|
19
32
|
}
|
|
20
33
|
if (options?.onLeave) {
|
|
21
34
|
options.onLeave(node);
|
|
22
35
|
}
|
|
23
36
|
}
|
|
24
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Depth-first visit of one subtree of a cached flat node list: `callback`
|
|
40
|
+
* on entry (false skips the node's descendants and its `onLeave`), then
|
|
41
|
+
* `onLeave` after its descendants, exactly as the recursive walk orders them.
|
|
42
|
+
*/
|
|
43
|
+
function traverseNodeRange({ nodes, subtreeEnds, index }, callback, onLeave) {
|
|
44
|
+
const end = subtreeEnds[index];
|
|
45
|
+
if (!onLeave) {
|
|
46
|
+
for (let i = index; i < end; ) {
|
|
47
|
+
if (callback(nodes[i]) === false) i = subtreeEnds[i];
|
|
48
|
+
else i++;
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const open = [];
|
|
53
|
+
for (let i = index; i < end; ) {
|
|
54
|
+
while (open.length > 0 && subtreeEnds[open[open.length - 1]] <= i) onLeave(nodes[open.pop()]);
|
|
55
|
+
if (callback(nodes[i]) === false) {
|
|
56
|
+
i = subtreeEnds[i];
|
|
57
|
+
} else {
|
|
58
|
+
open.push(i);
|
|
59
|
+
i++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
while (open.length > 0) onLeave(nodes[open.pop()]);
|
|
63
|
+
}
|
|
64
|
+
|
|
25
65
|
/**
|
|
26
66
|
* Get line locations from tree-sitter node
|
|
27
67
|
* Returns 1-indexed lines to match UCN output format
|
|
@@ -776,8 +816,29 @@ function findMatchesWithASTFilter(content, term, parser, options = {}) {
|
|
|
776
816
|
* traversal overhead (namedChild object creation, function call overhead).
|
|
777
817
|
*/
|
|
778
818
|
let _cachedRootNode = null;
|
|
819
|
+
let _cachedRootId = null;
|
|
779
820
|
let _cachedNodeList = null;
|
|
780
821
|
let _cachedSubtreeEnds = null;
|
|
822
|
+
let _previousRootNode = null;
|
|
823
|
+
let _previousRootId = null;
|
|
824
|
+
let _previousNodeList = null;
|
|
825
|
+
let _previousSubtreeEnds = null;
|
|
826
|
+
|
|
827
|
+
// The same syntax node can reach a caller through a different wrapper: the
|
|
828
|
+
// binding keeps one wrapper per node only while the last one is alive, so a
|
|
829
|
+
// collected wrapper makes every later `tree.rootNode` a fresh object (fix
|
|
830
|
+
// #387). A node is identified by its tree plus the native node id.
|
|
831
|
+
function nodeIdOf(node) {
|
|
832
|
+
const id = node?.tree ? node.id : undefined;
|
|
833
|
+
return typeof id === 'number' ? id : null;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function sameCachedNode(node, cached, cachedId) {
|
|
837
|
+
if (!cached || cachedId === null || node.tree !== cached.tree) return false;
|
|
838
|
+
return nodeIdOf(node) === cachedId;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const NAMED_CHILDREN_LIST = Symbol.for('ucn.namedChildrenList');
|
|
781
842
|
|
|
782
843
|
function _buildNodeList(rootNode) {
|
|
783
844
|
const nodes = [];
|
|
@@ -788,8 +849,9 @@ function _buildNodeList(rootNode) {
|
|
|
788
849
|
const idx = nodes.length;
|
|
789
850
|
nodes.push(node);
|
|
790
851
|
subtreeEnds.push(0);
|
|
791
|
-
// Batched children read — one native call instead of N+1 (see
|
|
792
|
-
|
|
852
|
+
// Batched children read — one native call instead of N+1 (see
|
|
853
|
+
// traverseTree); the array stays on the node for later reads.
|
|
854
|
+
const children = node[NAMED_CHILDREN_LIST] ? node[NAMED_CHILDREN_LIST]() : node.namedChildren;
|
|
793
855
|
for (let i = 0; i < children.length; i++) {
|
|
794
856
|
collect(children[i]);
|
|
795
857
|
}
|
|
@@ -806,16 +868,87 @@ function _buildNodeList(rootNode) {
|
|
|
806
868
|
* enabling O(1) subtree skipping (for 'return false' semantics).
|
|
807
869
|
*/
|
|
808
870
|
function getCachedNodeList(rootNode) {
|
|
809
|
-
if (rootNode === _cachedRootNode
|
|
871
|
+
if (_cachedNodeList && (rootNode === _cachedRootNode ||
|
|
872
|
+
sameCachedNode(rootNode, _cachedRootNode, _cachedRootId))) {
|
|
810
873
|
return { nodes: _cachedNodeList, subtreeEnds: _cachedSubtreeEnds };
|
|
811
874
|
}
|
|
875
|
+
// Two slots (fix #365): C/C++ indexing alternates between a selected
|
|
876
|
+
// recovery tree and its literal all-source tree, which evicted a single
|
|
877
|
+
// slot on every extractor pass.
|
|
878
|
+
if (_previousNodeList && (rootNode === _previousRootNode ||
|
|
879
|
+
sameCachedNode(rootNode, _previousRootNode, _previousRootId))) {
|
|
880
|
+
const nodes = _previousNodeList;
|
|
881
|
+
const subtreeEnds = _previousSubtreeEnds;
|
|
882
|
+
const id = _previousRootId;
|
|
883
|
+
_previousRootNode = _cachedRootNode;
|
|
884
|
+
_previousRootId = _cachedRootId;
|
|
885
|
+
_previousNodeList = _cachedNodeList;
|
|
886
|
+
_previousSubtreeEnds = _cachedSubtreeEnds;
|
|
887
|
+
_cachedRootNode = rootNode;
|
|
888
|
+
_cachedRootId = id;
|
|
889
|
+
_cachedNodeList = nodes;
|
|
890
|
+
_cachedSubtreeEnds = subtreeEnds;
|
|
891
|
+
return { nodes, subtreeEnds };
|
|
892
|
+
}
|
|
812
893
|
const { nodes, subtreeEnds } = _buildNodeList(rootNode);
|
|
894
|
+
_previousRootNode = _cachedRootNode;
|
|
895
|
+
_previousRootId = _cachedRootId;
|
|
896
|
+
_previousNodeList = _cachedNodeList;
|
|
897
|
+
_previousSubtreeEnds = _cachedSubtreeEnds;
|
|
813
898
|
_cachedRootNode = rootNode;
|
|
899
|
+
_cachedRootId = nodeIdOf(rootNode);
|
|
814
900
|
_cachedNodeList = nodes;
|
|
815
901
|
_cachedSubtreeEnds = subtreeEnds;
|
|
816
902
|
return { nodes, subtreeEnds };
|
|
817
903
|
}
|
|
818
904
|
|
|
905
|
+
/** Index of `node` in a flat preorder list (sorted by start offset), or -1. */
|
|
906
|
+
function nodeListIndex(nodes, node) {
|
|
907
|
+
const start = node.startIndex;
|
|
908
|
+
let lo = 0;
|
|
909
|
+
let hi = nodes.length;
|
|
910
|
+
while (lo < hi) {
|
|
911
|
+
const mid = (lo + hi) >> 1;
|
|
912
|
+
if (nodes[mid].startIndex < start) lo = mid + 1;
|
|
913
|
+
else hi = mid;
|
|
914
|
+
}
|
|
915
|
+
for (let i = lo; i < nodes.length && nodes[i].startIndex === start; i++) {
|
|
916
|
+
if (nodes[i] === node) return i;
|
|
917
|
+
}
|
|
918
|
+
const id = nodeIdOf(node);
|
|
919
|
+
if (id === null) return -1;
|
|
920
|
+
for (let i = lo; i < nodes.length && nodes[i].startIndex === start; i++) {
|
|
921
|
+
if (nodeIdOf(nodes[i]) === id) return i;
|
|
922
|
+
}
|
|
923
|
+
return -1;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* The already-built flat node list holding `node` (fix #388), as
|
|
928
|
+
* { nodes, subtreeEnds, index } with nodes[index] the node, or null when no
|
|
929
|
+
* cached list covers it. Named nodes only, like `namedChildren`.
|
|
930
|
+
*/
|
|
931
|
+
function cachedNodeRange(node) {
|
|
932
|
+
if (!node) return null;
|
|
933
|
+
const tree = node.tree;
|
|
934
|
+
if (!tree) return null;
|
|
935
|
+
if (_cachedNodeList && _cachedRootNode?.tree === tree) {
|
|
936
|
+
if (node === _cachedRootNode || sameCachedNode(node, _cachedRootNode, _cachedRootId)) {
|
|
937
|
+
return { nodes: _cachedNodeList, subtreeEnds: _cachedSubtreeEnds, index: 0 };
|
|
938
|
+
}
|
|
939
|
+
const index = nodeListIndex(_cachedNodeList, node);
|
|
940
|
+
if (index >= 0) return { nodes: _cachedNodeList, subtreeEnds: _cachedSubtreeEnds, index };
|
|
941
|
+
}
|
|
942
|
+
if (_previousNodeList && _previousRootNode?.tree === tree) {
|
|
943
|
+
if (node === _previousRootNode || sameCachedNode(node, _previousRootNode, _previousRootId)) {
|
|
944
|
+
return { nodes: _previousNodeList, subtreeEnds: _previousSubtreeEnds, index: 0 };
|
|
945
|
+
}
|
|
946
|
+
const index = nodeListIndex(_previousNodeList, node);
|
|
947
|
+
if (index >= 0) return { nodes: _previousNodeList, subtreeEnds: _previousSubtreeEnds, index };
|
|
948
|
+
}
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
|
|
819
952
|
/**
|
|
820
953
|
* Traverse a tree-sitter AST using a cached flat node list.
|
|
821
954
|
* Semantically equivalent to traverseTree() but ~3x faster when the same
|
|
@@ -873,8 +1006,13 @@ function visitNameNodes(tree, code, name, callback) {
|
|
|
873
1006
|
*/
|
|
874
1007
|
function clearNodeListCache() {
|
|
875
1008
|
_cachedRootNode = null;
|
|
1009
|
+
_cachedRootId = null;
|
|
876
1010
|
_cachedNodeList = null;
|
|
877
1011
|
_cachedSubtreeEnds = null;
|
|
1012
|
+
_previousRootNode = null;
|
|
1013
|
+
_previousRootId = null;
|
|
1014
|
+
_previousNodeList = null;
|
|
1015
|
+
_previousSubtreeEnds = null;
|
|
878
1016
|
}
|
|
879
1017
|
|
|
880
1018
|
/**
|
|
@@ -1071,13 +1209,168 @@ function sameNode(a, b) {
|
|
|
1071
1209
|
return !!a && !!b && (a === b || a.id === b.id);
|
|
1072
1210
|
}
|
|
1073
1211
|
|
|
1212
|
+
/**
|
|
1213
|
+
* Whether a function body contains a node of `targetTypes` in its OWN scope:
|
|
1214
|
+
* nested scopes listed in `boundaryTypes` (inner functions, lambdas, classes)
|
|
1215
|
+
* are not descended. Used to recognize generator/iterator functions by their
|
|
1216
|
+
* `yield` (a yield inside a nested def makes the NESTED def the generator).
|
|
1217
|
+
*/
|
|
1218
|
+
function containsOwnNode(body, targetTypes, boundaryTypes) {
|
|
1219
|
+
if (!body) return false;
|
|
1220
|
+
const stack = [body];
|
|
1221
|
+
while (stack.length > 0) {
|
|
1222
|
+
const node = stack.pop();
|
|
1223
|
+
const count = node.namedChildCount;
|
|
1224
|
+
for (let i = 0; i < count; i++) {
|
|
1225
|
+
const child = node.namedChild(i);
|
|
1226
|
+
if (targetTypes.has(child.type)) return true;
|
|
1227
|
+
if (!boundaryTypes.has(child.type)) stack.push(child);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* fix #367e: line ranges the parser could only recover, not parse. Inside an
|
|
1235
|
+
* outermost ERROR node, a damaged child (loose token, erroneous or MISSING
|
|
1236
|
+
* content) opens a region; clean statements/expressions that follow extend
|
|
1237
|
+
* it (they sit in unparsed context: `ok();` after a broken signature); a
|
|
1238
|
+
* clean complete declaration/definition/import/directive closes it, and
|
|
1239
|
+
* comments are neutral. A MISSING token outside any ERROR marks its own line.
|
|
1240
|
+
* Only subtrees with `hasError` are visited. Returns merged, sorted
|
|
1241
|
+
* [startLine, endLine] pairs (1-based), or [] for a clean tree.
|
|
1242
|
+
*/
|
|
1243
|
+
const COMPLETE_CONSTRUCT_RE = /declaration|definition|_item$|^import|^preproc_|^package_clause$|^using_directive$|^module$/;
|
|
1244
|
+
|
|
1245
|
+
function parseErrorRegions(rootNode) {
|
|
1246
|
+
const regions = [];
|
|
1247
|
+
if (!rootNode?.hasError) return regions;
|
|
1248
|
+
const lineOf = position => position.row + 1;
|
|
1249
|
+
// Children are read in one batch per node (fix #388).
|
|
1250
|
+
const errorRegions = node => {
|
|
1251
|
+
let open = null;
|
|
1252
|
+
const close = () => { if (open) { regions.push(open); open = null; } };
|
|
1253
|
+
const children = node.children;
|
|
1254
|
+
for (let i = 0; i < children.length; i++) {
|
|
1255
|
+
const child = children[i];
|
|
1256
|
+
const type = child.type;
|
|
1257
|
+
if (type.includes('comment')) continue;
|
|
1258
|
+
const damaged = child.hasError || child.isMissing || type === 'ERROR' ||
|
|
1259
|
+
!child.isNamed || child.childCount === 0;
|
|
1260
|
+
const start = lineOf(child.startPosition);
|
|
1261
|
+
const end = lineOf(child.endPosition);
|
|
1262
|
+
if (damaged) {
|
|
1263
|
+
if (open) open[1] = Math.max(open[1], end);
|
|
1264
|
+
else open = [start, end];
|
|
1265
|
+
} else if (COMPLETE_CONSTRUCT_RE.test(type)) {
|
|
1266
|
+
close();
|
|
1267
|
+
} else if (open) {
|
|
1268
|
+
open[1] = Math.max(open[1], end);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
close();
|
|
1272
|
+
};
|
|
1273
|
+
const visit = node => {
|
|
1274
|
+
if (node.isMissing) {
|
|
1275
|
+
const line = lineOf(node.startPosition);
|
|
1276
|
+
regions.push([line, line]);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
if (!node.hasError) return;
|
|
1280
|
+
if (node.type === 'ERROR') {
|
|
1281
|
+
errorRegions(node);
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
const children = node.children;
|
|
1285
|
+
for (let i = 0; i < children.length; i++) visit(children[i]);
|
|
1286
|
+
};
|
|
1287
|
+
visit(rootNode);
|
|
1288
|
+
regions.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
1289
|
+
const merged = [];
|
|
1290
|
+
for (const region of regions) {
|
|
1291
|
+
const last = merged[merged.length - 1];
|
|
1292
|
+
if (last && region[0] <= last[1]) last[1] = Math.max(last[1], region[1]);
|
|
1293
|
+
else merged.push([...region]);
|
|
1294
|
+
}
|
|
1295
|
+
return merged;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/**
|
|
1299
|
+
* fix #380: generic arity of the LAST segment of a written type name
|
|
1300
|
+
* (`Outcome` 0, `Outcome<int>` 1, `Dictionary<string, List<int>>` 2,
|
|
1301
|
+
* `Outcome<,>` 2, `A<int>.B` 0, `Outcome<int>?` 1). Null for text that is not
|
|
1302
|
+
* a type-name shape.
|
|
1303
|
+
*/
|
|
1304
|
+
function genericArityOf(text) {
|
|
1305
|
+
if (text == null) return null;
|
|
1306
|
+
let value = String(text).trim().replace(/\?$/, '');
|
|
1307
|
+
while (value.endsWith('[]')) value = value.slice(0, -2).trim();
|
|
1308
|
+
if (!value) return null;
|
|
1309
|
+
if (!value.endsWith('>')) {
|
|
1310
|
+
return /^[A-Za-z_@][\w@]*$/.test(value.split(/::|\./).pop() || '') ? 0 : null;
|
|
1311
|
+
}
|
|
1312
|
+
let depth = 0;
|
|
1313
|
+
let commas = 0;
|
|
1314
|
+
for (let i = value.length - 1; i >= 0; i--) {
|
|
1315
|
+
const ch = value[i];
|
|
1316
|
+
if (ch === '>') depth++;
|
|
1317
|
+
else if (ch === '<') {
|
|
1318
|
+
depth--;
|
|
1319
|
+
if (depth === 0) return commas + 1;
|
|
1320
|
+
} else if (ch === ',' && depth === 1) commas++;
|
|
1321
|
+
}
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// Exclusive clauses of one compound statement (fix #398F): `if`/`elif`/
|
|
1326
|
+
// `else` and `case` arms never both run; a `try` body and its handlers, two
|
|
1327
|
+
// handlers, a handler and the `try` `else` are alternatives. A `try` body and
|
|
1328
|
+
// its `else` run in sequence, and `finally` always runs.
|
|
1329
|
+
function branchClausesExclusive(a, b) {
|
|
1330
|
+
if (a === b) return false;
|
|
1331
|
+
const kind = tag => tag.split(':')[0];
|
|
1332
|
+
const ka = kind(a), kb = kind(b);
|
|
1333
|
+
const branch = new Set(['if', 'elif', 'else']);
|
|
1334
|
+
if (branch.has(ka) && branch.has(kb)) return true;
|
|
1335
|
+
if (ka === 'case' && kb === 'case') return true;
|
|
1336
|
+
if (ka === 'finally' || kb === 'finally') return false;
|
|
1337
|
+
const pair = new Set([ka, kb]);
|
|
1338
|
+
if (pair.has('except') && (pair.has('try') || pair.has('try-else') || ka === kb)) return true;
|
|
1339
|
+
return false;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* The package name a Go import path suggests when the import writes none,
|
|
1344
|
+
* by the Go tools' convention: the last element, or the one before a major
|
|
1345
|
+
* version suffix (v2 and up: `example.com/mod/v3` is package `mod`; a module
|
|
1346
|
+
* path never ends in /v0 or /v1, so such an element is a directory, and
|
|
1347
|
+
* `k8s.io/api/core/v1` declares package `v1`), without a `go-` prefix and up
|
|
1348
|
+
* to the first character no identifier holds (`github.com/goccy/go-json` is
|
|
1349
|
+
* `json`, `gopkg.in/yaml.v3` `yaml`, `k8s.io/mount-utils` `mount`). Only a
|
|
1350
|
+
* suggestion: the local name is the imported package's own clause (fix #400).
|
|
1351
|
+
*/
|
|
1352
|
+
function goPathPackageName(modulePath) {
|
|
1353
|
+
const parts = String(modulePath).split('/');
|
|
1354
|
+
let base = parts[parts.length - 1];
|
|
1355
|
+
if (/^v([2-9]|[1-9]\d+)$/.test(base) && parts.length > 1) base = parts[parts.length - 2];
|
|
1356
|
+
if (base.startsWith('go-')) base = base.slice(3);
|
|
1357
|
+
const cut = base.search(/[^\p{L}\p{N}_]/u);
|
|
1358
|
+
return cut > 0 ? base.slice(0, cut) : base;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1074
1361
|
module.exports = {
|
|
1362
|
+
genericArityOf,
|
|
1363
|
+
goPathPackageName,
|
|
1364
|
+
parseErrorRegions,
|
|
1365
|
+
containsOwnNode,
|
|
1075
1366
|
nodeTextWithoutComments,
|
|
1076
1367
|
sameNode,
|
|
1077
1368
|
traverseTree,
|
|
1078
1369
|
traverseTreeCached,
|
|
1079
1370
|
visitNameNodes,
|
|
1080
1371
|
getCachedNodeList,
|
|
1372
|
+
cachedNodeRange,
|
|
1373
|
+
traverseNodeRange,
|
|
1081
1374
|
clearNodeListCache,
|
|
1082
1375
|
nodeToLocation,
|
|
1083
1376
|
extractParams,
|
|
@@ -1096,4 +1389,5 @@ module.exports = {
|
|
|
1096
1389
|
extractStringArg,
|
|
1097
1390
|
stripQuotes,
|
|
1098
1391
|
extractSprintfPrefix,
|
|
1392
|
+
branchClausesExclusive,
|
|
1099
1393
|
};
|
package/mcp/server.js
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* Keeps a per-project index cache for fast repeat queries.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
// Reuse V8 code caches across processes (Node >= 22.1; fix #365): compiling
|
|
11
|
+
// the engine's modules is a fixed cost of every command. Node validates each
|
|
12
|
+
// entry against the source, and NODE_DISABLE_COMPILE_CACHE turns it off.
|
|
13
|
+
try { require('module').enableCompileCache?.(); } catch (_) { /* optional */ }
|
|
14
|
+
|
|
10
15
|
const fs = require('fs');
|
|
11
16
|
const path = require('path');
|
|
12
17
|
const { StdioMcpServer } = require('./stdio-server');
|
|
@@ -30,6 +35,7 @@ const {
|
|
|
30
35
|
formatSurfaceMessage,
|
|
31
36
|
} = require('../core/registry');
|
|
32
37
|
const { execute } = require('../core/execute');
|
|
38
|
+
const { describeError, UcnError } = require('../core/errors');
|
|
33
39
|
const { applyOutputBudget, MAX_OUTPUT_CHARS } = require('../core/output-budget');
|
|
34
40
|
|
|
35
41
|
// ============================================================================
|
|
@@ -41,14 +47,14 @@ const MAX_CACHE_SIZE = 10;
|
|
|
41
47
|
|
|
42
48
|
function getIndex(projectDir, options) {
|
|
43
49
|
if (typeof projectDir !== 'string' || projectDir.trim().length === 0) {
|
|
44
|
-
throw new
|
|
50
|
+
throw new UcnError('project_dir is required and must be a non-empty path.');
|
|
45
51
|
}
|
|
46
52
|
const maxFiles = options && options.maxFiles;
|
|
47
53
|
const followSymlinks = options && options.followSymlinks;
|
|
48
54
|
const includeBundled = options?.includeBundled === true;
|
|
49
55
|
const absDir = path.resolve(projectDir);
|
|
50
56
|
if (!fs.existsSync(absDir) || !fs.statSync(absDir).isDirectory()) {
|
|
51
|
-
throw new
|
|
57
|
+
throw new UcnError(`Project directory not found: ${absDir}`);
|
|
52
58
|
}
|
|
53
59
|
const root = findProjectRoot(absDir);
|
|
54
60
|
const cached = indexCache.get(root);
|
|
@@ -137,6 +143,11 @@ function toolError(message) {
|
|
|
137
143
|
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
138
144
|
}
|
|
139
145
|
|
|
146
|
+
/** An exception inside UCN: marked internal, never rendered as a refusal (fix #394). */
|
|
147
|
+
function internalToolError(message) {
|
|
148
|
+
return { content: [{ type: 'text', text: message }], isError: true };
|
|
149
|
+
}
|
|
150
|
+
|
|
140
151
|
function paramEditDistance(a, b) {
|
|
141
152
|
if (Math.abs(a.length - b.length) > 2) return 3;
|
|
142
153
|
const prev = new Array(b.length + 1);
|
|
@@ -260,7 +271,7 @@ const INPUT_SHAPE = {
|
|
|
260
271
|
sections: stringParam('Comma-separated projection. show: summary,callers,callees,source,dependencies,tests,types,example,related. repo: summary,files,stats,health.'),
|
|
261
272
|
exclude: stringParam('Comma-separated patterns to exclude (e.g. "test,mock,vendor")'),
|
|
262
273
|
include_tests: booleanParam('Include test files in results (excluded by default)'),
|
|
263
|
-
exclude_tests: booleanParam('
|
|
274
|
+
exclude_tests: booleanParam('Exclude test files: the default for entrypoints (include_tests=true lists them); usages and endpoints list test files unless exclude_tests=true.'),
|
|
264
275
|
include_methods: booleanParam('Include method callees where receiver evidence permits. Caller-bearing views always tier method sites.'),
|
|
265
276
|
expand_unverified: booleanParam('trace callers: follow unverified edges; downstream nodes remain marked possible, never confirmed.'),
|
|
266
277
|
min_confidence: numberParam('Minimum ordinal evidence weight (legacy name; not a probability) for caller/callee edges', { minimum: 0, maximum: 1 }),
|
|
@@ -292,11 +303,11 @@ const INPUT_SHAPE = {
|
|
|
292
303
|
git: booleanParam('show summary: attach last-modified, author, and recent-change metadata.'),
|
|
293
304
|
add_param: stringParam('Parameter name to add (plan command)'),
|
|
294
305
|
remove_param: stringParam('Parameter name to remove (plan command)'),
|
|
295
|
-
rename_to: stringParam('New function
|
|
306
|
+
rename_to: stringParam('New name for the function, method, or type (plan command)'),
|
|
296
307
|
default_value: stringParam('Default value for added parameter (plan command)'),
|
|
297
308
|
stack: stringParam('The stack trace text to parse (stacktrace command)'),
|
|
298
309
|
range: stringParam('source line range, e.g. "10-20" or "15"; requires file.'),
|
|
299
|
-
base: stringParam('Git ref
|
|
310
|
+
base: stringParam('Git ref (default: HEAD). impact/check without a symbol: diff against it; check with a symbol: compare the declaration with its version there. E.g. "HEAD~3", "main", a commit SHA'),
|
|
300
311
|
staged: booleanParam('impact/check without a symbol: analyze staged changes.'),
|
|
301
312
|
deep: booleanParam('repo: include health and sample the ordinal resolution-evidence profile, not accuracy.'),
|
|
302
313
|
compact: booleanParam('Compact output defaults to true for show/impact and false for usages; set the opposite value to change that command\'s presentation.'),
|
|
@@ -305,7 +316,7 @@ const INPUT_SHAPE = {
|
|
|
305
316
|
top_level: booleanParam('repo files: show only top-level functions.'),
|
|
306
317
|
class_name: stringParam('Class name to scope method analysis (e.g. "MarketDataFetcher" for close)'),
|
|
307
318
|
line: integerParam('Definition line pin. Resolves the symbol defined at this exact line (the middle component of a file:line:name handle). Disambiguates same-file same-name definitions.', { exclusiveMinimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
|
|
308
|
-
limit: integerParam('Max results to return (default: 500; structural search: 50; usages and lines: uncapped). Caps find, usages, search, deadcode, api, and repo files. Must be a positive integer.', { exclusiveMinimum: 0, maximum: 1000000 }),
|
|
319
|
+
limit: integerParam('Max results to return (default: 500; structural search: 50; usages and lines: uncapped). Caps find, usages, search, deadcode, api, and repo files. find text shows the top 5 in detail and counts the rest; limit raises both. Must be a positive integer.', { exclusiveMinimum: 0, maximum: 1000000 }),
|
|
309
320
|
max_files: integerParam('Max files to index (default: 10000). Use for very large codebases. Must be a positive integer.', { exclusiveMinimum: 0, maximum: 10000000 }),
|
|
310
321
|
max_chars: integerParam('Max output characters, not UTF-8 bytes, including notes and preserved metadata. Broad sweep commands (repo, entrypoints, endpoints, deadcode, deps, check, audit_async) default to 3K; all other commands default to 10K. Maximum: 100K. all=true lifts formatter caps but keeps the 100K transport ceiling.', { exclusiveMinimum: 0, maximum: 100000 }),
|
|
311
322
|
type: stringParam('Symbol type filter for structural search: function, class, call, method, type, state, field, constant, macro. Triggers index-based search.'),
|
|
@@ -469,6 +480,7 @@ server.registerTool(
|
|
|
469
480
|
}
|
|
470
481
|
const execution = execute(index, canonicalCommand, ep);
|
|
471
482
|
if (!execution.ok) {
|
|
483
|
+
if (execution.internalError) return internalToolError(execution.error + strippedNote);
|
|
472
484
|
return te(formatSurfaceMessage(execution.error, 'mcp'));
|
|
473
485
|
}
|
|
474
486
|
|
|
@@ -507,7 +519,9 @@ server.registerTool(
|
|
|
507
519
|
));
|
|
508
520
|
|
|
509
521
|
} catch (e) {
|
|
510
|
-
|
|
522
|
+
const described = describeError(e);
|
|
523
|
+
if (described.internalError) return internalToolError(described.error + strippedNote);
|
|
524
|
+
return te(described.error);
|
|
511
525
|
} finally {
|
|
512
526
|
// Persist calls cache after command execution.
|
|
513
527
|
// getIndex() only saves after build (when callsCache is empty).
|
|
@@ -516,7 +530,7 @@ server.registerTool(
|
|
|
516
530
|
// MED-1: also persist when reachability was computed in-process so
|
|
517
531
|
// long-lived MCP servers carry the BFS result forward to disk.
|
|
518
532
|
if (index && !ep.includeBundled && !ep.maxFiles &&
|
|
519
|
-
(index.callsCacheDirty || index.reachabilityDirty || index.computedDispatchDirty)) {
|
|
533
|
+
(index.callsCacheDirty || index.reachabilityDirty || index.computedDispatchDirty || index.macroExpansionDirty)) {
|
|
520
534
|
try { index.saveCache(); } catch (_) { /* best-effort */ }
|
|
521
535
|
index.callsCacheDirty = false;
|
|
522
536
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.5.0",
|
|
4
4
|
"mcpName": "io.github.mleoca/ucn",
|
|
5
5
|
"description": "Auditable AST code intelligence for AI agents: 18 task-oriented commands through one MCP tool, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java, C, C++, C#, and HTML.",
|
|
6
6
|
"main": "index.js",
|