ucn 4.2.3 → 5.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/.claude/skills/ucn/SKILL.md +89 -77
- package/.claude/skills/ucn/references/commands.md +62 -68
- package/.claude/skills/ucn/references/trust-contract.md +31 -6
- package/README.md +438 -305
- package/assets/demo.svg +31 -0
- package/cli/index.js +430 -1385
- package/core/account.js +144 -34
- package/core/analysis.js +182 -72
- package/core/ast-analysis.js +279 -0
- package/core/bridge.js +205 -24
- package/core/brief.js +27 -58
- package/core/build-worker.js +21 -140
- package/core/cache.js +513 -11
- package/core/callers.js +4920 -456
- package/core/check.js +13 -4
- package/core/command-contracts.js +402 -0
- package/core/compilation-database.js +276 -0
- package/core/confidence.js +4 -1
- package/core/deadcode.js +397 -19
- package/core/discovery.js +359 -46
- package/core/entrypoints.js +195 -41
- package/core/execute.js +887 -81
- package/core/graph-build.js +162 -7
- package/core/graph.js +53 -77
- package/core/imports.js +65 -6
- package/core/index-ir.js +138 -0
- package/core/ir.js +195 -0
- package/core/output/analysis.js +212 -22
- package/core/output/brief.js +23 -0
- package/core/output/check.js +4 -0
- package/core/output/doctor.js +37 -6
- package/core/output/endpoints.js +5 -2
- package/core/output/extraction.js +24 -12
- package/core/output/find.js +141 -36
- package/core/output/graph.js +11 -5
- package/core/output/public.js +462 -0
- package/core/output/refactoring.js +42 -10
- package/core/output/reporting.js +97 -20
- package/core/output/search.js +24 -16
- package/core/output/shared.js +22 -1
- package/core/output/tracing.js +30 -15
- package/core/output-budget.js +295 -0
- package/core/output.js +1 -0
- package/core/parallel-build.js +44 -11
- package/core/parser.js +3 -3
- package/core/project.js +384 -187
- package/core/public-command.js +47 -0
- package/core/registry.js +247 -117
- package/core/reporting.js +312 -290
- package/core/search.js +317 -185
- package/core/semantic-provider.js +110 -0
- package/core/stacktrace.js +25 -0
- package/core/tracing.js +101 -51
- package/core/trust-matrix.js +19 -40
- package/core/verify.js +534 -37
- package/languages/adapter.js +218 -0
- package/languages/c-family.js +2791 -0
- package/languages/c.js +3 -0
- package/languages/cpp.js +3 -0
- package/languages/csharp.js +1402 -0
- package/languages/go.js +60 -21
- package/languages/html.js +2 -2
- package/languages/index.js +85 -7
- package/languages/java.js +396 -13
- package/languages/javascript.js +199 -19
- package/languages/python.js +964 -22
- package/languages/rust.js +1317 -152
- package/languages/utils.js +40 -3
- package/mcp/server.js +254 -636
- package/package.json +39 -22
- package/eslint.config.js +0 -43
- package/jsconfig.json +0 -10
package/core/reporting.js
CHANGED
|
@@ -9,10 +9,25 @@
|
|
|
9
9
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const { codeUnitCompare, CALLABLE_SYMBOL_KINDS } = require('./shared');
|
|
12
|
-
const { _declaredFieldType, _projectTopLevelNames } = require('./callers');
|
|
13
12
|
const path = require('path');
|
|
14
13
|
const { isTestFile } = require('./discovery');
|
|
15
14
|
const { summarizeCommandTrust } = require('./trust-matrix');
|
|
15
|
+
const { projectComputedDispatch } = require('./ast-analysis');
|
|
16
|
+
const { langTraits } = require('../languages');
|
|
17
|
+
|
|
18
|
+
function matchesReportingScope(index, relativePath, options = {}) {
|
|
19
|
+
if (!relativePath) return false;
|
|
20
|
+
if (options.file && !relativePath.includes(options.file)) return false;
|
|
21
|
+
const exclude = Array.isArray(options.exclude)
|
|
22
|
+
? options.exclude
|
|
23
|
+
: (options.exclude
|
|
24
|
+
? String(options.exclude).split(',').map(s => s.trim()).filter(Boolean)
|
|
25
|
+
: []);
|
|
26
|
+
return index.matchesFilters(relativePath, {
|
|
27
|
+
...(exclude.length > 0 && { exclude }),
|
|
28
|
+
...(options.in && { in: options.in }),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
16
31
|
|
|
17
32
|
/**
|
|
18
33
|
* Get project statistics: file counts, symbol counts, LOC, language breakdown.
|
|
@@ -22,15 +37,15 @@ const { summarizeCommandTrust } = require('./trust-matrix');
|
|
|
22
37
|
* @returns {object}
|
|
23
38
|
*/
|
|
24
39
|
function getStats(index, options = {}) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
40
|
+
const scopedFiles = [...index.files].filter(([, fileEntry]) =>
|
|
41
|
+
matchesReportingScope(index, fileEntry.relativePath, options));
|
|
42
|
+
const scopedPaths = new Set(scopedFiles.map(([filePath]) => filePath));
|
|
43
|
+
const totalSymbols = scopedFiles.reduce(
|
|
44
|
+
(sum, [, fileEntry]) => sum + (fileEntry.symbols || []).length, 0);
|
|
30
45
|
|
|
31
46
|
const stats = {
|
|
32
47
|
root: index.root,
|
|
33
|
-
files:
|
|
48
|
+
files: scopedFiles.length,
|
|
34
49
|
symbols: totalSymbols, // Total symbol count, not unique names
|
|
35
50
|
buildTime: index.buildTime,
|
|
36
51
|
byLanguage: {},
|
|
@@ -38,7 +53,7 @@ function getStats(index, options = {}) {
|
|
|
38
53
|
...(index.truncated && { truncated: index.truncated })
|
|
39
54
|
};
|
|
40
55
|
|
|
41
|
-
for (const [, fileEntry] of
|
|
56
|
+
for (const [, fileEntry] of scopedFiles) {
|
|
42
57
|
const lang = fileEntry.language;
|
|
43
58
|
if (!stats.byLanguage[lang]) {
|
|
44
59
|
stats.byLanguage[lang] = { files: 0, lines: 0, symbols: 0 };
|
|
@@ -48,8 +63,8 @@ function getStats(index, options = {}) {
|
|
|
48
63
|
stats.byLanguage[lang].symbols += fileEntry.symbols.length;
|
|
49
64
|
}
|
|
50
65
|
|
|
51
|
-
for (const [,
|
|
52
|
-
for (const sym of symbols) {
|
|
66
|
+
for (const [, fileEntry] of scopedFiles) {
|
|
67
|
+
for (const sym of fileEntry.symbols || []) {
|
|
53
68
|
if (!Object.hasOwn(stats.byType, sym.type)) {
|
|
54
69
|
stats.byType[sym.type] = 0;
|
|
55
70
|
}
|
|
@@ -59,17 +74,22 @@ function getStats(index, options = {}) {
|
|
|
59
74
|
|
|
60
75
|
// Surface build warnings (parse failures, skipped files)
|
|
61
76
|
if (index.failedFiles && index.failedFiles.size > 0) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
77
|
+
const failedFiles = [...index.failedFiles]
|
|
78
|
+
.map(f => path.relative(index.root, f))
|
|
79
|
+
.filter(rel => matchesReportingScope(index, rel, options));
|
|
80
|
+
if (failedFiles.length > 0) {
|
|
81
|
+
stats.warnings = {
|
|
82
|
+
failedFiles,
|
|
83
|
+
count: failedFiles.length,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
66
86
|
}
|
|
67
87
|
|
|
68
88
|
// Per-function line counts for complexity audits
|
|
69
89
|
if (options.functions) {
|
|
70
90
|
const functions = [];
|
|
71
|
-
for (const [,
|
|
72
|
-
for (const sym of symbols) {
|
|
91
|
+
for (const [, fileEntry] of scopedFiles) {
|
|
92
|
+
for (const sym of fileEntry.symbols || []) {
|
|
73
93
|
if (CALLABLE_SYMBOL_KINDS.has(sym.type)) {
|
|
74
94
|
const lineCount = sym.endLine - sym.startLine + 1;
|
|
75
95
|
const relativePath = sym.relativePath || (sym.file ? path.relative(index.root, sym.file) : '');
|
|
@@ -107,258 +127,112 @@ function getStats(index, options = {}) {
|
|
|
107
127
|
index.buildCalleeIndex();
|
|
108
128
|
}
|
|
109
129
|
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
// importedReceiverCounts[name] — method calls whose receiver is an imported
|
|
118
|
-
// module alias in the calling file (e.g.
|
|
119
|
-
// `mod.foo()` where `mod` is a require alias).
|
|
120
|
-
// These resolve like top-level function calls.
|
|
121
|
-
//
|
|
122
|
-
// self/this/cls/super counted under bareNameCounts since they always resolve
|
|
123
|
-
// to the enclosing class's method (handled in attribution below).
|
|
124
|
-
// We dedupe per file by (name, line) so multi-record call sites count once.
|
|
125
|
-
const SELF_RECEIVERS = new Set(['self', 'this', 'cls', 'super']);
|
|
126
|
-
const bareNameCounts = new Map(); // name -> count
|
|
127
|
-
const methodByReceiverType = new Map(); // receiverType -> Map(name -> count)
|
|
128
|
-
const methodByName = new Map(); // name -> count of all method calls
|
|
129
|
-
const selfMethodByName = new Map(); // name -> count of self/this.name() calls
|
|
130
|
-
const importedReceiverCounts = new Map(); // name -> count of `mod.name()` calls
|
|
131
|
-
// where mod is an import alias
|
|
132
|
-
|
|
133
|
-
// Pre-compute import-alias sets per file. Used to distinguish `mod.foo()`
|
|
134
|
-
// (resolves to top-level foo) from `obj.foo()` on a local variable.
|
|
135
|
-
const fileImportAliases = new Map(); // filePath -> Set<string> of alias names
|
|
136
|
-
const fieldHopCache = new Map(); // rootType\0field -> declared type|null
|
|
137
|
-
// Names import-bound to an EXTERNAL module, per file (fix #256,
|
|
138
|
-
// dogfood-measured: 895 node:test `describe(...)` calls in test
|
|
139
|
-
// files were attributed to a project closure named `describe` —
|
|
140
|
-
// the #215 name discipline says an externally-bound bare name
|
|
141
|
-
// cannot reach a project def, so it never counts toward the hot
|
|
142
|
-
// leaderboard). Relative modules, resolved modules, and resolver
|
|
143
|
-
// gaps (first segment names a project path) all stay countable.
|
|
144
|
-
const fileExternalNames = new Map(); // filePath -> Set<string>
|
|
145
|
-
for (const [filePath, fileEntry] of index.files) {
|
|
146
|
-
const aliases = new Set();
|
|
147
|
-
// importNames are the named imports/exports brought into this file.
|
|
148
|
-
// importAliases (when present) carry namespace import aliases (e.g.
|
|
149
|
-
// `import * as mod from "..."` → 'mod').
|
|
150
|
-
for (const n of (fileEntry.importNames || [])) aliases.add(n);
|
|
151
|
-
if (Array.isArray(fileEntry.importAliases)) {
|
|
152
|
-
for (const a of fileEntry.importAliases) {
|
|
153
|
-
if (a && a.local) aliases.add(a.local);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
fileImportAliases.set(filePath, aliases);
|
|
157
|
-
let ext = null;
|
|
158
|
-
for (const b of (fileEntry.importBindings || [])) {
|
|
159
|
-
const mod = String(b.module || '');
|
|
160
|
-
if (!b.name || !mod || mod.startsWith('.') || mod.startsWith('/')) continue;
|
|
161
|
-
if (fileEntry.moduleResolved && fileEntry.moduleResolved[mod]) continue;
|
|
162
|
-
const firstSeg = mod.split(/[./]/).filter(Boolean)[0];
|
|
163
|
-
if (firstSeg && _projectTopLevelNames(index).has(firstSeg)) continue;
|
|
164
|
-
(ext || (ext = new Set())).add(b.name);
|
|
165
|
-
}
|
|
166
|
-
if (ext) fileExternalNames.set(filePath, ext);
|
|
167
|
-
}
|
|
168
|
-
|
|
130
|
+
// Build a cheap, provenance-independent upper bound per spelling.
|
|
131
|
+
// Every confirmed caller must first exist as a call record bearing the
|
|
132
|
+
// target name (or resolved import alias), so this count can safely
|
|
133
|
+
// decide which definitions are capable of entering the requested top
|
|
134
|
+
// N. Exact pinned caller resolution is then run only until no unseen
|
|
135
|
+
// candidate can beat the current Nth result.
|
|
136
|
+
const rawUpperByName = new Map();
|
|
169
137
|
for (const [filePath, entry] of index.callsCache) {
|
|
138
|
+
if (!scopedPaths.has(filePath)) continue;
|
|
139
|
+
if (index.files.get(filePath)?.isBundled) continue;
|
|
170
140
|
if (!entry || !Array.isArray(entry.calls)) continue;
|
|
171
141
|
const seenInFile = new Set();
|
|
172
|
-
const aliasesForFile = fileImportAliases.get(filePath) || new Set();
|
|
173
142
|
for (const c of entry.calls) {
|
|
174
143
|
if (!c || !c.name) continue;
|
|
175
144
|
const key = `${c.name}::${c.line || 0}`;
|
|
176
|
-
if (seenInFile.has(key))
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const isSelfMethod = c.isMethod && SELF_RECEIVERS.has(c.receiver);
|
|
180
|
-
if (!c.isMethod) {
|
|
181
|
-
// Bare-name call: foo() or pkg.Foo() (Go package call has receiver
|
|
182
|
-
// but isMethod:false — keep counting under bareName since they
|
|
183
|
-
// resolve like top-level functions in their package).
|
|
184
|
-
// Externally-bound names are the external library's calls,
|
|
185
|
-
// never a project def's (fix #256).
|
|
186
|
-
if (fileExternalNames.get(filePath)?.has(c.name)) continue;
|
|
187
|
-
bareNameCounts.set(c.name, (bareNameCounts.get(c.name) || 0) + 1);
|
|
188
|
-
} else if (isSelfMethod) {
|
|
189
|
-
// self/this.foo() — attributed to the enclosing class's foo
|
|
190
|
-
selfMethodByName.set(c.name, (selfMethodByName.get(c.name) || 0) + 1);
|
|
191
|
-
methodByName.set(c.name, (methodByName.get(c.name) || 0) + 1);
|
|
192
|
-
} else {
|
|
193
|
-
methodByName.set(c.name, (methodByName.get(c.name) || 0) + 1);
|
|
194
|
-
// Module-alias receiver? `mod.foo()` where `mod` was imported here.
|
|
195
|
-
// Treat the call as resolving to a top-level `foo` (the standalone
|
|
196
|
-
// function exported from `mod`).
|
|
197
|
-
if (c.receiver && aliasesForFile.has(c.receiver)) {
|
|
198
|
-
importedReceiverCounts.set(c.name,
|
|
199
|
-
(importedReceiverCounts.get(c.name) || 0) + 1);
|
|
200
|
-
}
|
|
201
|
-
// Field-access receivers (fix #251): `tm.service.Save()`
|
|
202
|
-
// carries receiverRootType, not receiverType — the same
|
|
203
|
-
// #202/#231 declared-field hop the caller/callee engine
|
|
204
|
-
// uses. Without it, edges `context` confirms were
|
|
205
|
-
// invisible to the hot leaderboard.
|
|
206
|
-
let recvType = c.receiverType;
|
|
207
|
-
if (!recvType && c.receiverField && c.receiverRootType) {
|
|
208
|
-
const hopKey = `${c.receiverRootType}\u0000${c.receiverField}`;
|
|
209
|
-
if (!fieldHopCache.has(hopKey)) {
|
|
210
|
-
const lang = index.files.get(filePath)?.language;
|
|
211
|
-
fieldHopCache.set(hopKey,
|
|
212
|
-
lang ? _declaredFieldType(index, c.receiverRootType, c.receiverField, lang) : null);
|
|
213
|
-
}
|
|
214
|
-
recvType = fieldHopCache.get(hopKey);
|
|
215
|
-
}
|
|
216
|
-
if (recvType) {
|
|
217
|
-
let inner = methodByReceiverType.get(recvType);
|
|
218
|
-
if (!inner) {
|
|
219
|
-
inner = new Map();
|
|
220
|
-
methodByReceiverType.set(recvType, inner);
|
|
221
|
-
}
|
|
222
|
-
inner.set(c.name, (inner.get(c.name) || 0) + 1);
|
|
223
|
-
}
|
|
145
|
+
if (!seenInFile.has(key)) {
|
|
146
|
+
seenInFile.add(key);
|
|
147
|
+
rawUpperByName.set(c.name, (rawUpperByName.get(c.name) || 0) + 1);
|
|
224
148
|
}
|
|
225
|
-
// Also account for resolvedName aliases (e.g. `import {foo as bar}; bar()`
|
|
226
|
-
// resolves to `foo`). Treat the resolved form the same way as the original.
|
|
227
149
|
if (c.resolvedName && c.resolvedName !== c.name) {
|
|
228
150
|
const rkey = `${c.resolvedName}::${c.line || 0}`;
|
|
229
151
|
if (!seenInFile.has(rkey)) {
|
|
230
152
|
seenInFile.add(rkey);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
(bareNameCounts.get(c.resolvedName) || 0) + 1);
|
|
234
|
-
}
|
|
153
|
+
rawUpperByName.set(c.resolvedName,
|
|
154
|
+
(rawUpperByName.get(c.resolvedName) || 0) + 1);
|
|
235
155
|
}
|
|
236
156
|
}
|
|
237
157
|
}
|
|
238
158
|
}
|
|
239
159
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const classOwnersByName = new Map(); // name -> Set<className>
|
|
160
|
+
const candidates = [];
|
|
161
|
+
const seenDefinitions = new Set();
|
|
243
162
|
for (const [name, symbols] of index.symbols) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
163
|
+
const upper = rawUpperByName.get(name) || 0;
|
|
164
|
+
if (upper === 0) continue;
|
|
165
|
+
const callable = symbols.filter(symbol =>
|
|
166
|
+
FUNCTION_TYPES.has(symbol.type) &&
|
|
167
|
+
matchesReportingScope(index, symbol.relativePath, options));
|
|
168
|
+
for (const symbol of callable) {
|
|
169
|
+
if (index.files.get(symbol.file)?.isBundled) continue;
|
|
170
|
+
if (options.productionCallsOnly && require('./shared').isTestPath(symbol.relativePath)) continue;
|
|
171
|
+
const identity = `${symbol.file}:${symbol.startLine}:${name}:` +
|
|
172
|
+
`${symbol.className || symbol.receiver || ''}:${symbol.params || ''}`;
|
|
173
|
+
if (seenDefinitions.has(identity)) continue;
|
|
174
|
+
seenDefinitions.add(identity);
|
|
175
|
+
|
|
176
|
+
// A linked C/C++ prototype and implementation close to the
|
|
177
|
+
// same compiler identity. Show the implementation once.
|
|
178
|
+
if (symbol.isSignature && callable.some(candidate =>
|
|
179
|
+
!candidate.isSignature &&
|
|
180
|
+
(candidate.className || candidate.receiver || null) ===
|
|
181
|
+
(symbol.className || symbol.receiver || null) &&
|
|
182
|
+
(index.importGraph.get(candidate.file)?.has(symbol.file) ||
|
|
183
|
+
index.importGraph.get(symbol.file)?.has(candidate.file)))) {
|
|
184
|
+
continue;
|
|
251
185
|
}
|
|
186
|
+
candidates.push({ name, symbol, upper });
|
|
252
187
|
}
|
|
253
188
|
}
|
|
189
|
+
candidates.sort((a, b) =>
|
|
190
|
+
(b.upper - a.upper) ||
|
|
191
|
+
codeUnitCompare(a.symbol.relativePath, b.symbol.relativePath) ||
|
|
192
|
+
(a.symbol.startLine || 0) - (b.symbol.startLine || 0));
|
|
254
193
|
|
|
255
|
-
// MEDIUM-6: aggregate by name. Multiple definitions of the same name
|
|
256
|
-
// in different files (e.g. `tmp` in test/helpers/index.js AND
|
|
257
|
-
// test/accuracy.test.js) previously each got the GLOBAL call count,
|
|
258
|
-
// duplicating the row and inflating the leaderboard. We now emit
|
|
259
|
-
// one row per name with a `locations` list, so the user sees both
|
|
260
|
-
// definitions but the count appears exactly once.
|
|
261
|
-
//
|
|
262
|
-
// BUG-H2: with the buckets above, attribute counts per (name, ownerClass):
|
|
263
|
-
// - standalone function: bareNameCounts[name]
|
|
264
|
-
// - class method (Foo.bar): methodByReceiverType[Foo][bar]
|
|
265
|
-
// + selfMethodByName[bar] / numOwnerClasses
|
|
266
|
-
// + (residual unresolved method calls split evenly)
|
|
267
|
-
// - falls back to methodByName[name] when no receiverType evidence exists.
|
|
268
194
|
const hotList = [];
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
let
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const locKey = `${relativePath}:${sym.startLine}`;
|
|
281
|
-
if (seenLoc.has(locKey)) continue;
|
|
282
|
-
seenLoc.add(locKey);
|
|
283
|
-
locations.push({
|
|
284
|
-
file: relativePath,
|
|
285
|
-
startLine: sym.startLine,
|
|
286
|
-
endLine: sym.endLine,
|
|
287
|
-
...(sym.className && { className: sym.className }),
|
|
195
|
+
const { findCallers } = require('./callers');
|
|
196
|
+
const scopedCallerQuery = !!(options.file || options.in ||
|
|
197
|
+
(options.exclude && options.exclude.length > 0));
|
|
198
|
+
let refined = 0;
|
|
199
|
+
if (top > 0) {
|
|
200
|
+
for (let candidateIndex = 0; candidateIndex < candidates.length; candidateIndex++) {
|
|
201
|
+
const { name, symbol } = candidates[candidateIndex];
|
|
202
|
+
const exact = findCallers(index, name, {
|
|
203
|
+
targetDefinitions: [symbol],
|
|
204
|
+
includeTests: true,
|
|
205
|
+
collectAccount: true,
|
|
288
206
|
});
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// Calls like `dict.get()` (no receiverType) are NOT attributed — they
|
|
317
|
-
// would inflate the count with builtin/unrelated method calls.
|
|
318
|
-
const selfShare = selfMethodByName.get(name) || 0;
|
|
319
|
-
const totalOwners = (classOwnersByName.get(name) || new Set()).size || 1;
|
|
320
|
-
|
|
321
|
-
let typedHits = 0;
|
|
322
|
-
for (const cls of ownerClasses) {
|
|
323
|
-
const inner = methodByReceiverType.get(cls);
|
|
324
|
-
if (inner) typedHits += (inner.get(name) || 0);
|
|
207
|
+
refined++;
|
|
208
|
+
const count = exact.filter(caller =>
|
|
209
|
+
caller.tier !== 'unverified' &&
|
|
210
|
+
(!scopedCallerQuery || scopedPaths.has(caller.file)) &&
|
|
211
|
+
!index.files.get(caller.file)?.isBundled &&
|
|
212
|
+
(!options.productionCallsOnly ||
|
|
213
|
+
!require('./shared').isTestPath(caller.relativePath || caller.file))).length;
|
|
214
|
+
if (count > 0) {
|
|
215
|
+
const owner = symbol.className ||
|
|
216
|
+
(symbol.receiver || '').replace(/^\*/, '');
|
|
217
|
+
hotList.push({
|
|
218
|
+
name: owner ? `${owner}.${name}` : name,
|
|
219
|
+
file: symbol.relativePath,
|
|
220
|
+
startLine: symbol.startLine,
|
|
221
|
+
endLine: symbol.endLine,
|
|
222
|
+
callCount: count,
|
|
223
|
+
evidence: 'confirmed-callers',
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
hotList.sort((a, b) =>
|
|
227
|
+
(b.callCount - a.callCount) ||
|
|
228
|
+
codeUnitCompare(a.file, b.file) ||
|
|
229
|
+
(a.startLine || 0) - (b.startLine || 0));
|
|
230
|
+
if (hotList.length >= top) {
|
|
231
|
+
const threshold = hotList[top - 1].callCount;
|
|
232
|
+
const nextUpper = candidates[candidateIndex + 1]?.upper ?? -1;
|
|
233
|
+
if (nextUpper < threshold) break;
|
|
325
234
|
}
|
|
326
|
-
|
|
327
|
-
// Self-method calls: split evenly across owner classes (each class's own
|
|
328
|
-
// self.method() resolves to itself). When this row covers all owners
|
|
329
|
-
// (locations cover the only class that has this method), give the full
|
|
330
|
-
// self-share to this row.
|
|
331
|
-
const selfShareForRow = selfShare * (ownerClasses.size / totalOwners);
|
|
332
|
-
|
|
333
|
-
count = typedHits + Math.round(selfShareForRow);
|
|
334
|
-
// If we used the self-method heuristic across multiple classes, mark approximate.
|
|
335
|
-
if (selfShare > 0 && totalOwners > 1) approximate = true;
|
|
336
235
|
}
|
|
337
|
-
if (count === 0) continue; // skip dead symbols
|
|
338
|
-
|
|
339
|
-
if (approximate) usedHeuristicSplit = true;
|
|
340
|
-
// Sort locations by (file, startLine) for stable display.
|
|
341
|
-
locations.sort((a, b) =>
|
|
342
|
-
codeUnitCompare(a.file, b.file) ||
|
|
343
|
-
(a.startLine || 0) - (b.startLine || 0)
|
|
344
|
-
);
|
|
345
|
-
const primary = locations[0];
|
|
346
|
-
hotList.push({
|
|
347
|
-
// Use the representative symbol's className for display name
|
|
348
|
-
// (so "Foo.bar" is preserved when applicable). When defs
|
|
349
|
-
// disagree on className, just show the bare name.
|
|
350
|
-
name: representative && representative.className
|
|
351
|
-
? `${representative.className}.${name}`
|
|
352
|
-
: name,
|
|
353
|
-
// Primary location remains for backward-compat with consumers
|
|
354
|
-
// that read `file`/`startLine`/`endLine` directly.
|
|
355
|
-
file: primary.file,
|
|
356
|
-
startLine: primary.startLine,
|
|
357
|
-
endLine: primary.endLine,
|
|
358
|
-
callCount: count,
|
|
359
|
-
...(approximate && { approximate: true }),
|
|
360
|
-
...(locations.length > 1 && { locations }),
|
|
361
|
-
});
|
|
362
236
|
}
|
|
363
237
|
|
|
364
238
|
// Stable order: callCount desc, then (relativePath, startLine) asc.
|
|
@@ -370,11 +244,13 @@ function getStats(index, options = {}) {
|
|
|
370
244
|
|
|
371
245
|
stats.hot = {
|
|
372
246
|
top,
|
|
373
|
-
total: hotList.length,
|
|
247
|
+
total: refined === candidates.length ? hotList.length : candidates.length,
|
|
248
|
+
totalKind: refined === candidates.length ? 'confirmed' : 'raw-call-candidates',
|
|
249
|
+
refined,
|
|
374
250
|
items: hotList.slice(0, top),
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
251
|
+
note: refined === candidates.length
|
|
252
|
+
? 'Counts are confirmed caller-engine edges pinned to each displayed definition; unverified dispatch is excluded.'
|
|
253
|
+
: `Displayed counts are exact confirmed caller-engine edges; ${candidates.length} raw candidates were bounded and ${refined} required exact refinement.`,
|
|
378
254
|
};
|
|
379
255
|
}
|
|
380
256
|
|
|
@@ -435,11 +311,10 @@ function getToc(index, options = {}) {
|
|
|
435
311
|
if (options.in) {
|
|
436
312
|
if (!index.matchesFilters(fileEntry.relativePath, { in: options.in })) continue;
|
|
437
313
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
);
|
|
314
|
+
// Every callable kind, incl. accessors/properties/indexers — a
|
|
315
|
+
// hand-rolled subset here silently dropped C# `this[]` (kind
|
|
316
|
+
// 'property') and Python accessor kinds from the detailed listing.
|
|
317
|
+
let functions = fileEntry.symbols.filter(s => CALLABLE_SYMBOL_KINDS.has(s.type));
|
|
443
318
|
const classes = fileEntry.symbols.filter(s =>
|
|
444
319
|
['class', 'interface', 'type', 'enum', 'struct', 'trait', 'impl', 'record', 'namespace'].includes(s.type)
|
|
445
320
|
);
|
|
@@ -521,7 +396,10 @@ function getToc(index, options = {}) {
|
|
|
521
396
|
|
|
522
397
|
return {
|
|
523
398
|
meta: {
|
|
524
|
-
complete
|
|
399
|
+
// `complete` describes this returned list, while the narrower
|
|
400
|
+
// dynamic-import fact has an unambiguous name of its own.
|
|
401
|
+
complete: hiddenFiles === 0 && totalDynamic === 0,
|
|
402
|
+
noDynamicImports: totalDynamic === 0,
|
|
525
403
|
skipped: 0,
|
|
526
404
|
dynamicImports: totalDynamic,
|
|
527
405
|
uncertain: 0,
|
|
@@ -561,11 +439,19 @@ function getToc(index, options = {}) {
|
|
|
561
439
|
* @param {object} options - { deep, sampleSize, in, file }
|
|
562
440
|
*/
|
|
563
441
|
function doctor(index, options = {}) {
|
|
564
|
-
const
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
442
|
+
const projectLanguage = index._getPredominantLanguage();
|
|
443
|
+
const staticSpecialImports = projectLanguage &&
|
|
444
|
+
!langTraits(projectLanguage)?.hasDynamicImports;
|
|
445
|
+
const importBlindspotLabel = staticSpecialImports
|
|
446
|
+
? (projectLanguage === 'rust' ? 'glob import(s)' : 'blank/dot import(s)')
|
|
447
|
+
: 'dynamic import(s)';
|
|
448
|
+
const normalizedExclude = Array.isArray(options.exclude)
|
|
449
|
+
? options.exclude.filter(Boolean)
|
|
450
|
+
: (options.exclude
|
|
451
|
+
? String(options.exclude).split(',').map(s => s.trim()).filter(Boolean)
|
|
452
|
+
: []);
|
|
453
|
+
const hasFilter = !!(options.in || options.file || normalizedExclude.length > 0);
|
|
454
|
+
const matchInFilter = (rel) => matchesReportingScope(index, rel, options);
|
|
569
455
|
|
|
570
456
|
const fileCounts = { total: 0, scanned: 0 };
|
|
571
457
|
const langs = {};
|
|
@@ -580,8 +466,13 @@ function doctor(index, options = {}) {
|
|
|
580
466
|
dynamicImports: { count: 0, fileCount: 0, files: [] },
|
|
581
467
|
evalCalls: { count: 0, fileCount: 0, files: [] },
|
|
582
468
|
reflection: { count: 0, fileCount: 0, files: [] },
|
|
469
|
+
computedDispatch:{ count: 0, fileCount: 0, files: [] },
|
|
583
470
|
parseFailures: { count: 0, fileCount: 0, files: [] },
|
|
584
471
|
parseRecoveries:{ count: 0, fileCount: 0, files: [] },
|
|
472
|
+
unsupportedSources: {
|
|
473
|
+
count: 0, fileCount: 0, files: [], languages: {}, extensions: {},
|
|
474
|
+
},
|
|
475
|
+
skippedSources: { count: 0, fileCount: 0, files: [], reasons: {} },
|
|
585
476
|
};
|
|
586
477
|
|
|
587
478
|
// Reflection/eval signals come from the shared text-blind-spot counter
|
|
@@ -589,6 +480,7 @@ function doctor(index, options = {}) {
|
|
|
589
480
|
// footer, so the two never drift (field-report #2). Occurrence counts.
|
|
590
481
|
const { hasTextBlindspots, countTextBlindspots } = require('./shared');
|
|
591
482
|
|
|
483
|
+
const computedDispatch = projectComputedDispatch(index);
|
|
592
484
|
for (const [filePath, fe] of index.files) {
|
|
593
485
|
fileCounts.total++;
|
|
594
486
|
const rel = fe.relativePath || filePath;
|
|
@@ -610,6 +502,10 @@ function doctor(index, options = {}) {
|
|
|
610
502
|
};
|
|
611
503
|
|
|
612
504
|
if (fe.dynamicImports && fe.dynamicImports > 0) recordBlind(blindSpots.dynamicImports, fe.dynamicImports);
|
|
505
|
+
if (computedDispatch.has(filePath)) {
|
|
506
|
+
recordBlind(blindSpots.computedDispatch,
|
|
507
|
+
computedDispatch.get(filePath).length);
|
|
508
|
+
}
|
|
613
509
|
if (fe.parseError) recordBlind(blindSpots.parseFailures, 1);
|
|
614
510
|
if (fe.parseRecovery) recordBlind(blindSpots.parseRecoveries, 1);
|
|
615
511
|
|
|
@@ -640,6 +536,35 @@ function doctor(index, options = {}) {
|
|
|
640
536
|
recordedParseFailureFiles.add(rel);
|
|
641
537
|
}
|
|
642
538
|
|
|
539
|
+
// Project-wide discovery also records common source languages for which
|
|
540
|
+
// UCN has no parser. These are not parse failures: they are an explicit
|
|
541
|
+
// engine boundary and a required grep/ripgrep handoff.
|
|
542
|
+
for (const skipped of index.unsupportedFiles || []) {
|
|
543
|
+
const rel = skipped.relativePath;
|
|
544
|
+
if (!matchInFilter(rel)) continue;
|
|
545
|
+
const unsupported = blindSpots.unsupportedSources;
|
|
546
|
+
unsupported.count++;
|
|
547
|
+
unsupported.fileCount++;
|
|
548
|
+
unsupported.languages[skipped.language] =
|
|
549
|
+
(unsupported.languages[skipped.language] || 0) + 1;
|
|
550
|
+
unsupported.extensions[skipped.extension] =
|
|
551
|
+
(unsupported.extensions[skipped.extension] || 0) + 1;
|
|
552
|
+
if (unsupported.files.length < BLINDSPOT_FILE_CAP) unsupported.files.push(rel);
|
|
553
|
+
}
|
|
554
|
+
fileCounts.unsupported = blindSpots.unsupportedSources.fileCount;
|
|
555
|
+
|
|
556
|
+
for (const issue of index.discoveryIssues || []) {
|
|
557
|
+
const rel = issue.relativePath || '.';
|
|
558
|
+
if (!matchInFilter(rel)) continue;
|
|
559
|
+
const skipped = blindSpots.skippedSources;
|
|
560
|
+
skipped.count++;
|
|
561
|
+
skipped.fileCount++;
|
|
562
|
+
skipped.reasons[issue.reason || 'unknown'] =
|
|
563
|
+
(skipped.reasons[issue.reason || 'unknown'] || 0) + 1;
|
|
564
|
+
if (skipped.files.length < BLINDSPOT_FILE_CAP) skipped.files.push(rel);
|
|
565
|
+
}
|
|
566
|
+
fileCounts.skipped = blindSpots.skippedSources.fileCount;
|
|
567
|
+
|
|
643
568
|
// Evidence profile — sampled only in deep mode. This is deliberately NOT
|
|
644
569
|
// called "accuracy" or "coverage": it describes how UCN classified edges
|
|
645
570
|
// it found. Compiler/LSP oracle evaluation is the accuracy measurement.
|
|
@@ -663,47 +588,80 @@ function doctor(index, options = {}) {
|
|
|
663
588
|
if (blindSpots.parseRecoveries.count > 0) blindSignals.push(`${blindSpots.parseRecoveries.count} parse-recovery file(s)`);
|
|
664
589
|
if (blindSpots.evalCalls.count > 0) blindSignals.push(`${blindSpots.evalCalls.count} eval/exec use(s) in ${blindSpots.evalCalls.fileCount} file(s)`);
|
|
665
590
|
if (blindSpots.reflection.count > 0) blindSignals.push(`${blindSpots.reflection.count} reflection use(s) in ${blindSpots.reflection.fileCount} file(s)`);
|
|
666
|
-
if (blindSpots.
|
|
591
|
+
if (blindSpots.computedDispatch.count > 0) {
|
|
592
|
+
blindSignals.push(`${blindSpots.computedDispatch.count} computed dispatch call(s) in ${blindSpots.computedDispatch.fileCount} file(s)`);
|
|
593
|
+
}
|
|
594
|
+
if (blindSpots.dynamicImports.count > 0) blindSignals.push(`${blindSpots.dynamicImports.count} ${importBlindspotLabel} in ${blindSpots.dynamicImports.fileCount} file(s)`);
|
|
595
|
+
if (blindSpots.unsupportedSources.count > 0) {
|
|
596
|
+
const mix = Object.entries(blindSpots.unsupportedSources.languages)
|
|
597
|
+
.map(([language, count]) => `${language} ${count}`)
|
|
598
|
+
.join(', ');
|
|
599
|
+
blindSignals.push(`${blindSpots.unsupportedSources.count} unsupported source file(s): ${mix}`);
|
|
600
|
+
}
|
|
601
|
+
if (blindSpots.skippedSources.count > 0) {
|
|
602
|
+
const reasons = Object.entries(blindSpots.skippedSources.reasons)
|
|
603
|
+
.map(([reason, count]) => `${reason} ${count}`).join(', ');
|
|
604
|
+
blindSignals.push(`${blindSpots.skippedSources.count} source discovery gap(s): ${reasons}`);
|
|
605
|
+
}
|
|
667
606
|
|
|
668
607
|
// Trust is task-specific. A healthy index can be excellent for navigation
|
|
669
608
|
// while still requiring review before a breaking refactor or deletion.
|
|
670
609
|
// Never infer semantic accuracy from rule-assigned confidence decimals.
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
610
|
+
const unsupportedCount = blindSpots.unsupportedSources.count;
|
|
611
|
+
const incompleteIndex = blindSpots.parseFailures.count > 0 ||
|
|
612
|
+
blindSpots.parseRecoveries.count > 0 || cache.fresh === false ||
|
|
613
|
+
!!index.truncated || unsupportedCount > 0 || blindSpots.skippedSources.count > 0;
|
|
614
|
+
const indexLevel = fileCounts.scanned === 0 && unsupportedCount > 0 ? 'UNSUPPORTED'
|
|
615
|
+
: fileCounts.scanned === 0 ? 'UNKNOWN'
|
|
616
|
+
: incompleteIndex ? 'PARTIAL' : 'HIGH';
|
|
617
|
+
const indexReason = fileCounts.scanned === 0 && unsupportedCount > 0
|
|
618
|
+
? `0 supported files indexed; ${unsupportedCount} unsupported source file(s) require grep/ripgrep`
|
|
619
|
+
: fileCounts.scanned === 0 ? 'empty scope'
|
|
675
620
|
: blindSpots.parseFailures.count > 0
|
|
676
621
|
? `${blindSpots.parseFailures.count} file(s) failed to parse`
|
|
677
622
|
: blindSpots.parseRecoveries.count > 0
|
|
678
623
|
? `${blindSpots.parseRecoveries.count} file(s) required parser recovery; indexed results may be partial`
|
|
679
|
-
: cache.fresh === false ? 'index cache is stale'
|
|
624
|
+
: cache.fresh === false ? 'index cache is stale'
|
|
625
|
+
: index.truncated ? `file discovery stopped at maxFiles=${index.truncated.maxFiles}`
|
|
626
|
+
: blindSpots.skippedSources.count > 0
|
|
627
|
+
? `${blindSpots.skippedSources.count} source path(s) were not indexed; inspect discovery reasons`
|
|
628
|
+
: unsupportedCount > 0
|
|
629
|
+
? `${fileCounts.scanned} supported file(s) indexed; ${unsupportedCount} unsupported source file(s) require grep/ripgrep`
|
|
630
|
+
: 'fresh index; no parse failures';
|
|
680
631
|
|
|
681
632
|
let evidenceLevel = 'UNKNOWN';
|
|
682
633
|
let evidenceReason = 'not sampled; run --deep for a stratified evidence profile';
|
|
683
634
|
if (evidenceProfile) {
|
|
684
635
|
if (evidenceProfile.total === 0) {
|
|
636
|
+
evidenceLevel = 'NONE';
|
|
685
637
|
evidenceReason = 'sample contained no caller edges';
|
|
686
638
|
} else {
|
|
687
639
|
const confirmedShare = evidenceProfile.confirmed / evidenceProfile.total;
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
640
|
+
// Confirmed share is a classification mix, not accuracy and not a
|
|
641
|
+
// readiness score. A conservative engine can be excellent while
|
|
642
|
+
// intentionally routing many edges to visible unverified bands.
|
|
643
|
+
evidenceLevel = evidenceProfile.adequate ? 'PROFILED' : 'LIMITED';
|
|
691
644
|
evidenceReason = `${(confirmedShare * 100).toFixed(1)}% confirmed-evidence edges across ${evidenceProfile.sampled} pinned definitions`;
|
|
692
645
|
if (!evidenceProfile.adequate) evidenceReason += '; sample too small for a readiness decision';
|
|
693
646
|
}
|
|
694
647
|
}
|
|
695
648
|
|
|
696
|
-
const dynamicCount = blindSpots.evalCalls.count + blindSpots.reflection.count +
|
|
649
|
+
const dynamicCount = blindSpots.evalCalls.count + blindSpots.reflection.count +
|
|
650
|
+
blindSpots.dynamicImports.count + blindSpots.computedDispatch.count;
|
|
697
651
|
const semanticLevel = blindSpots.parseFailures.count > 0 || blindSpots.parseRecoveries.count > 0 ? 'LOW'
|
|
698
|
-
:
|
|
652
|
+
: unsupportedCount > 0 ? 'PARTIAL'
|
|
653
|
+
: dynamicCount > 0 ? 'REVIEW' : 'UNKNOWN';
|
|
699
654
|
const semanticReason = blindSignals.length
|
|
700
655
|
? `semantic recall may miss runtime-resolved edges: ${blindSignals.join(', ')}`
|
|
701
656
|
: 'no known runtime blind spots detected; alias/dynamic completeness is not compiler-verified locally';
|
|
702
657
|
|
|
703
658
|
const navigationLevel = indexLevel;
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
659
|
+
const hardIndexRisk = blindSpots.parseFailures.count > 0 ||
|
|
660
|
+
blindSpots.parseRecoveries.count > 0 || cache.fresh === false || !!index.truncated;
|
|
661
|
+
const refactorLevel = indexLevel === 'UNKNOWN' ? 'UNKNOWN'
|
|
662
|
+
: indexLevel === 'UNSUPPORTED' ? 'UNSUPPORTED'
|
|
663
|
+
: hardIndexRisk ? 'LOW'
|
|
664
|
+
: unsupportedCount > 0 ? 'PARTIAL' : 'REVIEW';
|
|
707
665
|
const deletionLevel = refactorLevel === 'LOW' ? 'LOW' : 'REVIEW';
|
|
708
666
|
const dimensions = {
|
|
709
667
|
index: { level: indexLevel, reason: indexReason },
|
|
@@ -712,36 +670,46 @@ function doctor(index, options = {}) {
|
|
|
712
670
|
navigation: { level: navigationLevel, reason: indexReason },
|
|
713
671
|
refactor: {
|
|
714
672
|
level: refactorLevel,
|
|
715
|
-
reason:
|
|
716
|
-
? '
|
|
717
|
-
:
|
|
673
|
+
reason: indexLevel === 'UNKNOWN'
|
|
674
|
+
? 'no supported source scope was indexed'
|
|
675
|
+
: indexLevel === 'UNSUPPORTED'
|
|
676
|
+
? 'UCN cannot analyze this source scope; use grep/ripgrep and a language-native tool'
|
|
677
|
+
: unsupportedCount > 0
|
|
678
|
+
? 'supported files are analyzable, but skipped language files require grep/ripgrep before cross-language changes'
|
|
679
|
+
: 'text-ground accounting is available; aliases, reflection, and unverified edges still require review',
|
|
718
680
|
},
|
|
719
681
|
deletion: {
|
|
720
682
|
level: deletionLevel,
|
|
721
683
|
reason: 'deletion additionally requires usages/deadcode review and tests; caller accounting alone is insufficient',
|
|
722
684
|
},
|
|
723
685
|
};
|
|
724
|
-
|
|
725
|
-
|
|
686
|
+
// `repo` is an orientation/navigation command. Its headline must answer
|
|
687
|
+
// that task, while refactor/deletion remain separately visible dimensions.
|
|
688
|
+
const trust = navigationLevel;
|
|
689
|
+
const trustReason = dimensions.navigation.reason;
|
|
726
690
|
|
|
727
691
|
return {
|
|
728
692
|
root: index.root,
|
|
693
|
+
projectLanguage,
|
|
729
694
|
version: require('../package.json').version, // running ucn version — surfaces MCP/CLI drift (field-report #3)
|
|
730
695
|
files: fileCounts,
|
|
731
696
|
symbols: totalSymbols,
|
|
732
697
|
languages: langs,
|
|
733
698
|
blindSpots,
|
|
734
699
|
evidenceProfile,
|
|
735
|
-
// Backward-compatible field name. `kind` prevents consumers from
|
|
736
|
-
// mistaking this for semantic coverage or measured accuracy.
|
|
737
|
-
coverage: evidenceProfile,
|
|
738
700
|
cache,
|
|
739
701
|
commandTrust: summarizeCommandTrust(),
|
|
740
702
|
trust,
|
|
741
703
|
trustReason,
|
|
742
|
-
trustScope: '
|
|
704
|
+
trustScope: 'navigation-readiness',
|
|
743
705
|
dimensions,
|
|
744
|
-
...(
|
|
706
|
+
...(hasFilter && {
|
|
707
|
+
filter: {
|
|
708
|
+
...(options.file && { file: options.file }),
|
|
709
|
+
...(options.in && { in: options.in }),
|
|
710
|
+
...(normalizedExclude.length > 0 && { exclude: normalizedExclude }),
|
|
711
|
+
},
|
|
712
|
+
}),
|
|
745
713
|
};
|
|
746
714
|
}
|
|
747
715
|
|
|
@@ -807,6 +775,9 @@ function computeEvidenceProfile(index, { sampleSize, matchInFilter }) {
|
|
|
807
775
|
const allEdges = [...callers, ...(callers.unverifiedEntries || [])];
|
|
808
776
|
const seenSites = new Set();
|
|
809
777
|
for (const c of allEdges) {
|
|
778
|
+
const rel = c.relativePath ||
|
|
779
|
+
(c.file && path.isAbsolute(c.file) ? path.relative(index.root, c.file) : c.file);
|
|
780
|
+
if (!matchInFilter(rel)) continue;
|
|
810
781
|
const site = `${c.file || c.relativePath}:${c.line}:${c.tier || c.reason || ''}`;
|
|
811
782
|
if (seenSites.has(site)) continue;
|
|
812
783
|
seenSites.add(site);
|
|
@@ -836,16 +807,34 @@ function computeEvidenceProfile(index, { sampleSize, matchInFilter }) {
|
|
|
836
807
|
*/
|
|
837
808
|
function orient(index, options = {}) {
|
|
838
809
|
const top = options.top || 8;
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
810
|
+
const scope = {
|
|
811
|
+
file: options.file,
|
|
812
|
+
in: options.in,
|
|
813
|
+
exclude: options.exclude,
|
|
814
|
+
};
|
|
815
|
+
const { isTestPath } = require('./shared');
|
|
816
|
+
const hasProductionFiles = [...index.files.values()].some(fileEntry => {
|
|
817
|
+
const relativePath = fileEntry.relativePath;
|
|
818
|
+
return relativePath &&
|
|
819
|
+
matchesReportingScope(index, relativePath, scope) &&
|
|
820
|
+
!isTestPath(relativePath);
|
|
821
|
+
});
|
|
822
|
+
const stats = getStats(index, {
|
|
823
|
+
...scope,
|
|
824
|
+
hot: true,
|
|
825
|
+
top,
|
|
826
|
+
// A production-only call count is useful only when the selected scope
|
|
827
|
+
// actually contains production files. In an all-test repository it
|
|
828
|
+
// would erase the raw ranking that orient promises as its fallback.
|
|
829
|
+
productionCallsOnly: options.includeTests !== true && hasProductionFiles,
|
|
830
|
+
});
|
|
831
|
+
const health = doctor(index, scope);
|
|
843
832
|
|
|
844
833
|
// Densest directories (leaf dirname rollup — "where does the code live")
|
|
845
834
|
const dirMap = new Map();
|
|
846
835
|
for (const [, fe] of index.files) {
|
|
847
836
|
const rp = fe.relativePath;
|
|
848
|
-
if (!rp) continue;
|
|
837
|
+
if (!rp || !matchesReportingScope(index, rp, scope)) continue;
|
|
849
838
|
const slash = rp.lastIndexOf('/');
|
|
850
839
|
const dir = slash === -1 ? '.' : rp.slice(0, slash);
|
|
851
840
|
const e = dirMap.get(dir) || { dir, files: 0, symbols: 0 };
|
|
@@ -861,7 +850,24 @@ function orient(index, options = {}) {
|
|
|
861
850
|
let entrypoints = null;
|
|
862
851
|
try {
|
|
863
852
|
const { detectEntrypoints } = require('./entrypoints');
|
|
864
|
-
const
|
|
853
|
+
const { addTestExclusions } = require('./shared');
|
|
854
|
+
const orientExclude = Array.isArray(options.exclude)
|
|
855
|
+
? options.exclude
|
|
856
|
+
: (options.exclude
|
|
857
|
+
? String(options.exclude).split(',').map(s => s.trim()).filter(Boolean)
|
|
858
|
+
: []);
|
|
859
|
+
// Same default as the entrypoints command: the orientation count
|
|
860
|
+
// describes the project's own entry surface, not test fixtures.
|
|
861
|
+
const detected = detectEntrypoints(index, {
|
|
862
|
+
file: options.file,
|
|
863
|
+
exclude: options.includeTests === true
|
|
864
|
+
? orientExclude
|
|
865
|
+
: addTestExclusions(orientExclude),
|
|
866
|
+
});
|
|
867
|
+
const eps = Array.isArray(detected)
|
|
868
|
+
? detected.filter(entry => matchesReportingScope(
|
|
869
|
+
index, entry.relativePath || entry.file, scope))
|
|
870
|
+
: detected;
|
|
865
871
|
if (Array.isArray(eps)) {
|
|
866
872
|
const byType = new Map();
|
|
867
873
|
for (const e of eps) byType.set(e.type, (byType.get(e.type) || 0) + 1);
|
|
@@ -877,7 +883,6 @@ function orient(index, options = {}) {
|
|
|
877
883
|
// Orientation wants the ENGINE's hot functions, not fixture helpers —
|
|
878
884
|
// prefer production-path entries (labeled as such by the formatter);
|
|
879
885
|
// an all-test project falls back to the raw ranking.
|
|
880
|
-
const { isTestPath } = require('./shared');
|
|
881
886
|
const allHot = (stats.hot?.items || []).map(i => ({
|
|
882
887
|
name: i.name,
|
|
883
888
|
file: i.file,
|
|
@@ -892,22 +897,39 @@ function orient(index, options = {}) {
|
|
|
892
897
|
|
|
893
898
|
return {
|
|
894
899
|
root: stats.root,
|
|
900
|
+
...(options.in && { scope: options.in }),
|
|
901
|
+
...(options.file && !options.in && { scope: options.file }),
|
|
895
902
|
files: stats.files,
|
|
896
903
|
symbols: stats.symbols,
|
|
897
904
|
buildTime: stats.buildTime,
|
|
898
905
|
byLanguage: stats.byLanguage,
|
|
899
906
|
dirs,
|
|
900
|
-
hot: {
|
|
907
|
+
hot: {
|
|
908
|
+
total: stats.hot?.total ?? 0,
|
|
909
|
+
totalKind: stats.hot?.totalKind || 'confirmed',
|
|
910
|
+
refined: stats.hot?.refined ?? 0,
|
|
911
|
+
top,
|
|
912
|
+
production,
|
|
913
|
+
items: hotItems,
|
|
914
|
+
},
|
|
901
915
|
entrypoints,
|
|
902
916
|
trust: {
|
|
903
917
|
level: health.trust,
|
|
918
|
+
projectLanguage: health.projectLanguage,
|
|
904
919
|
blindSpots: {
|
|
905
920
|
dynamicImports: health.blindSpots?.dynamicImports?.count ?? 0,
|
|
906
921
|
evalCalls: health.blindSpots?.evalCalls?.count ?? 0,
|
|
907
922
|
reflection: health.blindSpots?.reflection?.count ?? 0,
|
|
923
|
+
computedDispatch: health.blindSpots?.computedDispatch?.count ?? 0,
|
|
908
924
|
parseFailures: health.blindSpots?.parseFailures?.count ?? 0,
|
|
925
|
+
parseRecoveries: health.blindSpots?.parseRecoveries?.count ?? 0,
|
|
926
|
+
unsupportedSources: health.blindSpots?.unsupportedSources?.count ?? 0,
|
|
927
|
+
skippedSources: health.blindSpots?.skippedSources?.count ?? 0,
|
|
909
928
|
},
|
|
910
929
|
},
|
|
930
|
+
unsupportedSources: health.blindSpots?.unsupportedSources ?? null,
|
|
931
|
+
skippedSources: health.blindSpots?.skippedSources ?? null,
|
|
932
|
+
projectLanguage: health.projectLanguage,
|
|
911
933
|
suggest: hottestProd ? hottestProd.name : null,
|
|
912
934
|
};
|
|
913
935
|
}
|