ucn 4.0.0 → 4.0.2
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/analysis.js +13 -13
- 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/analysis.js +15 -0
- package/core/output/doctor.js +13 -3
- package/core/output/reporting.js +13 -3
- package/core/reporting.js +89 -71
- package/core/shared.js +66 -0
- package/mcp/server.js +5 -2
- 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/analysis.js
CHANGED
|
@@ -12,7 +12,7 @@ const path = require('path');
|
|
|
12
12
|
const { execFileSync } = require('child_process');
|
|
13
13
|
const { parse } = require('./parser');
|
|
14
14
|
const { detectLanguage, langTraits } = require('../languages');
|
|
15
|
-
const { NON_CALLABLE_TYPES, addTestExclusions } = require('./shared');
|
|
15
|
+
const { NON_CALLABLE_TYPES, addTestExclusions, countTextBlindspots } = require('./shared');
|
|
16
16
|
const { computeReachability, symbolKey } = require('./entrypoints');
|
|
17
17
|
const { getLanguageModule } = require('../languages');
|
|
18
18
|
|
|
@@ -600,20 +600,20 @@ function detectCompleteness(index) {
|
|
|
600
600
|
const content = index._readFile(filePath);
|
|
601
601
|
|
|
602
602
|
if (langTraits(fileEntry.language)?.hasDynamicImports) {
|
|
603
|
-
// Dynamic imports:
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
//
|
|
609
|
-
|
|
610
|
-
evalUsage += (content.match(/new\s+Function\s*\(/g) || []).length;
|
|
603
|
+
// Dynamic imports: use the parser's structural count — the SAME
|
|
604
|
+
// source `doctor` uses — instead of a text regex. The old
|
|
605
|
+
// /import\s*\(/ matched Python grouped imports `from x import
|
|
606
|
+
// (...)`, flashing a false "N dynamic imports" incompleteness
|
|
607
|
+
// warning on essentially every Python project (field-report #2,
|
|
608
|
+
// reviewer-confirmed: doctor and about now agree on one count).
|
|
609
|
+
dynamicImports += fileEntry.dynamicImports || 0;
|
|
611
610
|
}
|
|
612
611
|
|
|
613
|
-
//
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
612
|
+
// eval/exec and reflection: the SAME shared counter doctor uses, so
|
|
613
|
+
// the about footer and the trust report never diverge (field-report #2).
|
|
614
|
+
const bs = countTextBlindspots(content, fileEntry.language);
|
|
615
|
+
evalUsage += bs.eval;
|
|
616
|
+
reflectionUsage += bs.reflection;
|
|
617
617
|
} catch (e) {
|
|
618
618
|
// Skip unreadable files
|
|
619
619
|
}
|
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/analysis.js
CHANGED
|
@@ -732,6 +732,21 @@ function formatAbout(about, options = {}) {
|
|
|
732
732
|
for (const c of testTop) renderAboutCaller(c);
|
|
733
733
|
}
|
|
734
734
|
if (aboutCallerReach.note) lines.push(aboutCallerReach.note);
|
|
735
|
+
|
|
736
|
+
// Field-report #5: when every CONFIRMED caller is a test and the
|
|
737
|
+
// production call sites are method-style (landed in UNVERIFIED as
|
|
738
|
+
// method-ambiguous — e.g. a module function sharing a name with a
|
|
739
|
+
// method), the bare "0 prod" count reads like dead code. Flag it so the
|
|
740
|
+
// empty prod count isn't misread; the real calls are listed below.
|
|
741
|
+
if (prodTop.length === 0 && testTop.length > 0) {
|
|
742
|
+
const uv = about.callers.unverified;
|
|
743
|
+
const methodStyle = uv && uv.top
|
|
744
|
+
? uv.top.some(u => u.reason === 'method-ambiguous' || u.reason === 'possible-dispatch')
|
|
745
|
+
: false;
|
|
746
|
+
if (uv && uv.total > 0 && methodStyle) {
|
|
747
|
+
lines.push(` Note: 0 production callers CONFIRMED — the ${uv.total} call site(s) under UNVERIFIED below include method-style calls that may bind to this or a same-name method, so this is not dead code.`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
735
750
|
}
|
|
736
751
|
|
|
737
752
|
// Callers — UNVERIFIED tier (always visible; the contract forbids hiding)
|
package/core/output/doctor.js
CHANGED
|
@@ -13,6 +13,7 @@ function formatDoctor(result) {
|
|
|
13
13
|
const lines = [];
|
|
14
14
|
lines.push(`UCN Trust Report — ${result.root}`);
|
|
15
15
|
lines.push('═'.repeat(60));
|
|
16
|
+
if (result.version) lines.push(`Version: ucn ${result.version}`);
|
|
16
17
|
lines.push(`Index: ${result.files.scanned} file${result.files.scanned === 1 ? '' : 's'}, ${result.symbols} symbol${result.symbols === 1 ? '' : 's'}`);
|
|
17
18
|
|
|
18
19
|
if (result.filter) lines.push(`Filter: ${result.filter}`);
|
|
@@ -60,13 +61,22 @@ function formatDoctor(result) {
|
|
|
60
61
|
['Reflection', bs.reflection],
|
|
61
62
|
['Parse failures', bs.parseFailures],
|
|
62
63
|
];
|
|
64
|
+
const unitFor = { 'Dynamic imports': 'import', 'Eval/exec calls': 'use', 'Reflection': 'use', 'Parse failures': 'failure' };
|
|
63
65
|
let anyBlindSpot = false;
|
|
64
66
|
for (const [label, info] of bsLines) {
|
|
65
67
|
if (info && info.count > 0) {
|
|
66
68
|
anyBlindSpot = true;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
// fileCount is the TRUE (uncapped) number of files; info.files is a
|
|
70
|
+
// capped display sample. Show "N use(s) in M file(s)" and, when the
|
|
71
|
+
// sample is truncated, "... and K more file(s)" against the true M —
|
|
72
|
+
// never present the display cap as the population (field-report #2).
|
|
73
|
+
const fileCount = info.fileCount != null ? info.fileCount : info.files.length;
|
|
74
|
+
const unit = unitFor[label] || 'use';
|
|
75
|
+
lines.push(` ${label}: ${info.count} ${unit}${info.count === 1 ? '' : 's'} in ${fileCount} file${fileCount === 1 ? '' : 's'}`);
|
|
76
|
+
const shownFiles = info.files.slice(0, 3);
|
|
77
|
+
const sample = shownFiles.map(f => ` - ${f}`).join('\n');
|
|
78
|
+
const moreFiles = fileCount - shownFiles.length;
|
|
79
|
+
const more = moreFiles > 0 ? `\n ... and ${moreFiles} more file${moreFiles === 1 ? '' : 's'}` : '';
|
|
70
80
|
if (sample) lines.push(sample + more);
|
|
71
81
|
}
|
|
72
82
|
}
|
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/reporting.js
CHANGED
|
@@ -538,28 +538,23 @@ function doctor(index, options = {}) {
|
|
|
538
538
|
const fileCounts = { total: 0, scanned: 0 };
|
|
539
539
|
const langs = {};
|
|
540
540
|
let totalSymbols = 0; // counted post-filter for accuracy when --in is set
|
|
541
|
+
// Each category tracks: count = total OCCURRENCES (uses), fileCount = TRUE
|
|
542
|
+
// number of files affected (uncapped), files = a capped sample for display.
|
|
543
|
+
// Keeping count and fileCount distinct is what lets the formatter say
|
|
544
|
+
// "481 uses in 121 files" instead of mislabeling a file count as uses or
|
|
545
|
+
// presenting the 10-file display cap as the population (field-report #2).
|
|
546
|
+
const BLINDSPOT_FILE_CAP = 10;
|
|
541
547
|
const blindSpots = {
|
|
542
|
-
dynamicImports: { count: 0, files: [] },
|
|
543
|
-
evalCalls: { count: 0, files: [] },
|
|
544
|
-
reflection: { count: 0, files: [] },
|
|
545
|
-
parseFailures: { count: 0, files: [] },
|
|
548
|
+
dynamicImports: { count: 0, fileCount: 0, files: [] },
|
|
549
|
+
evalCalls: { count: 0, fileCount: 0, files: [] },
|
|
550
|
+
reflection: { count: 0, fileCount: 0, files: [] },
|
|
551
|
+
parseFailures: { count: 0, fileCount: 0, files: [] },
|
|
546
552
|
};
|
|
547
553
|
|
|
548
|
-
// Reflection signals
|
|
549
|
-
//
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
javascript: /\bnew Function\s*\(|\bReflect\.\w+\s*\(/,
|
|
553
|
-
typescript: /\bnew Function\s*\(|\bReflect\.\w+\s*\(/,
|
|
554
|
-
go: /"reflect"|reflect\.\w+\s*\(/,
|
|
555
|
-
java: /\.getDeclaredMethod\b|\.getMethod\b|\.getDeclaredField\b|Class\.forName\b/,
|
|
556
|
-
rust: /\bAny::downcast/,
|
|
557
|
-
};
|
|
558
|
-
const EVAL_PATTERNS = {
|
|
559
|
-
python: /\b(eval|exec)\s*\(/,
|
|
560
|
-
javascript: /\beval\s*\(/,
|
|
561
|
-
typescript: /\beval\s*\(/,
|
|
562
|
-
};
|
|
554
|
+
// Reflection/eval signals come from the shared text-blind-spot counter
|
|
555
|
+
// (core/shared.js) — the SAME routine detectCompleteness uses for the about
|
|
556
|
+
// footer, so the two never drift (field-report #2). Occurrence counts.
|
|
557
|
+
const { hasTextBlindspots, countTextBlindspots } = require('./shared');
|
|
563
558
|
|
|
564
559
|
for (const [filePath, fe] of index.files) {
|
|
565
560
|
fileCounts.total++;
|
|
@@ -574,29 +569,22 @@ function doctor(index, options = {}) {
|
|
|
574
569
|
langs[lang].lines += fe.lines || 0;
|
|
575
570
|
totalSymbols += (fe.symbols || []).length;
|
|
576
571
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
572
|
+
const recordBlind = (cat, occurrences) => {
|
|
573
|
+
if (occurrences <= 0) return;
|
|
574
|
+
cat.count += occurrences;
|
|
575
|
+
cat.fileCount++;
|
|
576
|
+
if (cat.files.length < BLINDSPOT_FILE_CAP) cat.files.push(rel);
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
if (fe.dynamicImports && fe.dynamicImports > 0) recordBlind(blindSpots.dynamicImports, fe.dynamicImports);
|
|
580
|
+
if (fe.parseError) recordBlind(blindSpots.parseFailures, 1);
|
|
585
581
|
|
|
586
|
-
// Read file once for eval/reflection signals
|
|
587
|
-
|
|
588
|
-
const reflRe = REFLECTION_PATTERNS[lang];
|
|
589
|
-
if (evalRe || reflRe) {
|
|
582
|
+
// Read file once for eval/reflection signals (shared counter).
|
|
583
|
+
if (hasTextBlindspots(lang)) {
|
|
590
584
|
try {
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (blindSpots.evalCalls.files.length < 10) blindSpots.evalCalls.files.push(rel);
|
|
595
|
-
}
|
|
596
|
-
if (reflRe && reflRe.test(content)) {
|
|
597
|
-
blindSpots.reflection.count++;
|
|
598
|
-
if (blindSpots.reflection.files.length < 10) blindSpots.reflection.files.push(rel);
|
|
599
|
-
}
|
|
585
|
+
const bs = countTextBlindspots(fs.readFileSync(filePath, 'utf-8'), lang);
|
|
586
|
+
recordBlind(blindSpots.evalCalls, bs.eval);
|
|
587
|
+
recordBlind(blindSpots.reflection, bs.reflection);
|
|
600
588
|
} catch (e) { /* ignore read errors */ }
|
|
601
589
|
}
|
|
602
590
|
}
|
|
@@ -620,57 +608,87 @@ function doctor(index, options = {}) {
|
|
|
620
608
|
|
|
621
609
|
// Compute trust verdict.
|
|
622
610
|
//
|
|
623
|
-
// 1
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
//
|
|
611
|
+
// Field-report #1: the old logic dropped the tier by one PER blind-spot
|
|
612
|
+
// category present, so any non-trivial Python/TS project (all of which have
|
|
613
|
+
// some getattr/eval/dynamic import) was forced to LOW even when --deep
|
|
614
|
+
// measured ~99% of edges at confidence ≥ 0.5 — a self-contradicting verdict
|
|
615
|
+
// ("99.1% ... LOW") that trains agents to distrust a healthy index. The fix:
|
|
616
|
+
// - When --deep coverage exists it drives the tier. Coverage measures the
|
|
617
|
+
// CONFIDENCE of edges UCN FOUND, NOT completeness — a reflection-hidden
|
|
618
|
+
// edge is absent from the sample, never a low-confidence edge dragging
|
|
619
|
+
// the % down — so sparse blind spots are a CAVEAT, while PERVASIVE ones
|
|
620
|
+
// (a large share of files) can hide edges the sample can't see and cap
|
|
621
|
+
// the verdict at MEDIUM (density, not mere presence; see below).
|
|
622
|
+
// - Parse failures are a separate exception: a file UCN couldn't parse is
|
|
623
|
+
// not in the sample at all, a genuine uncounted hole → cap at MEDIUM.
|
|
624
|
+
// - Without --deep there is no measurement, so blind spots are the only
|
|
625
|
+
// signal — but bounded to ONE tier total (not one per category), so a
|
|
626
|
+
// handful of getattr doesn't read as untrustworthy.
|
|
629
627
|
let trust = 'UNKNOWN';
|
|
630
628
|
let trustReason = '';
|
|
631
|
-
const
|
|
629
|
+
const tier = ['HIGH', 'MEDIUM', 'LOW'];
|
|
630
|
+
|
|
631
|
+
const blindSignals = [];
|
|
632
|
+
if (blindSpots.parseFailures.count > 0) blindSignals.push(`${blindSpots.parseFailures.count} parse failure(s)`);
|
|
633
|
+
if (blindSpots.evalCalls.count > 0) blindSignals.push(`${blindSpots.evalCalls.count} eval/exec use(s) in ${blindSpots.evalCalls.fileCount} file(s)`);
|
|
634
|
+
if (blindSpots.reflection.count > 0) blindSignals.push(`${blindSpots.reflection.count} reflection use(s) in ${blindSpots.reflection.fileCount} file(s)`);
|
|
635
|
+
if (blindSpots.dynamicImports.count > 0) blindSignals.push(`${blindSpots.dynamicImports.count} dynamic import(s) in ${blindSpots.dynamicImports.fileCount} file(s)`);
|
|
632
636
|
|
|
633
637
|
if (coverage && coverage.total > 0) {
|
|
634
638
|
const safe = coverage.high + coverage.medium;
|
|
635
639
|
const safePct = safe / coverage.total;
|
|
636
|
-
let
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
//
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
640
|
+
let idx = safePct >= 0.85 ? 0 : safePct >= 0.6 ? 1 : 2;
|
|
641
|
+
// Parse failures: unparsed files aren't in the sample at all.
|
|
642
|
+
if (blindSpots.parseFailures.count > 0) idx = Math.max(idx, 1);
|
|
643
|
+
// Coverage measures the CONFIDENCE of edges UCN found, NOT completeness:
|
|
644
|
+
// a call hidden behind reflection/dynamic dispatch is simply absent from
|
|
645
|
+
// findCallers' result, never a low-confidence edge that drags the % down.
|
|
646
|
+
// So when blind spots are PERVASIVE — affecting a large share of files —
|
|
647
|
+
// they can hide a real fraction of the call graph that the sample can't
|
|
648
|
+
// see, and the verdict is capped at MEDIUM. Density, not mere presence:
|
|
649
|
+
// a handful of getattr stays a caveat (the old code dropped a tier per
|
|
650
|
+
// category, forcing every project to LOW); reflection across half the
|
|
651
|
+
// files does cap. Gated on a file-count floor — file share is meaningless
|
|
652
|
+
// for a handful of files, so small projects ride on coverage alone.
|
|
653
|
+
const scanned = fileCounts.scanned || 1;
|
|
654
|
+
const share = (fc) => fc / scanned;
|
|
655
|
+
const pervasiveBlindSpot = scanned >= 10 && (
|
|
656
|
+
share(blindSpots.reflection.fileCount) >= 0.5 ||
|
|
657
|
+
share(blindSpots.dynamicImports.fileCount) >= 0.4 ||
|
|
658
|
+
share(blindSpots.evalCalls.fileCount) >= 0.15
|
|
659
|
+
);
|
|
660
|
+
const baseIdx = idx;
|
|
661
|
+
if (pervasiveBlindSpot) idx = Math.max(idx, 1);
|
|
662
|
+
const capped = idx > baseIdx;
|
|
650
663
|
trust = tier[idx];
|
|
651
|
-
|
|
664
|
+
const reasons = [`${(safePct * 100).toFixed(1)}% of found edges have confidence ≥ 0.5`];
|
|
665
|
+
if (blindSignals.length) {
|
|
666
|
+
reasons.push(capped
|
|
667
|
+
? `capped at MEDIUM — pervasive blind spots may hide edges the sample can't see: ${blindSignals.join(', ')}`
|
|
668
|
+
: `blind spots (caveat — coverage measures found edges, not completeness): ${blindSignals.join(', ')}`);
|
|
669
|
+
}
|
|
652
670
|
trustReason = reasons.join('; ');
|
|
653
671
|
} else if (coverage) {
|
|
654
672
|
// Sampled but zero edges — can't say anything about confidence.
|
|
655
673
|
trust = 'UNKNOWN';
|
|
656
674
|
trustReason = 'no edges sampled (empty scope or filter matched nothing)';
|
|
657
675
|
} else if (fileCounts.scanned > 0) {
|
|
658
|
-
// Cheap path (no --deep):
|
|
659
|
-
|
|
676
|
+
// Cheap path (no --deep): no measurement, so blind spots are the only
|
|
677
|
+
// signal — bounded to one tier total. Run --deep for a measured verdict.
|
|
660
678
|
let idx = 0;
|
|
661
|
-
|
|
662
|
-
if (blindSpots.
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
if (blindSpots.dynamicImports.count > 0) { idx = Math.min(2, idx + 1); blindSignals.push(`${blindSpots.dynamicImports.count} dynamic import(s)`); }
|
|
679
|
+
if (blindSpots.parseFailures.count > 0) idx = Math.max(idx, 1);
|
|
680
|
+
if (blindSpots.evalCalls.count + blindSpots.reflection.count + blindSpots.dynamicImports.count > 0) {
|
|
681
|
+
idx = Math.min(2, idx + 1);
|
|
682
|
+
}
|
|
666
683
|
trust = tier[idx];
|
|
667
684
|
trustReason = blindSignals.length
|
|
668
|
-
? `coverage not deep-checked; blind spots: ${blindSignals.join(', ')}`
|
|
669
|
-
: 'no parse failures; coverage not deep-checked';
|
|
685
|
+
? `coverage not deep-checked (run --deep); blind spots: ${blindSignals.join(', ')}`
|
|
686
|
+
: 'no parse failures; coverage not deep-checked (run --deep)';
|
|
670
687
|
}
|
|
671
688
|
|
|
672
689
|
return {
|
|
673
690
|
root: index.root,
|
|
691
|
+
version: require('../package.json').version, // running ucn version — surfaces MCP/CLI drift (field-report #3)
|
|
674
692
|
files: fileCounts,
|
|
675
693
|
symbols: totalSymbols,
|
|
676
694
|
languages: langs,
|
package/core/shared.js
CHANGED
|
@@ -139,6 +139,69 @@ 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
|
+
|
|
162
|
+
// Per-language text patterns for the "blind spots" UCN's AST can't follow:
|
|
163
|
+
// eval/exec-style code execution and reflection (dynamic attribute access /
|
|
164
|
+
// dynamic dispatch). ONE source of truth so doctor's trust scan and
|
|
165
|
+
// detectCompleteness's about-footer warning count identically (field-report #2:
|
|
166
|
+
// they used to diverge — doctor 497 reflection vs footer 194, eval 3 vs 2 —
|
|
167
|
+
// because each kept its own regex set). Dynamic imports are NOT here: those are
|
|
168
|
+
// structural (fileEntry.dynamicImports), the AST-accurate count both paths share.
|
|
169
|
+
// `new Function(...)` is categorized as eval (code execution), not reflection.
|
|
170
|
+
const BLINDSPOT_TEXT_PATTERNS = {
|
|
171
|
+
reflection: {
|
|
172
|
+
python: /\b(getattr|hasattr|setattr|__import__|importlib\.import_module)\s*\(/g,
|
|
173
|
+
javascript: /\bReflect\.\w+\s*\(/g,
|
|
174
|
+
typescript: /\bReflect\.\w+\s*\(/g,
|
|
175
|
+
go: /\breflect\.\w+\s*\(/g,
|
|
176
|
+
java: /\.getDeclaredMethod\b|\.getMethod\b|\.getDeclaredField\b|Class\.forName\b/g,
|
|
177
|
+
rust: /\bAny::downcast/g,
|
|
178
|
+
},
|
|
179
|
+
eval: {
|
|
180
|
+
python: /\b(eval|exec)\s*\(/g,
|
|
181
|
+
javascript: /\beval\s*\(|\bnew\s+Function\s*\(/g,
|
|
182
|
+
typescript: /\beval\s*\(|\bnew\s+Function\s*\(/g,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/** True when a language has any text-blind-spot pattern (so callers can skip the file read otherwise). */
|
|
187
|
+
function hasTextBlindspots(language) {
|
|
188
|
+
return !!(BLINDSPOT_TEXT_PATTERNS.reflection[language] || BLINDSPOT_TEXT_PATTERNS.eval[language]);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Count text-detected blind spots (eval/exec, reflection) in one file's source.
|
|
193
|
+
* Returns { eval, reflection } OCCURRENCE counts (global match). Shared by doctor
|
|
194
|
+
* and detectCompleteness so both report the same numbers (field-report #2).
|
|
195
|
+
*/
|
|
196
|
+
function countTextBlindspots(content, language) {
|
|
197
|
+
const reRe = BLINDSPOT_TEXT_PATTERNS.reflection[language];
|
|
198
|
+
const evRe = BLINDSPOT_TEXT_PATTERNS.eval[language];
|
|
199
|
+
return {
|
|
200
|
+
eval: evRe ? (content.match(evRe) || []).length : 0,
|
|
201
|
+
reflection: reRe ? (content.match(reRe) || []).length : 0,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
142
205
|
module.exports = {
|
|
143
206
|
pickBestDefinition,
|
|
144
207
|
addTestExclusions,
|
|
@@ -148,4 +211,7 @@ module.exports = {
|
|
|
148
211
|
parseSymbolHandle,
|
|
149
212
|
looksLikeHandle,
|
|
150
213
|
isTestPath,
|
|
214
|
+
isOverrideMarked,
|
|
215
|
+
hasTextBlindspots,
|
|
216
|
+
countTextBlindspots,
|
|
151
217
|
};
|
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);
|
|
@@ -790,7 +791,9 @@ server.registerTool(
|
|
|
790
791
|
async function main() {
|
|
791
792
|
const transport = new StdioServerTransport();
|
|
792
793
|
await server.connect(transport);
|
|
793
|
-
|
|
794
|
+
// Print the running version so MCP-vs-CLI drift is visible (field-report #3:
|
|
795
|
+
// a stale `npx -y ucn` cache can silently run an older engine than the CLI).
|
|
796
|
+
console.error(`UCN MCP server v${require('../package.json').version} running on stdio`);
|
|
794
797
|
}
|
|
795
798
|
|
|
796
799
|
main().catch(e => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucn",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.2",
|
|
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",
|