ucn 4.0.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +31 -7
- package/README.md +89 -50
- package/cli/index.js +199 -94
- package/core/account.js +3 -1
- package/core/analysis.js +322 -304
- package/core/bridge.js +13 -8
- package/core/cache.js +109 -19
- package/core/callers.js +969 -77
- package/core/check.js +19 -8
- package/core/deadcode.js +368 -40
- package/core/discovery.js +31 -11
- package/core/entrypoints.js +149 -17
- package/core/execute.js +330 -43
- package/core/graph-build.js +61 -10
- package/core/graph.js +282 -61
- package/core/imports.js +72 -3
- package/core/output/analysis-ext.js +70 -10
- package/core/output/analysis.js +52 -33
- package/core/output/check.js +4 -1
- package/core/output/endpoints.js +8 -3
- package/core/output/extraction.js +12 -1
- package/core/output/find.js +23 -9
- package/core/output/graph.js +32 -9
- package/core/output/refactoring.js +147 -49
- package/core/output/reporting.js +104 -5
- package/core/output/search.js +30 -3
- package/core/output/shared.js +31 -4
- package/core/output/tracing.js +22 -11
- package/core/parser.js +8 -6
- package/core/project.js +167 -7
- package/core/registry.js +20 -16
- package/core/reporting.js +131 -13
- package/core/search.js +270 -55
- package/core/shared.js +240 -1
- package/core/stacktrace.js +23 -1
- package/core/tracing.js +278 -36
- package/core/verify.js +352 -349
- package/languages/go.js +29 -17
- package/languages/index.js +56 -0
- package/languages/java.js +133 -16
- package/languages/javascript.js +215 -27
- package/languages/python.js +113 -47
- package/languages/rust.js +89 -26
- package/languages/utils.js +35 -7
- package/mcp/server.js +69 -30
- package/package.json +4 -1
package/core/reporting.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
10
|
const fs = require('fs');
|
|
11
|
+
const { codeUnitCompare, CALLABLE_SYMBOL_KINDS } = require('./shared');
|
|
12
|
+
const { _declaredFieldType, _projectTopLevelNames } = require('./callers');
|
|
11
13
|
const path = require('path');
|
|
12
14
|
const { isTestFile } = require('./discovery');
|
|
13
15
|
|
|
@@ -67,9 +69,7 @@ function getStats(index, options = {}) {
|
|
|
67
69
|
const functions = [];
|
|
68
70
|
for (const [name, symbols] of index.symbols) {
|
|
69
71
|
for (const sym of symbols) {
|
|
70
|
-
if (
|
|
71
|
-
sym.type === 'constructor' || sym.type === 'public' || sym.type === 'abstract' ||
|
|
72
|
-
sym.type === 'classmethod') {
|
|
72
|
+
if (CALLABLE_SYMBOL_KINDS.has(sym.type)) {
|
|
73
73
|
const lineCount = sym.endLine - sym.startLine + 1;
|
|
74
74
|
const relativePath = sym.relativePath || (sym.file ? path.relative(index.root, sym.file) : '');
|
|
75
75
|
functions.push({
|
|
@@ -97,10 +97,7 @@ function getStats(index, options = {}) {
|
|
|
97
97
|
const top = options.top === 0
|
|
98
98
|
? 0
|
|
99
99
|
: ((options.top != null && Number(options.top) > 0) ? Number(options.top) : 10);
|
|
100
|
-
const FUNCTION_TYPES =
|
|
101
|
-
'function', 'method', 'static', 'constructor',
|
|
102
|
-
'public', 'abstract', 'classmethod'
|
|
103
|
-
]);
|
|
100
|
+
const FUNCTION_TYPES = CALLABLE_SYMBOL_KINDS;
|
|
104
101
|
|
|
105
102
|
// Ensure the calls cache is fully populated before counting.
|
|
106
103
|
// First-time stats --hot may need to parse files to extract calls;
|
|
@@ -135,6 +132,15 @@ function getStats(index, options = {}) {
|
|
|
135
132
|
// Pre-compute import-alias sets per file. Used to distinguish `mod.foo()`
|
|
136
133
|
// (resolves to top-level foo) from `obj.foo()` on a local variable.
|
|
137
134
|
const fileImportAliases = new Map(); // filePath -> Set<string> of alias names
|
|
135
|
+
const fieldHopCache = new Map(); // rootType\0field -> declared type|null
|
|
136
|
+
// Names import-bound to an EXTERNAL module, per file (fix #256,
|
|
137
|
+
// dogfood-measured: 895 node:test `describe(...)` calls in test
|
|
138
|
+
// files were attributed to a project closure named `describe` —
|
|
139
|
+
// the #215 name discipline says an externally-bound bare name
|
|
140
|
+
// cannot reach a project def, so it never counts toward the hot
|
|
141
|
+
// leaderboard). Relative modules, resolved modules, and resolver
|
|
142
|
+
// gaps (first segment names a project path) all stay countable.
|
|
143
|
+
const fileExternalNames = new Map(); // filePath -> Set<string>
|
|
138
144
|
for (const [filePath, fileEntry] of index.files) {
|
|
139
145
|
const aliases = new Set();
|
|
140
146
|
// importNames are the named imports/exports brought into this file.
|
|
@@ -147,6 +153,16 @@ function getStats(index, options = {}) {
|
|
|
147
153
|
}
|
|
148
154
|
}
|
|
149
155
|
fileImportAliases.set(filePath, aliases);
|
|
156
|
+
let ext = null;
|
|
157
|
+
for (const b of (fileEntry.importBindings || [])) {
|
|
158
|
+
const mod = String(b.module || '');
|
|
159
|
+
if (!b.name || !mod || mod.startsWith('.') || mod.startsWith('/')) continue;
|
|
160
|
+
if (fileEntry.moduleResolved && fileEntry.moduleResolved[mod]) continue;
|
|
161
|
+
const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
|
|
162
|
+
if (firstSeg && _projectTopLevelNames(index).has(firstSeg)) continue;
|
|
163
|
+
(ext || (ext = new Set())).add(b.name);
|
|
164
|
+
}
|
|
165
|
+
if (ext) fileExternalNames.set(filePath, ext);
|
|
150
166
|
}
|
|
151
167
|
|
|
152
168
|
for (const [filePath, entry] of index.callsCache) {
|
|
@@ -164,6 +180,9 @@ function getStats(index, options = {}) {
|
|
|
164
180
|
// Bare-name call: foo() or pkg.Foo() (Go package call has receiver
|
|
165
181
|
// but isMethod:false — keep counting under bareName since they
|
|
166
182
|
// resolve like top-level functions in their package).
|
|
183
|
+
// Externally-bound names are the external library's calls,
|
|
184
|
+
// never a project def's (fix #256).
|
|
185
|
+
if (fileExternalNames.get(filePath)?.has(c.name)) continue;
|
|
167
186
|
bareNameCounts.set(c.name, (bareNameCounts.get(c.name) || 0) + 1);
|
|
168
187
|
} else if (isSelfMethod) {
|
|
169
188
|
// self/this.foo() — attributed to the enclosing class's foo
|
|
@@ -178,11 +197,26 @@ function getStats(index, options = {}) {
|
|
|
178
197
|
importedReceiverCounts.set(c.name,
|
|
179
198
|
(importedReceiverCounts.get(c.name) || 0) + 1);
|
|
180
199
|
}
|
|
181
|
-
|
|
182
|
-
|
|
200
|
+
// Field-access receivers (fix #251): `tm.service.Save()`
|
|
201
|
+
// carries receiverRootType, not receiverType — the same
|
|
202
|
+
// #202/#231 declared-field hop the caller/callee engine
|
|
203
|
+
// uses. Without it, edges `context` confirms were
|
|
204
|
+
// invisible to the hot leaderboard.
|
|
205
|
+
let recvType = c.receiverType;
|
|
206
|
+
if (!recvType && c.receiverField && c.receiverRootType) {
|
|
207
|
+
const hopKey = `${c.receiverRootType}\u0000${c.receiverField}`;
|
|
208
|
+
if (!fieldHopCache.has(hopKey)) {
|
|
209
|
+
const lang = index.files.get(filePath)?.language;
|
|
210
|
+
fieldHopCache.set(hopKey,
|
|
211
|
+
lang ? _declaredFieldType(index, c.receiverRootType, c.receiverField, lang) : null);
|
|
212
|
+
}
|
|
213
|
+
recvType = fieldHopCache.get(hopKey);
|
|
214
|
+
}
|
|
215
|
+
if (recvType) {
|
|
216
|
+
let inner = methodByReceiverType.get(recvType);
|
|
183
217
|
if (!inner) {
|
|
184
218
|
inner = new Map();
|
|
185
|
-
methodByReceiverType.set(
|
|
219
|
+
methodByReceiverType.set(recvType, inner);
|
|
186
220
|
}
|
|
187
221
|
inner.set(c.name, (inner.get(c.name) || 0) + 1);
|
|
188
222
|
}
|
|
@@ -304,7 +338,7 @@ function getStats(index, options = {}) {
|
|
|
304
338
|
if (approximate) usedHeuristicSplit = true;
|
|
305
339
|
// Sort locations by (file, startLine) for stable display.
|
|
306
340
|
locations.sort((a, b) =>
|
|
307
|
-
a.file
|
|
341
|
+
codeUnitCompare(a.file, b.file) ||
|
|
308
342
|
(a.startLine || 0) - (b.startLine || 0)
|
|
309
343
|
);
|
|
310
344
|
const primary = locations[0];
|
|
@@ -329,7 +363,7 @@ function getStats(index, options = {}) {
|
|
|
329
363
|
// Stable order: callCount desc, then (relativePath, startLine) asc.
|
|
330
364
|
hotList.sort((a, b) =>
|
|
331
365
|
(b.callCount - a.callCount) ||
|
|
332
|
-
a.file
|
|
366
|
+
codeUnitCompare(a.file, b.file) ||
|
|
333
367
|
(a.startLine || 0) - (b.startLine || 0)
|
|
334
368
|
);
|
|
335
369
|
|
|
@@ -736,4 +770,88 @@ function computeCoverageSample(index, { sampleSize, inFilter, matchInFilter }) {
|
|
|
736
770
|
return buckets;
|
|
737
771
|
}
|
|
738
772
|
|
|
739
|
-
|
|
773
|
+
/**
|
|
774
|
+
* orient — one-call cold-repo orientation: size + language mix, densest
|
|
775
|
+
* directories, most-called functions, entry-point counts, and the doctor
|
|
776
|
+
* trust verdict. Composes existing engine reads; counts and pointers only
|
|
777
|
+
* (no caller claims, so no account — the toc/stats category).
|
|
778
|
+
*/
|
|
779
|
+
function orient(index, options = {}) {
|
|
780
|
+
const top = options.top || 8;
|
|
781
|
+
// Fetch a deeper hot list so production functions survive the filter
|
|
782
|
+
// below even when test helpers dominate raw call counts.
|
|
783
|
+
const stats = getStats(index, { hot: true, top: Math.min(top * 5, 200) });
|
|
784
|
+
const health = doctor(index, {});
|
|
785
|
+
|
|
786
|
+
// Densest directories (leaf dirname rollup — "where does the code live")
|
|
787
|
+
const dirMap = new Map();
|
|
788
|
+
for (const [, fe] of index.files) {
|
|
789
|
+
const rp = fe.relativePath;
|
|
790
|
+
if (!rp) continue;
|
|
791
|
+
const slash = rp.lastIndexOf('/');
|
|
792
|
+
const dir = slash === -1 ? '.' : rp.slice(0, slash);
|
|
793
|
+
const e = dirMap.get(dir) || { dir, files: 0, symbols: 0 };
|
|
794
|
+
e.files += 1;
|
|
795
|
+
e.symbols += (fe.symbols || []).length;
|
|
796
|
+
dirMap.set(dir, e);
|
|
797
|
+
}
|
|
798
|
+
const dirs = [...dirMap.values()]
|
|
799
|
+
.sort((a, b) => b.symbols - a.symbols || codeUnitCompare(a.dir, b.dir))
|
|
800
|
+
.slice(0, top);
|
|
801
|
+
|
|
802
|
+
// Entry-point counts by type — orientation must not fail on detection
|
|
803
|
+
let entrypoints = null;
|
|
804
|
+
try {
|
|
805
|
+
const { detectEntrypoints } = require('./entrypoints');
|
|
806
|
+
const eps = detectEntrypoints(index, {});
|
|
807
|
+
if (Array.isArray(eps)) {
|
|
808
|
+
const byType = new Map();
|
|
809
|
+
for (const e of eps) byType.set(e.type, (byType.get(e.type) || 0) + 1);
|
|
810
|
+
entrypoints = {
|
|
811
|
+
total: eps.length,
|
|
812
|
+
byType: [...byType.entries()]
|
|
813
|
+
.map(([type, count]) => ({ type, count }))
|
|
814
|
+
.sort((a, b) => b.count - a.count || codeUnitCompare(a.type, b.type)),
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
} catch { /* detection error → entrypoints stays null, rendered as unavailable */ }
|
|
818
|
+
|
|
819
|
+
// Orientation wants the ENGINE's hot functions, not fixture helpers —
|
|
820
|
+
// prefer production-path entries (labeled as such by the formatter);
|
|
821
|
+
// an all-test project falls back to the raw ranking.
|
|
822
|
+
const { isTestPath } = require('./shared');
|
|
823
|
+
const allHot = (stats.hot?.items || []).map(i => ({
|
|
824
|
+
name: i.name,
|
|
825
|
+
file: i.file,
|
|
826
|
+
line: i.startLine,
|
|
827
|
+
callCount: i.callCount,
|
|
828
|
+
...(i.className ? { className: i.className } : {}),
|
|
829
|
+
}));
|
|
830
|
+
const prodHot = allHot.filter(i => i.file && !isTestPath(i.file));
|
|
831
|
+
const production = prodHot.length > 0;
|
|
832
|
+
const hotItems = (production ? prodHot : allHot).slice(0, top);
|
|
833
|
+
const hottestProd = hotItems[0] || null;
|
|
834
|
+
|
|
835
|
+
return {
|
|
836
|
+
root: stats.root,
|
|
837
|
+
files: stats.files,
|
|
838
|
+
symbols: stats.symbols,
|
|
839
|
+
buildTime: stats.buildTime,
|
|
840
|
+
byLanguage: stats.byLanguage,
|
|
841
|
+
dirs,
|
|
842
|
+
hot: { total: stats.hot?.total ?? 0, top, production, items: hotItems },
|
|
843
|
+
entrypoints,
|
|
844
|
+
trust: {
|
|
845
|
+
level: health.trust,
|
|
846
|
+
blindSpots: {
|
|
847
|
+
dynamicImports: health.blindSpots?.dynamicImports?.count ?? 0,
|
|
848
|
+
evalCalls: health.blindSpots?.evalCalls?.count ?? 0,
|
|
849
|
+
reflection: health.blindSpots?.reflection?.count ?? 0,
|
|
850
|
+
parseFailures: health.blindSpots?.parseFailures?.count ?? 0,
|
|
851
|
+
},
|
|
852
|
+
},
|
|
853
|
+
suggest: hottestProd ? hottestProd.name : null,
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
module.exports = { getStats, getToc, doctor, orient };
|