ucn 4.0.0 → 4.0.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/cli/index.js +4 -2
- package/core/callers.js +5 -19
- package/core/deadcode.js +98 -1
- package/core/execute.js +2 -1
- package/core/graph-build.js +1 -1
- package/core/output/reporting.js +13 -3
- package/core/shared.js +21 -0
- package/mcp/server.js +2 -1
- package/package.json +1 -1
package/cli/index.js
CHANGED
|
@@ -1052,7 +1052,8 @@ function runProjectCommand(rootDir, command, arg) {
|
|
|
1052
1052
|
r => output.formatDeadcode(r, {
|
|
1053
1053
|
top: flags.top,
|
|
1054
1054
|
decoratedHint: !flags.includeDecorated && result.excludedDecorated > 0 ? `${result.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use --include-decorated to include them.` : undefined,
|
|
1055
|
-
exportedHint: !flags.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded (all have callers). Use --include-exported to audit them.` : undefined
|
|
1055
|
+
exportedHint: !flags.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded (all have callers). Use --include-exported to audit them.` : undefined,
|
|
1056
|
+
externalContractHint: !flags.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 to include them.` : undefined
|
|
1056
1057
|
})
|
|
1057
1058
|
);
|
|
1058
1059
|
break;
|
|
@@ -1911,7 +1912,8 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
|
|
|
1911
1912
|
console.log(output.formatDeadcode(result, {
|
|
1912
1913
|
top: iflags.top,
|
|
1913
1914
|
decoratedHint: !iflags.includeDecorated && result.excludedDecorated > 0 ? `${result.excludedDecorated} decorated/annotated symbol(s) hidden (framework-registered). Use --include-decorated to include them.` : undefined,
|
|
1914
|
-
exportedHint: !iflags.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded (all have callers). Use --include-exported to audit them.` : undefined
|
|
1915
|
+
exportedHint: !iflags.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded (all have callers). Use --include-exported to audit them.` : undefined,
|
|
1916
|
+
externalContractHint: !iflags.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 to include them.` : undefined
|
|
1915
1917
|
}));
|
|
1916
1918
|
break;
|
|
1917
1919
|
}
|
package/core/callers.js
CHANGED
|
@@ -10,7 +10,7 @@ const path = require('path');
|
|
|
10
10
|
const crypto = require('crypto');
|
|
11
11
|
const { detectLanguage, getParser, getLanguageModule, langTraits } = require('../languages');
|
|
12
12
|
const { isTestFile } = require('./discovery');
|
|
13
|
-
const { NON_CALLABLE_TYPES } = require('./shared');
|
|
13
|
+
const { NON_CALLABLE_TYPES, isOverrideMarked } = require('./shared');
|
|
14
14
|
const { scoreEdge, tierForResolution, TIER } = require('./confidence');
|
|
15
15
|
const { findGoModule } = require('./imports');
|
|
16
16
|
|
|
@@ -233,7 +233,7 @@ function findCallers(index, name, options = {}) {
|
|
|
233
233
|
// `some`, not `every`: one marked overload proves the NAME exists
|
|
234
234
|
// on an external contract — receiver identity is then unprovable
|
|
235
235
|
// for every call shape (external signatures are invisible).
|
|
236
|
-
const marked = tDefs.find(d =>
|
|
236
|
+
const marked = tDefs.find(d => isOverrideMarked(d));
|
|
237
237
|
if (marked) _extContract = { via: _externalContractVia(index, marked) };
|
|
238
238
|
}
|
|
239
239
|
return _extContract;
|
|
@@ -3838,7 +3838,7 @@ function _nameBindingReaches(index, startAbs, name, targetFiles, maxDepth = 4) {
|
|
|
3838
3838
|
const next = [];
|
|
3839
3839
|
for (const [abs, attr] of frontier) {
|
|
3840
3840
|
if (targetFiles.has(abs)) return 'yes';
|
|
3841
|
-
const stateKey = `${abs}
|
|
3841
|
+
const stateKey = `${abs}\x00${attr}`;
|
|
3842
3842
|
if (visited.has(stateKey)) continue;
|
|
3843
3843
|
visited.add(stateKey);
|
|
3844
3844
|
const fe = index.files.get(abs);
|
|
@@ -4463,22 +4463,8 @@ function _targetAncestryFullyResolved(index, targetDefs) {
|
|
|
4463
4463
|
return true;
|
|
4464
4464
|
}
|
|
4465
4465
|
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
* are language-disjoint: traitImpl is Rust-only, an 'override' modifier is
|
|
4469
|
-
* Java's lowercased @Override annotation, an override-bearing memberType is
|
|
4470
|
-
* TS's `override` keyword, and an override decorator is Python's
|
|
4471
|
-
* typing.@override. The marker is compiler-checked syntax in all four —
|
|
4472
|
-
* never inferred.
|
|
4473
|
-
*/
|
|
4474
|
-
function _externalContractMarker(def) {
|
|
4475
|
-
if (def.traitImpl) return true;
|
|
4476
|
-
if (def.modifiers && def.modifiers.includes('override')) return true;
|
|
4477
|
-
if (def.memberType && /\boverride\b/.test(def.memberType)) return true;
|
|
4478
|
-
if (def.decorators && def.decorators.some(d =>
|
|
4479
|
-
String(d).replace(/\(.*$/, '').split('.').pop() === 'override')) return true;
|
|
4480
|
-
return false;
|
|
4481
|
-
}
|
|
4466
|
+
// _externalContractMarker moved to core/shared.js as isOverrideMarked (shared
|
|
4467
|
+
// with deadcode's out-of-tree override suppression — one source of truth).
|
|
4482
4468
|
|
|
4483
4469
|
/**
|
|
4484
4470
|
* Name of the external contract a marked method implements, for dispatch
|
package/core/deadcode.js
CHANGED
|
@@ -8,6 +8,90 @@
|
|
|
8
8
|
const { detectLanguage, getParser, getLanguageModule, safeParse, langTraits } = require('../languages');
|
|
9
9
|
const { isTestFile } = require('./discovery');
|
|
10
10
|
const { isFrameworkEntrypoint } = require('./entrypoints');
|
|
11
|
+
const { splitParentList } = require('./graph-build');
|
|
12
|
+
const { isOverrideMarked } = require('./shared');
|
|
13
|
+
|
|
14
|
+
const _CLASS_KINDS = ['class', 'struct', 'interface', 'trait', 'record'];
|
|
15
|
+
|
|
16
|
+
/** Strip a base-type expression to its bare name: `Mapping[str, int]`→Mapping, `java.util.List<Foo>`→List, `a::b::C`→C. */
|
|
17
|
+
function _bareBaseName(raw) {
|
|
18
|
+
return String(raw).replace(/[<[(].*$/s, '').split('.').pop().split('::').pop().trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The universal object root (Python `object`, Java/JS `Object`) has a fixed,
|
|
22
|
+
// known method surface — Object/dunder methods, themselves entry points — so it
|
|
23
|
+
// never dispatches an arbitrary subclass method by name or override. It is NOT
|
|
24
|
+
// an external *dispatching* base: `class Foo(object)` must still report its
|
|
25
|
+
// genuinely-dead inherent methods, exactly like `class Foo`, instead of
|
|
26
|
+
// diverging on a purely cosmetic base declaration. (Java `Object` is the
|
|
27
|
+
// universalSupertype trait; `object` is the Python equivalent — a language
|
|
28
|
+
// convention, rule #9.)
|
|
29
|
+
const _UNIVERSAL_ROOTS = new Set(['object', 'Object']);
|
|
30
|
+
|
|
31
|
+
/** True when a base name resolves to NO in-project class/struct/interface/trait/record (an out-of-tree type). */
|
|
32
|
+
function _baseIsExternal(index, bare) {
|
|
33
|
+
if (!bare || _UNIVERSAL_ROOTS.has(bare)) return false;
|
|
34
|
+
const defs = index.symbols.get(bare);
|
|
35
|
+
return !(defs && defs.some(d => _CLASS_KINDS.includes(d.type)));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Does the method's enclosing class EXTEND at least one base that is NOT in the
|
|
40
|
+
* project index? An out-of-tree base is a framework/library type UCN can't see;
|
|
41
|
+
* via inheritance it may dispatch into a public method of the subclass
|
|
42
|
+
* polymorphically (Starlette → build_middleware_stack) or by name convention
|
|
43
|
+
* (Pydantic → bytes_schema). The class def is matched in the method's own file.
|
|
44
|
+
*
|
|
45
|
+
* `implements` is deliberately NOT consulted: implementing an external
|
|
46
|
+
* interface/trait makes only the INTERFACE'S OWN methods a contract (those
|
|
47
|
+
* carry traitImpl/@Override markers → handled by Rule A), not the class's
|
|
48
|
+
* unrelated inherent methods. Counting it would wrongly shield, e.g., a Rust
|
|
49
|
+
* struct's genuinely-dead inherent method just because the struct also
|
|
50
|
+
* `impl Display for`s.
|
|
51
|
+
*/
|
|
52
|
+
function _classHasExternalBase(index, symbol) {
|
|
53
|
+
const classDefs = (index.symbols.get(symbol.className) || []).filter(c =>
|
|
54
|
+
c.file === symbol.file && _CLASS_KINDS.includes(c.type));
|
|
55
|
+
for (const cd of classDefs) {
|
|
56
|
+
if (!cd.extends) continue;
|
|
57
|
+
const supers = Array.isArray(cd.extends) ? cd.extends : splitParentList(cd.extends);
|
|
58
|
+
for (const raw of supers) {
|
|
59
|
+
if (_baseIsExternal(index, _bareBaseName(raw))) return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A method that may be reached through an out-of-tree base class — the deadcode
|
|
67
|
+
* analog of fix #210's external-contract methods. A zero in-project usage count
|
|
68
|
+
* is NOT evidence of deadness here; claiming the symbol dead invites deleting a
|
|
69
|
+
* live framework override (e.g. FastAPI.build_middleware_stack overriding
|
|
70
|
+
* Starlette, or GenerateJsonSchema.bytes_schema name-dispatched by Pydantic —
|
|
71
|
+
* the only caller lives in an unindexed dependency).
|
|
72
|
+
* (A) explicit override marker (@Override / `override` / typing.@override /
|
|
73
|
+
* Rust `impl Trait for X`) AND a single project-wide method owner of the
|
|
74
|
+
* name (no in-project supertype defines it → the contract is external —
|
|
75
|
+
* the #210 ownerCount===1 rule).
|
|
76
|
+
* (B) a public-by-shape method whose class EXTENDS an unresolved base.
|
|
77
|
+
* Private/underscore members are never external-contract surface, so a
|
|
78
|
+
* genuinely-dead one stays claimable (the fix #211 shape predicate).
|
|
79
|
+
* Data-driven, not language-keyed: classes without an `extends` clause (Go
|
|
80
|
+
* embedding, Rust structs / inherent impls) never trip (B), and Rust trait
|
|
81
|
+
* impls trip (A) via traitImpl — so new languages inherit correct behavior.
|
|
82
|
+
*/
|
|
83
|
+
function overridesOutOfTreeBase(index, symbol) {
|
|
84
|
+
if (!symbol.className) return false; // standalone function can't override
|
|
85
|
+
if (isOverrideMarked(symbol)) {
|
|
86
|
+
const owners = (index.symbols.get(symbol.name) || []).filter(s => s.className);
|
|
87
|
+
if (owners.length <= 1) return true;
|
|
88
|
+
}
|
|
89
|
+
const mods = symbol.modifiers || [];
|
|
90
|
+
const publicByShape = !mods.includes('private') &&
|
|
91
|
+
!symbol.name.startsWith('#') && !symbol.name.startsWith('_');
|
|
92
|
+
if (publicByShape && _classHasExternalBase(index, symbol)) return true;
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
11
95
|
|
|
12
96
|
/** Check if a position in a line is inside a string literal (quotes/backticks) */
|
|
13
97
|
function isInsideString(line, pos) {
|
|
@@ -228,6 +312,7 @@ function deadcode(index, options = {}) {
|
|
|
228
312
|
const results = [];
|
|
229
313
|
let excludedDecorated = 0;
|
|
230
314
|
let excludedExported = 0;
|
|
315
|
+
let excludedExternalContract = 0;
|
|
231
316
|
|
|
232
317
|
// Ensure callee index is built (lazy, reused across operations)
|
|
233
318
|
if (!index.calleeIndex) {
|
|
@@ -488,6 +573,16 @@ function deadcode(index, options = {}) {
|
|
|
488
573
|
const totalUsages = nonDefUsages.length;
|
|
489
574
|
|
|
490
575
|
if (totalUsages === 0) {
|
|
576
|
+
// External-contract override: the method may be invoked through
|
|
577
|
+
// an out-of-tree base class UCN can't index (deadcode analog of
|
|
578
|
+
// fix #210). A zero usage count is not evidence of deadness.
|
|
579
|
+
// Hidden by default; revealed under --include-exported, since
|
|
580
|
+
// it is external-reachable surface, not internal dead code.
|
|
581
|
+
const isExternalContract = overridesOutOfTreeBase(index, symbol);
|
|
582
|
+
if (isExternalContract && !options.includeExported) {
|
|
583
|
+
excludedExternalContract++;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
491
586
|
// Collect decorators/annotations for hint display
|
|
492
587
|
// Python: symbol.decorators (e.g., ['app.route("/path")', 'login_required'])
|
|
493
588
|
// Java/Rust/Go: symbol.modifiers may contain annotations (e.g., 'bean', 'scheduled')
|
|
@@ -530,7 +625,8 @@ function deadcode(index, options = {}) {
|
|
|
530
625
|
usageCount: 0,
|
|
531
626
|
...(decorators.length > 0 && { decorators }),
|
|
532
627
|
...(annotations.length > 0 && { annotations }),
|
|
533
|
-
...(declaredOn && { declaredOn })
|
|
628
|
+
...(declaredOn && { declaredOn }),
|
|
629
|
+
...(isExternalContract && { externalContract: true })
|
|
534
630
|
});
|
|
535
631
|
}
|
|
536
632
|
}
|
|
@@ -545,6 +641,7 @@ function deadcode(index, options = {}) {
|
|
|
545
641
|
// Attach exclusion counts as array properties (backwards-compatible)
|
|
546
642
|
results.excludedDecorated = excludedDecorated;
|
|
547
643
|
results.excludedExported = excludedExported;
|
|
644
|
+
results.excludedExternalContract = excludedExternalContract;
|
|
548
645
|
|
|
549
646
|
return results;
|
|
550
647
|
} finally { index._endOp(); }
|
package/core/execute.js
CHANGED
|
@@ -751,9 +751,10 @@ const HANDLERS = {
|
|
|
751
751
|
if (limit && limit > 0 && Array.isArray(result) && result.length > limit) {
|
|
752
752
|
note = limitNote(limit, result.length);
|
|
753
753
|
const sliced = result.slice(0, limit);
|
|
754
|
-
// Preserve custom properties (excludedExported, excludedDecorated) from deadcode()
|
|
754
|
+
// Preserve custom properties (excludedExported, excludedDecorated, excludedExternalContract) from deadcode()
|
|
755
755
|
if (result.excludedExported != null) sliced.excludedExported = result.excludedExported;
|
|
756
756
|
if (result.excludedDecorated != null) sliced.excludedDecorated = result.excludedDecorated;
|
|
757
|
+
if (result.excludedExternalContract != null) sliced.excludedExternalContract = result.excludedExternalContract;
|
|
757
758
|
result = sliced;
|
|
758
759
|
}
|
|
759
760
|
const tNote = truncationNote(index);
|
package/core/graph-build.js
CHANGED
|
@@ -263,4 +263,4 @@ function splitParentList(clause) {
|
|
|
263
263
|
.filter(Boolean);
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
module.exports = { buildDirIndex, buildImportGraph, buildInheritanceGraph, _resolveJavaPackageImport };
|
|
266
|
+
module.exports = { buildDirIndex, buildImportGraph, buildInheritanceGraph, splitParentList, _resolveJavaPackageImport };
|
package/core/output/reporting.js
CHANGED
|
@@ -194,7 +194,7 @@ function formatStatsJson(stats) {
|
|
|
194
194
|
* @param {string} [options.exportedHint] - Hint about exported symbols exclusion
|
|
195
195
|
*/
|
|
196
196
|
function formatDeadcode(results, options = {}) {
|
|
197
|
-
if (results.length === 0 && !results.excludedDecorated && !results.excludedExported) {
|
|
197
|
+
if (results.length === 0 && !results.excludedDecorated && !results.excludedExported && !results.excludedExternalContract) {
|
|
198
198
|
return 'No dead code found.';
|
|
199
199
|
}
|
|
200
200
|
|
|
@@ -232,7 +232,11 @@ function formatDeadcode(results, options = {}) {
|
|
|
232
232
|
const declStr = item.declaredOn
|
|
233
233
|
? ` [declared on ${item.declaredOn.kind} ${item.declaredOn.name} — contract surface, not executable code]`
|
|
234
234
|
: '';
|
|
235
|
-
|
|
235
|
+
// Revealed under --include-exported: mark as external-reachable, not dead.
|
|
236
|
+
const extStr = item.externalContract
|
|
237
|
+
? ' [reachable via out-of-tree base — external contract, not dead]'
|
|
238
|
+
: '';
|
|
239
|
+
lines.push(` ${lineRange(item.startLine, item.endLine)} ${item.name} (${item.type})${exported}${hintStr}${declStr}${extStr}`);
|
|
236
240
|
}
|
|
237
241
|
|
|
238
242
|
if (hidden > 0) {
|
|
@@ -251,6 +255,10 @@ function formatDeadcode(results, options = {}) {
|
|
|
251
255
|
const exportedHint = options.exportedHint || `${results.excludedExported} exported symbol(s) excluded (all have callers). Use --include-exported to audit them.`;
|
|
252
256
|
lines.push(`\n${exportedHint}`);
|
|
253
257
|
}
|
|
258
|
+
if (results.excludedExternalContract > 0) {
|
|
259
|
+
const extHint = options.externalContractHint || `${results.excludedExternalContract} symbol(s) hidden (override an out-of-tree base class — reachable via external contract, not dead). Use --include-exported to include them.`;
|
|
260
|
+
lines.push(`\n${extHint}`);
|
|
261
|
+
}
|
|
254
262
|
|
|
255
263
|
if (lines.length === 0) {
|
|
256
264
|
return 'No dead code found.';
|
|
@@ -270,6 +278,7 @@ function formatDeadcodeJson(results) {
|
|
|
270
278
|
count: results.length,
|
|
271
279
|
...(results.excludedExported > 0 && { excludedExported: results.excludedExported }),
|
|
272
280
|
...(results.excludedDecorated > 0 && { excludedDecorated: results.excludedDecorated }),
|
|
281
|
+
...(results.excludedExternalContract > 0 && { excludedExternalContract: results.excludedExternalContract }),
|
|
273
282
|
symbols: results.map(item => {
|
|
274
283
|
const handleSym = { ...item, relativePath: item.relativePath || item.file };
|
|
275
284
|
const handle = formatSymbolHandle(handleSym);
|
|
@@ -283,7 +292,8 @@ function formatDeadcodeJson(results) {
|
|
|
283
292
|
...(item.isExported && { isExported: true }),
|
|
284
293
|
...(item.decorators && item.decorators.length > 0 && { decorators: item.decorators }),
|
|
285
294
|
...(item.annotations && item.annotations.length > 0 && { annotations: item.annotations }),
|
|
286
|
-
...(item.declaredOn && { declaredOn: item.declaredOn })
|
|
295
|
+
...(item.declaredOn && { declaredOn: item.declaredOn }),
|
|
296
|
+
...(item.externalContract && { externalContract: true })
|
|
287
297
|
};
|
|
288
298
|
}),
|
|
289
299
|
},
|
package/core/shared.js
CHANGED
|
@@ -139,6 +139,26 @@ function looksLikeHandle(input) {
|
|
|
139
139
|
return /^.+:\d+(?::.+)?$/.test(input);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Explicit override marker on a method definition (fix #210). Marker fields are
|
|
144
|
+
* language-disjoint and compiler-checked syntax (never inferred): traitImpl is
|
|
145
|
+
* Rust's `impl Trait`, an 'override' modifier is Java's lowercased @Override, an
|
|
146
|
+
* override-bearing memberType is TS's `override` keyword, and an override
|
|
147
|
+
* decorator is Python's typing.@override. Shared by the external-contract
|
|
148
|
+
* reasoning in both the caller dispatch gate and deadcode (out-of-tree override
|
|
149
|
+
* suppression) — one source of truth so a new marker is added once, not in two
|
|
150
|
+
* drifting copies.
|
|
151
|
+
*/
|
|
152
|
+
function isOverrideMarked(def) {
|
|
153
|
+
if (def.traitImpl) return true;
|
|
154
|
+
const mods = def.modifiers || [];
|
|
155
|
+
if (mods.includes('override')) return true;
|
|
156
|
+
if (def.memberType && /\boverride\b/.test(def.memberType)) return true;
|
|
157
|
+
if (def.decorators && def.decorators.some(d =>
|
|
158
|
+
String(d).replace(/\(.*$/, '').split('.').pop() === 'override')) return true;
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
142
162
|
module.exports = {
|
|
143
163
|
pickBestDefinition,
|
|
144
164
|
addTestExclusions,
|
|
@@ -148,4 +168,5 @@ module.exports = {
|
|
|
148
168
|
parseSymbolHandle,
|
|
149
169
|
looksLikeHandle,
|
|
150
170
|
isTestPath,
|
|
171
|
+
isOverrideMarked,
|
|
151
172
|
};
|
package/mcp/server.js
CHANGED
|
@@ -576,7 +576,8 @@ server.registerTool(
|
|
|
576
576
|
let dcText = output.formatDeadcode(result, {
|
|
577
577
|
top: ep.top || 0,
|
|
578
578
|
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
|
|
579
|
+
exportedHint: !ep.includeExported && result.excludedExported > 0 ? `${result.excludedExported} exported symbol(s) excluded (all have callers). Use include_exported=true to audit them.` : undefined,
|
|
580
|
+
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
|
|
580
581
|
});
|
|
581
582
|
if (dcNote) dcText += '\n\n' + dcNote;
|
|
582
583
|
return tr(dcText);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"mcpName": "io.github.mleoca/ucn",
|
|
5
5
|
"description": "Code intelligence toolkit for AI agents — extract functions, trace call chains, find callers, detect dead code without reading entire files. Works as MCP server, CLI, or agent skill. Supports JS/TS, Python, Go, Rust, Java.",
|
|
6
6
|
"main": "index.js",
|