ucn 4.0.1 → 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 +334 -316
- 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 +67 -33
- package/core/output/check.js +4 -1
- package/core/output/doctor.js +13 -3
- 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 +220 -84
- package/core/search.js +270 -55
- package/core/shared.js +285 -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 +72 -31
- 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
|
|
|
@@ -538,28 +572,23 @@ function doctor(index, options = {}) {
|
|
|
538
572
|
const fileCounts = { total: 0, scanned: 0 };
|
|
539
573
|
const langs = {};
|
|
540
574
|
let totalSymbols = 0; // counted post-filter for accuracy when --in is set
|
|
575
|
+
// Each category tracks: count = total OCCURRENCES (uses), fileCount = TRUE
|
|
576
|
+
// number of files affected (uncapped), files = a capped sample for display.
|
|
577
|
+
// Keeping count and fileCount distinct is what lets the formatter say
|
|
578
|
+
// "481 uses in 121 files" instead of mislabeling a file count as uses or
|
|
579
|
+
// presenting the 10-file display cap as the population (field-report #2).
|
|
580
|
+
const BLINDSPOT_FILE_CAP = 10;
|
|
541
581
|
const blindSpots = {
|
|
542
|
-
dynamicImports: { count: 0, files: [] },
|
|
543
|
-
evalCalls: { count: 0, files: [] },
|
|
544
|
-
reflection: { count: 0, files: [] },
|
|
545
|
-
parseFailures: { count: 0, files: [] },
|
|
582
|
+
dynamicImports: { count: 0, fileCount: 0, files: [] },
|
|
583
|
+
evalCalls: { count: 0, fileCount: 0, files: [] },
|
|
584
|
+
reflection: { count: 0, fileCount: 0, files: [] },
|
|
585
|
+
parseFailures: { count: 0, fileCount: 0, files: [] },
|
|
546
586
|
};
|
|
547
587
|
|
|
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
|
-
};
|
|
588
|
+
// Reflection/eval signals come from the shared text-blind-spot counter
|
|
589
|
+
// (core/shared.js) — the SAME routine detectCompleteness uses for the about
|
|
590
|
+
// footer, so the two never drift (field-report #2). Occurrence counts.
|
|
591
|
+
const { hasTextBlindspots, countTextBlindspots } = require('./shared');
|
|
563
592
|
|
|
564
593
|
for (const [filePath, fe] of index.files) {
|
|
565
594
|
fileCounts.total++;
|
|
@@ -574,29 +603,22 @@ function doctor(index, options = {}) {
|
|
|
574
603
|
langs[lang].lines += fe.lines || 0;
|
|
575
604
|
totalSymbols += (fe.symbols || []).length;
|
|
576
605
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
606
|
+
const recordBlind = (cat, occurrences) => {
|
|
607
|
+
if (occurrences <= 0) return;
|
|
608
|
+
cat.count += occurrences;
|
|
609
|
+
cat.fileCount++;
|
|
610
|
+
if (cat.files.length < BLINDSPOT_FILE_CAP) cat.files.push(rel);
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
if (fe.dynamicImports && fe.dynamicImports > 0) recordBlind(blindSpots.dynamicImports, fe.dynamicImports);
|
|
614
|
+
if (fe.parseError) recordBlind(blindSpots.parseFailures, 1);
|
|
585
615
|
|
|
586
|
-
// Read file once for eval/reflection signals
|
|
587
|
-
|
|
588
|
-
const reflRe = REFLECTION_PATTERNS[lang];
|
|
589
|
-
if (evalRe || reflRe) {
|
|
616
|
+
// Read file once for eval/reflection signals (shared counter).
|
|
617
|
+
if (hasTextBlindspots(lang)) {
|
|
590
618
|
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
|
-
}
|
|
619
|
+
const bs = countTextBlindspots(fs.readFileSync(filePath, 'utf-8'), lang);
|
|
620
|
+
recordBlind(blindSpots.evalCalls, bs.eval);
|
|
621
|
+
recordBlind(blindSpots.reflection, bs.reflection);
|
|
600
622
|
} catch (e) { /* ignore read errors */ }
|
|
601
623
|
}
|
|
602
624
|
}
|
|
@@ -620,57 +642,87 @@ function doctor(index, options = {}) {
|
|
|
620
642
|
|
|
621
643
|
// Compute trust verdict.
|
|
622
644
|
//
|
|
623
|
-
// 1
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
//
|
|
645
|
+
// Field-report #1: the old logic dropped the tier by one PER blind-spot
|
|
646
|
+
// category present, so any non-trivial Python/TS project (all of which have
|
|
647
|
+
// some getattr/eval/dynamic import) was forced to LOW even when --deep
|
|
648
|
+
// measured ~99% of edges at confidence ≥ 0.5 — a self-contradicting verdict
|
|
649
|
+
// ("99.1% ... LOW") that trains agents to distrust a healthy index. The fix:
|
|
650
|
+
// - When --deep coverage exists it drives the tier. Coverage measures the
|
|
651
|
+
// CONFIDENCE of edges UCN FOUND, NOT completeness — a reflection-hidden
|
|
652
|
+
// edge is absent from the sample, never a low-confidence edge dragging
|
|
653
|
+
// the % down — so sparse blind spots are a CAVEAT, while PERVASIVE ones
|
|
654
|
+
// (a large share of files) can hide edges the sample can't see and cap
|
|
655
|
+
// the verdict at MEDIUM (density, not mere presence; see below).
|
|
656
|
+
// - Parse failures are a separate exception: a file UCN couldn't parse is
|
|
657
|
+
// not in the sample at all, a genuine uncounted hole → cap at MEDIUM.
|
|
658
|
+
// - Without --deep there is no measurement, so blind spots are the only
|
|
659
|
+
// signal — but bounded to ONE tier total (not one per category), so a
|
|
660
|
+
// handful of getattr doesn't read as untrustworthy.
|
|
629
661
|
let trust = 'UNKNOWN';
|
|
630
662
|
let trustReason = '';
|
|
631
|
-
const
|
|
663
|
+
const tier = ['HIGH', 'MEDIUM', 'LOW'];
|
|
664
|
+
|
|
665
|
+
const blindSignals = [];
|
|
666
|
+
if (blindSpots.parseFailures.count > 0) blindSignals.push(`${blindSpots.parseFailures.count} parse failure(s)`);
|
|
667
|
+
if (blindSpots.evalCalls.count > 0) blindSignals.push(`${blindSpots.evalCalls.count} eval/exec use(s) in ${blindSpots.evalCalls.fileCount} file(s)`);
|
|
668
|
+
if (blindSpots.reflection.count > 0) blindSignals.push(`${blindSpots.reflection.count} reflection use(s) in ${blindSpots.reflection.fileCount} file(s)`);
|
|
669
|
+
if (blindSpots.dynamicImports.count > 0) blindSignals.push(`${blindSpots.dynamicImports.count} dynamic import(s) in ${blindSpots.dynamicImports.fileCount} file(s)`);
|
|
632
670
|
|
|
633
671
|
if (coverage && coverage.total > 0) {
|
|
634
672
|
const safe = coverage.high + coverage.medium;
|
|
635
673
|
const safePct = safe / coverage.total;
|
|
636
|
-
let
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
//
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
674
|
+
let idx = safePct >= 0.85 ? 0 : safePct >= 0.6 ? 1 : 2;
|
|
675
|
+
// Parse failures: unparsed files aren't in the sample at all.
|
|
676
|
+
if (blindSpots.parseFailures.count > 0) idx = Math.max(idx, 1);
|
|
677
|
+
// Coverage measures the CONFIDENCE of edges UCN found, NOT completeness:
|
|
678
|
+
// a call hidden behind reflection/dynamic dispatch is simply absent from
|
|
679
|
+
// findCallers' result, never a low-confidence edge that drags the % down.
|
|
680
|
+
// So when blind spots are PERVASIVE — affecting a large share of files —
|
|
681
|
+
// they can hide a real fraction of the call graph that the sample can't
|
|
682
|
+
// see, and the verdict is capped at MEDIUM. Density, not mere presence:
|
|
683
|
+
// a handful of getattr stays a caveat (the old code dropped a tier per
|
|
684
|
+
// category, forcing every project to LOW); reflection across half the
|
|
685
|
+
// files does cap. Gated on a file-count floor — file share is meaningless
|
|
686
|
+
// for a handful of files, so small projects ride on coverage alone.
|
|
687
|
+
const scanned = fileCounts.scanned || 1;
|
|
688
|
+
const share = (fc) => fc / scanned;
|
|
689
|
+
const pervasiveBlindSpot = scanned >= 10 && (
|
|
690
|
+
share(blindSpots.reflection.fileCount) >= 0.5 ||
|
|
691
|
+
share(blindSpots.dynamicImports.fileCount) >= 0.4 ||
|
|
692
|
+
share(blindSpots.evalCalls.fileCount) >= 0.15
|
|
693
|
+
);
|
|
694
|
+
const baseIdx = idx;
|
|
695
|
+
if (pervasiveBlindSpot) idx = Math.max(idx, 1);
|
|
696
|
+
const capped = idx > baseIdx;
|
|
650
697
|
trust = tier[idx];
|
|
651
|
-
|
|
698
|
+
const reasons = [`${(safePct * 100).toFixed(1)}% of found edges have confidence ≥ 0.5`];
|
|
699
|
+
if (blindSignals.length) {
|
|
700
|
+
reasons.push(capped
|
|
701
|
+
? `capped at MEDIUM — pervasive blind spots may hide edges the sample can't see: ${blindSignals.join(', ')}`
|
|
702
|
+
: `blind spots (caveat — coverage measures found edges, not completeness): ${blindSignals.join(', ')}`);
|
|
703
|
+
}
|
|
652
704
|
trustReason = reasons.join('; ');
|
|
653
705
|
} else if (coverage) {
|
|
654
706
|
// Sampled but zero edges — can't say anything about confidence.
|
|
655
707
|
trust = 'UNKNOWN';
|
|
656
708
|
trustReason = 'no edges sampled (empty scope or filter matched nothing)';
|
|
657
709
|
} else if (fileCounts.scanned > 0) {
|
|
658
|
-
// Cheap path (no --deep):
|
|
659
|
-
|
|
710
|
+
// Cheap path (no --deep): no measurement, so blind spots are the only
|
|
711
|
+
// signal — bounded to one tier total. Run --deep for a measured verdict.
|
|
660
712
|
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)`); }
|
|
713
|
+
if (blindSpots.parseFailures.count > 0) idx = Math.max(idx, 1);
|
|
714
|
+
if (blindSpots.evalCalls.count + blindSpots.reflection.count + blindSpots.dynamicImports.count > 0) {
|
|
715
|
+
idx = Math.min(2, idx + 1);
|
|
716
|
+
}
|
|
666
717
|
trust = tier[idx];
|
|
667
718
|
trustReason = blindSignals.length
|
|
668
|
-
? `coverage not deep-checked; blind spots: ${blindSignals.join(', ')}`
|
|
669
|
-
: 'no parse failures; coverage not deep-checked';
|
|
719
|
+
? `coverage not deep-checked (run --deep); blind spots: ${blindSignals.join(', ')}`
|
|
720
|
+
: 'no parse failures; coverage not deep-checked (run --deep)';
|
|
670
721
|
}
|
|
671
722
|
|
|
672
723
|
return {
|
|
673
724
|
root: index.root,
|
|
725
|
+
version: require('../package.json').version, // running ucn version — surfaces MCP/CLI drift (field-report #3)
|
|
674
726
|
files: fileCounts,
|
|
675
727
|
symbols: totalSymbols,
|
|
676
728
|
languages: langs,
|
|
@@ -718,4 +770,88 @@ function computeCoverageSample(index, { sampleSize, inFilter, matchInFilter }) {
|
|
|
718
770
|
return buckets;
|
|
719
771
|
}
|
|
720
772
|
|
|
721
|
-
|
|
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 };
|