ucn 5.4.2 → 5.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +27 -16
- package/cli/index.js +45 -14
- package/core/accessors.js +118 -3
- package/core/account.js +62 -11
- package/core/analysis.js +517 -266
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +529 -31
- package/core/callers.js +12228 -921
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +7 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +305 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +472 -66
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +3026 -0
- package/core/rust-modules.js +203 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1185 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2265 -112
- package/languages/utils.js +298 -4
- package/mcp/server.js +22 -8
- package/package.json +1 -1
package/core/registry.js
CHANGED
|
@@ -136,7 +136,7 @@ const FLAG_APPLICABILITY = {
|
|
|
136
136
|
// related. Caller-bearing projections always preserve ACCOUNT/CONTRACT.
|
|
137
137
|
show: ['name', 'file', 'exclude', 'className', 'line', 'sections', 'includeMethods', 'includeTests', 'top', 'all', 'withTypes', 'minConfidence', 'showConfidence', 'unreachableOnly', 'compact', 'git', 'diverse', 'lines'],
|
|
138
138
|
find: ['name', 'file', 'exclude', 'className', 'includeTests', 'limit', 'exact', 'in', 'compact', 'type', 'withSource', 'lines'],
|
|
139
|
-
usages: ['name', 'file', 'exclude', 'className', '
|
|
139
|
+
usages: ['name', 'file', 'exclude', 'className', 'excludeTests', 'limit', 'codeOnly', 'context', 'in', 'compact', 'all', 'lines'],
|
|
140
140
|
search: ['term', 'file', 'exclude', 'includeTests', 'top', 'limit', 'codeOnly', 'caseSensitive', 'context', 'regex', 'in', 'type', 'param', 'receiver', 'returns', 'decorator', 'exported', 'unused', 'lines'],
|
|
141
141
|
source: ['name', 'file', 'className', 'line', 'range', 'all', 'maxLines', 'raw'],
|
|
142
142
|
trace: ['name', 'file', 'exclude', 'className', 'line', 'direction', 'to', 'includeMethods', 'depth', 'all', 'expandUnverified'],
|
package/core/reporting.js
CHANGED
|
@@ -220,14 +220,37 @@ function getStats(index, options = {}) {
|
|
|
220
220
|
}
|
|
221
221
|
return ranges.length > 0 && lineInRanges(line, ranges);
|
|
222
222
|
};
|
|
223
|
+
// Files whose callers can COUNT (fix #365). The exact count below
|
|
224
|
+
// keeps only callers in scope, outside bundles, and - for a
|
|
225
|
+
// production-only ranking - outside test files, so records elsewhere
|
|
226
|
+
// can never raise a definition's count: they leave the bound, and
|
|
227
|
+
// the exact passes never scan them (every per-site verdict is
|
|
228
|
+
// file-local). Orientation on django spent most of its time
|
|
229
|
+
// resolving the test suite's calls, which it then discarded.
|
|
230
|
+
const { isTestPath } = require('./shared');
|
|
231
|
+
const countableFile = (filePath) => {
|
|
232
|
+
if (!scopedPaths.has(filePath)) return false;
|
|
233
|
+
const fileEntry = index.files.get(filePath);
|
|
234
|
+
if (!fileEntry || fileEntry.isBundled) return false;
|
|
235
|
+
return !options.productionCallsOnly || !isTestPath(fileEntry.relativePath || filePath);
|
|
236
|
+
};
|
|
237
|
+
const countableFiles = new Set([...index.files.keys()].filter(countableFile));
|
|
238
|
+
// Call records per name in the countable files: what one exact
|
|
239
|
+
// refinement of that name scans (the HOT work unit, fix #382).
|
|
240
|
+
const recordsByName = new Map();
|
|
241
|
+
// The bound must count calls macro expansions generate (fix #374).
|
|
242
|
+
require('./rust-macro-expansion').materializeRustMacroCalls(index);
|
|
223
243
|
for (const [filePath, entry] of index.callsCache) {
|
|
224
|
-
if (!
|
|
244
|
+
if (!countableFiles.has(filePath)) continue;
|
|
225
245
|
const fileEntry = index.files.get(filePath);
|
|
226
|
-
if (fileEntry?.isBundled) continue;
|
|
227
246
|
if (!entry || !Array.isArray(entry.calls)) continue;
|
|
228
247
|
const seenInFile = new Set();
|
|
229
248
|
for (const c of entry.calls) {
|
|
230
249
|
if (!c || !c.name) continue;
|
|
250
|
+
recordsByName.set(c.name, (recordsByName.get(c.name) || 0) + 1);
|
|
251
|
+
if (c.resolvedName && c.resolvedName !== c.name) {
|
|
252
|
+
recordsByName.set(c.resolvedName, (recordsByName.get(c.resolvedName) || 0) + 1);
|
|
253
|
+
}
|
|
231
254
|
if (receiverProvablyExternal(filePath, fileEntry, c)) continue;
|
|
232
255
|
if (options.productionCallsOnly && inInlineTest(filePath, c.line)) continue;
|
|
233
256
|
// Distinct receiver types on one line can confirm DIFFERENT
|
|
@@ -252,6 +275,16 @@ function getStats(index, options = {}) {
|
|
|
252
275
|
}
|
|
253
276
|
const ownerOf = (symbol) => normalizeTypeName(symbol.className || symbol.receiver || '');
|
|
254
277
|
|
|
278
|
+
// One HOT entry per callable IDENTITY (fix #382). The caller engine
|
|
279
|
+
// closes a pinned definition over its identity group (overload
|
|
280
|
+
// signatures with their implementation, C/C++ prototypes with their
|
|
281
|
+
// definitions, C++ specializations with their primary template), so
|
|
282
|
+
// definitions whose closed groups are the same set answer the same
|
|
283
|
+
// query: HOT listed a Python @overload stub pair beside the
|
|
284
|
+
// implementation and each TS overload signature of `h` with one
|
|
285
|
+
// count. Members of one closed set collapse into one entry, shown at
|
|
286
|
+
// its first implementation (else its first declaration).
|
|
287
|
+
const { _closeCallableIdentityGroup } = require('./callers');
|
|
255
288
|
const candidates = [];
|
|
256
289
|
const seenDefinitions = new Set();
|
|
257
290
|
for (const [name, symbols] of index.symbols) {
|
|
@@ -262,6 +295,7 @@ function getStats(index, options = {}) {
|
|
|
262
295
|
FUNCTION_TYPES.has(symbol.type) &&
|
|
263
296
|
matchesReportingScope(index, symbol.relativePath, options));
|
|
264
297
|
const definers = new Set(callable.map(ownerOf).filter(Boolean));
|
|
298
|
+
const eligible = [];
|
|
265
299
|
for (const symbol of callable) {
|
|
266
300
|
let upper = untyped;
|
|
267
301
|
if (typedByClass) {
|
|
@@ -271,14 +305,6 @@ function getStats(index, options = {}) {
|
|
|
271
305
|
}
|
|
272
306
|
}
|
|
273
307
|
if (upper === 0) continue;
|
|
274
|
-
// Fair share of the name's ceiling: a name shared by 278
|
|
275
|
-
// methods cannot make all 278 hot, so a definition's likely
|
|
276
|
-
// count is nearer upper/definers than upper. Ordering by the
|
|
277
|
-
// share puts the genuinely hot definitions first; the early
|
|
278
|
-
// stop below still uses the exact remaining ceiling, so the
|
|
279
|
-
// exact answer is unchanged and only a bounded refinement
|
|
280
|
-
// (`maxRefine`) benefits from the order.
|
|
281
|
-
const share = upper / Math.max(1, callable.length);
|
|
282
308
|
if (index.files.get(symbol.file)?.isBundled) continue;
|
|
283
309
|
if (options.productionCallsOnly &&
|
|
284
310
|
(require('./shared').isTestPath(symbol.relativePath) ||
|
|
@@ -287,63 +313,159 @@ function getStats(index, options = {}) {
|
|
|
287
313
|
`${symbol.className || symbol.receiver || ''}:${symbol.params || ''}`;
|
|
288
314
|
if (seenDefinitions.has(identity)) continue;
|
|
289
315
|
seenDefinitions.add(identity);
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
316
|
+
eligible.push({ name, symbol, upper });
|
|
317
|
+
}
|
|
318
|
+
// Groups close only over a signature or a C++ template
|
|
319
|
+
// specialization; names with neither keep one entry per
|
|
320
|
+
// definition without asking.
|
|
321
|
+
const grouped = eligible.length > 1 && symbols.some(definition =>
|
|
322
|
+
definition.isSignature || definition.isSpecialization || definition.templateDependent);
|
|
323
|
+
if (!grouped) {
|
|
324
|
+
candidates.push(...eligible);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
// Entries whose closures reach each other are one entity (a C
|
|
328
|
+
// prototype closes over every definition of its linkage, each
|
|
329
|
+
// definition over the prototypes). The entity is counted through
|
|
330
|
+
// a member whose closure covers all of it (the implementation
|
|
331
|
+
// when it does, else the declaration), so the count is the
|
|
332
|
+
// entity's; without such a member, each distinct closure keeps
|
|
333
|
+
// its own entry.
|
|
334
|
+
const closures = eligible.map(entry =>
|
|
335
|
+
new Set(_closeCallableIdentityGroup(index, [entry.symbol], symbols)));
|
|
336
|
+
const parent = eligible.map((entry, i) => i);
|
|
337
|
+
const find = i => (parent[i] === i ? i : (parent[i] = find(parent[i])));
|
|
338
|
+
for (let i = 0; i < eligible.length; i++) {
|
|
339
|
+
for (let j = i + 1; j < eligible.length; j++) {
|
|
340
|
+
if (closures[i].has(eligible[j].symbol) || closures[j].has(eligible[i].symbol)) {
|
|
341
|
+
parent[find(j)] = find(i);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const components = new Map(); // root -> entry indexes in symbol order
|
|
346
|
+
for (let i = 0; i < eligible.length; i++) {
|
|
347
|
+
const root = find(i);
|
|
348
|
+
if (!components.has(root)) components.set(root, []);
|
|
349
|
+
components.get(root).push(i);
|
|
350
|
+
}
|
|
351
|
+
const position = new Map(symbols.map((definition, i) => [definition, i]));
|
|
352
|
+
for (const members of components.values()) {
|
|
353
|
+
const covering = members.filter(i =>
|
|
354
|
+
members.every(j => closures[i].has(eligible[j].symbol)));
|
|
355
|
+
const groups = covering.length > 0 ? [covering] : [...members.reduce((byKey, i) => {
|
|
356
|
+
const key = [...closures[i]].map(definition => position.get(definition))
|
|
357
|
+
.sort((a, b) => a - b).join(',');
|
|
358
|
+
if (!byKey.has(key)) byKey.set(key, []);
|
|
359
|
+
byKey.get(key).push(i);
|
|
360
|
+
return byKey;
|
|
361
|
+
}, new Map()).values()];
|
|
362
|
+
for (const group of groups) {
|
|
363
|
+
const shown = group.find(i => !eligible[i].symbol.isSignature) ?? group[0];
|
|
364
|
+
candidates.push({
|
|
365
|
+
...eligible[shown],
|
|
366
|
+
upper: Math.max(...(covering.length > 0 ? members : group).map(i => eligible[i].upper)),
|
|
367
|
+
});
|
|
300
368
|
}
|
|
301
|
-
candidates.push({ name, symbol, upper, share });
|
|
302
369
|
}
|
|
303
370
|
}
|
|
304
371
|
const maxRefine = Number.isInteger(options.maxRefine) && options.maxRefine > 0
|
|
305
372
|
? options.maxRefine : Infinity;
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
373
|
+
// Refinement work is bounded by call records, not wall time, so the
|
|
374
|
+
// answer is the same on every run and machine (fix #382). One
|
|
375
|
+
// refinement scans the call records of its name in the countable
|
|
376
|
+
// files; `workBudget` caps their sum. Exact answers on real projects
|
|
377
|
+
// stay far below it; a very large repository (kubernetes: ~68M
|
|
378
|
+
// records under its candidate ceilings) stops and says so.
|
|
379
|
+
const workBudget = Number.isFinite(options.workBudget) && options.workBudget > 0
|
|
380
|
+
? options.workBudget : STATS_HOT_WORK_BUDGET;
|
|
381
|
+
const workOf = candidate => recordsByName.get(candidate.name) || 0;
|
|
382
|
+
// Ceilings in descending order: the early stop fires as soon as no
|
|
383
|
+
// unseen ceiling can reach the current Nth count, so exactly the
|
|
384
|
+
// candidates able to enter the top N are refined.
|
|
309
385
|
candidates.sort((a, b) =>
|
|
310
|
-
(maxRefine !== Infinity ? (b.share - a.share) : 0) ||
|
|
311
386
|
(b.upper - a.upper) ||
|
|
312
387
|
codeUnitCompare(a.symbol.relativePath, b.symbol.relativePath) ||
|
|
313
388
|
(a.symbol.startLine || 0) - (b.symbol.startLine || 0));
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
389
|
+
// Ceiling order is the cheapest exact walk. When the budget binds
|
|
390
|
+
// first, the ceiling walk has spent it on the definitions of a few
|
|
391
|
+
// names shared by hundreds of types (every `String`, `Name`,
|
|
392
|
+
// generated `DeepCopyInto` carries its whole name's ceiling), so a
|
|
393
|
+
// second budget walks the rest in fair-share order (ceiling per
|
|
394
|
+
// same-name candidate) and the approximate list spans many names.
|
|
395
|
+
// Exactness is proven the same way in both walks: no unrefined
|
|
396
|
+
// ceiling can reach the current Nth count.
|
|
397
|
+
const fairShareOrder = () => {
|
|
398
|
+
const definitionsOf = new Map();
|
|
399
|
+
for (const candidate of candidates) {
|
|
400
|
+
definitionsOf.set(candidate.name, (definitionsOf.get(candidate.name) || 0) + 1);
|
|
401
|
+
}
|
|
402
|
+
return candidates.map((candidate, i) => i).sort((a, b) =>
|
|
403
|
+
(candidates[b].upper / definitionsOf.get(candidates[b].name)) -
|
|
404
|
+
(candidates[a].upper / definitionsOf.get(candidates[a].name)) || a - b);
|
|
405
|
+
};
|
|
406
|
+
const completionBudget = Number.isFinite(workBudget)
|
|
407
|
+
? Math.floor(workBudget * HOT_COMPLETION_BUDGET_SHARE) : 0;
|
|
320
408
|
|
|
321
409
|
const hotList = [];
|
|
322
410
|
const { findCallers } = require('./callers');
|
|
323
411
|
const scopedCallerQuery = !!(options.file || options.in ||
|
|
324
412
|
(options.exclude && options.exclude.length > 0));
|
|
325
413
|
let refined = 0;
|
|
414
|
+
let work = 0;
|
|
415
|
+
// One test-path verdict per caller file (fix #382: asked per caller
|
|
416
|
+
// of every refinement).
|
|
417
|
+
const testPathMemo = new Map();
|
|
418
|
+
const testPath = (file) => {
|
|
419
|
+
let verdict = testPathMemo.get(file);
|
|
420
|
+
if (verdict === undefined) {
|
|
421
|
+
verdict = require('./shared').isTestPath(file);
|
|
422
|
+
testPathMemo.set(file, verdict);
|
|
423
|
+
}
|
|
424
|
+
return verdict;
|
|
425
|
+
};
|
|
326
426
|
let budgetExhausted = false;
|
|
427
|
+
const topCounts = []; // the `top` largest counts so far, descending
|
|
428
|
+
const done = new Uint8Array(candidates.length);
|
|
429
|
+
let ceilingHead = 0; // first candidate in ceiling order not refined
|
|
327
430
|
// One operation scope for the whole refinement loop (fix #340): the
|
|
328
431
|
// per-file derivations findCallers builds are shared across candidates.
|
|
329
432
|
if (top > 0) index._beginOp();
|
|
330
433
|
try {
|
|
331
434
|
if (top > 0) {
|
|
332
|
-
|
|
435
|
+
let order = null; // null = ceiling order
|
|
436
|
+
let limit = workBudget;
|
|
437
|
+
let exact = false;
|
|
438
|
+
for (let step = 0; step < candidates.length && !exact;) {
|
|
439
|
+
const candidateIndex = order ? order[step] : step;
|
|
440
|
+
step++;
|
|
441
|
+
if (done[candidateIndex]) continue;
|
|
333
442
|
if (refined >= maxRefine) { budgetExhausted = true; break; }
|
|
443
|
+
if (work >= limit) {
|
|
444
|
+
budgetExhausted = true;
|
|
445
|
+
if (order || completionBudget === 0) break;
|
|
446
|
+
// Ceiling budget spent: complete in fair-share order.
|
|
447
|
+
order = fairShareOrder();
|
|
448
|
+
limit = work + completionBudget;
|
|
449
|
+
step = 0;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
334
452
|
const { name, symbol } = candidates[candidateIndex];
|
|
335
|
-
const
|
|
453
|
+
const callers = findCallers(index, name, {
|
|
336
454
|
targetDefinitions: [symbol],
|
|
337
455
|
includeTests: true,
|
|
338
456
|
collectAccount: true,
|
|
457
|
+
...(countableFiles.size < index.files.size && { candidateFiles: countableFiles }),
|
|
339
458
|
});
|
|
340
459
|
refined++;
|
|
341
|
-
|
|
460
|
+
work += workOf(candidates[candidateIndex]);
|
|
461
|
+
done[candidateIndex] = 1;
|
|
462
|
+
while (ceilingHead < candidates.length && done[ceilingHead]) ceilingHead++;
|
|
463
|
+
const count = callers.filter(caller =>
|
|
342
464
|
caller.tier !== 'unverified' &&
|
|
343
465
|
(!scopedCallerQuery || scopedPaths.has(caller.file)) &&
|
|
344
466
|
!index.files.get(caller.file)?.isBundled &&
|
|
345
467
|
(!options.productionCallsOnly ||
|
|
346
|
-
(!
|
|
468
|
+
(!testPath(caller.relativePath || caller.file) &&
|
|
347
469
|
!inInlineTest(caller.file, caller.line)))).length;
|
|
348
470
|
if (count > 0) {
|
|
349
471
|
const owner = symbol.className ||
|
|
@@ -356,16 +478,22 @@ function getStats(index, options = {}) {
|
|
|
356
478
|
callCount: count,
|
|
357
479
|
evidence: 'confirmed-callers',
|
|
358
480
|
});
|
|
481
|
+
// The stop test needs only the top-th largest count;
|
|
482
|
+
// the full list is sorted once below (fix #365: sorting
|
|
483
|
+
// it per refinement was quadratic in refinements).
|
|
484
|
+
let at = topCounts.length;
|
|
485
|
+
while (at > 0 && topCounts[at - 1] < count) at--;
|
|
486
|
+
if (at < top) {
|
|
487
|
+
topCounts.splice(at, 0, count);
|
|
488
|
+
if (topCounts.length > top) topCounts.pop();
|
|
489
|
+
}
|
|
359
490
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (hotList.length >= top) {
|
|
365
|
-
const threshold = hotList[top - 1].callCount;
|
|
366
|
-
if (remainingUpper[candidateIndex + 1] < threshold) break;
|
|
367
|
-
}
|
|
491
|
+
// Exact once no unrefined ceiling can reach the Nth count
|
|
492
|
+
// (the highest unrefined ceiling is the ceiling-order head).
|
|
493
|
+
if (topCounts.length >= top && (ceilingHead >= candidates.length ||
|
|
494
|
+
candidates[ceilingHead].upper < topCounts[top - 1])) exact = true;
|
|
368
495
|
}
|
|
496
|
+
if (exact) budgetExhausted = false;
|
|
369
497
|
}
|
|
370
498
|
} finally { if (top > 0) index._endOp(); }
|
|
371
499
|
|
|
@@ -382,7 +510,10 @@ function getStats(index, options = {}) {
|
|
|
382
510
|
totalKind: refined === candidates.length ? 'confirmed' : 'raw-call-candidates',
|
|
383
511
|
refined,
|
|
384
512
|
items: hotList.slice(0, top),
|
|
385
|
-
...(budgetExhausted && {
|
|
513
|
+
...(budgetExhausted && {
|
|
514
|
+
budgetExhausted: true,
|
|
515
|
+
...(refined >= maxRefine ? { maxRefine } : { workBudget, work }),
|
|
516
|
+
}),
|
|
386
517
|
note: refined === candidates.length
|
|
387
518
|
? 'Counts are confirmed caller-engine edges pinned to each displayed definition; unverified dispatch is excluded.'
|
|
388
519
|
: `Displayed counts are exact confirmed caller-engine edges; ${candidates.length} raw candidates were bounded and ${refined} required exact refinement.`,
|
|
@@ -700,6 +831,37 @@ function doctor(index, options = {}) {
|
|
|
700
831
|
}
|
|
701
832
|
fileCounts.skipped = blindSpots.skippedSources.fileCount;
|
|
702
833
|
|
|
834
|
+
// C/C++ token-pasting macro invocations whose expansion cannot be
|
|
835
|
+
// computed (fix #362). Deep mode only: it reads the calls of every
|
|
836
|
+
// C/C++ file.
|
|
837
|
+
if (options.deep && [...index.files.values()].some(fe => langTraits(fe.language)?.textualIncludes)) {
|
|
838
|
+
const { macroExpansionSummary } = require('./macro-expansion');
|
|
839
|
+
const summary = macroExpansionSummary(index);
|
|
840
|
+
blindSpots.macroPasteDispatch = {
|
|
841
|
+
count: summary.blind.count,
|
|
842
|
+
fileCount: summary.blind.fileCount,
|
|
843
|
+
files: summary.blind.files,
|
|
844
|
+
expandedSites: summary.sites,
|
|
845
|
+
unexpandedPatterns: summary.patterns.length,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Rust macro_rules! invocations of project macros that could not be
|
|
850
|
+
// expanded (fix #374): read from the persisted per-file records.
|
|
851
|
+
if ([...index.files.values()].some(fe => fe.rustMacroExpansion)) {
|
|
852
|
+
const { rustMacroExpansionSummary } = require('./rust-macro-expansion');
|
|
853
|
+
const summary = rustMacroExpansionSummary(index);
|
|
854
|
+
if (summary.sites > 0 || summary.blind.count > 0) {
|
|
855
|
+
blindSpots.macroRulesExpansion = {
|
|
856
|
+
count: summary.blind.count,
|
|
857
|
+
fileCount: summary.blind.fileCount,
|
|
858
|
+
files: summary.blind.files,
|
|
859
|
+
expandedSites: summary.sites,
|
|
860
|
+
...(summary.blind.reasons && { reasons: summary.blind.reasons }),
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
703
865
|
// Evidence profile — sampled only in deep mode. This is deliberately NOT
|
|
704
866
|
// called "accuracy" or "coverage": it describes how UCN classified edges
|
|
705
867
|
// it found. Compiler/LSP oracle evaluation is the accuracy measurement.
|
|
@@ -726,6 +888,12 @@ function doctor(index, options = {}) {
|
|
|
726
888
|
if (blindSpots.computedDispatch.count > 0) {
|
|
727
889
|
blindSignals.push(`${blindSpots.computedDispatch.count} computed dispatch call(s) in ${blindSpots.computedDispatch.fileCount} file(s)`);
|
|
728
890
|
}
|
|
891
|
+
if (blindSpots.macroPasteDispatch?.count > 0) {
|
|
892
|
+
blindSignals.push(`${blindSpots.macroPasteDispatch.count} token-pasting macro dispatch invocation(s) not expanded in ${blindSpots.macroPasteDispatch.fileCount} file(s)`);
|
|
893
|
+
}
|
|
894
|
+
if (blindSpots.macroRulesExpansion?.count > 0) {
|
|
895
|
+
blindSignals.push(`${blindSpots.macroRulesExpansion.count} macro_rules! invocation(s) not expanded in ${blindSpots.macroRulesExpansion.fileCount} file(s)`);
|
|
896
|
+
}
|
|
729
897
|
if (blindSpots.dynamicImports.count > 0) blindSignals.push(`${blindSpots.dynamicImports.count} ${importBlindspotLabel} in ${blindSpots.dynamicImports.fileCount} file(s)`);
|
|
730
898
|
if (blindSpots.unsupportedSources.count > 0) {
|
|
731
899
|
const mix = Object.entries(blindSpots.unsupportedSources.languages)
|
|
@@ -940,11 +1108,20 @@ function computeEvidenceProfile(index, { sampleSize, matchInFilter }) {
|
|
|
940
1108
|
* trust verdict. Composes existing engine reads; counts and pointers only
|
|
941
1109
|
* (no caller claims, so no account — the toc/stats category).
|
|
942
1110
|
*/
|
|
943
|
-
// Orientation
|
|
944
|
-
//
|
|
945
|
-
//
|
|
946
|
-
//
|
|
947
|
-
|
|
1111
|
+
// Orientation's HOT list is exact unless exact refinement would examine more
|
|
1112
|
+
// call records than this (fix #382; a count of 10000 refinements before, fix
|
|
1113
|
+
// #340/#365). A production ranking charges its bound only with production
|
|
1114
|
+
// call records and scans only production files, so exact lists on real
|
|
1115
|
+
// projects examine well under a million records (django ~450k, tokio ~500k).
|
|
1116
|
+
// The budget binds only on very large repositories (kubernetes), where the
|
|
1117
|
+
// header says the ranking is approximate.
|
|
1118
|
+
const ORIENT_HOT_WORK_BUDGET = 1500000;
|
|
1119
|
+
// `repo --sections=stats --hot` ranks by all callers and is the explicit
|
|
1120
|
+
// request for an exact list: a larger budget, disclosed the same way.
|
|
1121
|
+
const STATS_HOT_WORK_BUDGET = 8000000;
|
|
1122
|
+
// When a ceiling-order walk exhausts its budget, this share of it again is
|
|
1123
|
+
// spent completing the ranking in fair-share order.
|
|
1124
|
+
const HOT_COMPLETION_BUDGET_SHARE = 0.5;
|
|
948
1125
|
|
|
949
1126
|
function orient(index, options = {}) {
|
|
950
1127
|
const top = options.top || 8;
|
|
@@ -968,8 +1145,10 @@ function orient(index, options = {}) {
|
|
|
968
1145
|
// actually contains production files. In an all-test repository it
|
|
969
1146
|
// would erase the raw ranking that orient promises as its fallback.
|
|
970
1147
|
productionCallsOnly: options.includeTests !== true && hasProductionFiles,
|
|
971
|
-
|
|
972
|
-
|
|
1148
|
+
...(Number.isInteger(options.hotRefineBudget) && options.hotRefineBudget > 0 &&
|
|
1149
|
+
{ maxRefine: options.hotRefineBudget }),
|
|
1150
|
+
workBudget: Number.isFinite(options.hotWorkBudget) && options.hotWorkBudget > 0
|
|
1151
|
+
? options.hotWorkBudget : ORIENT_HOT_WORK_BUDGET,
|
|
973
1152
|
});
|
|
974
1153
|
const health = doctor(index, scope);
|
|
975
1154
|
|
|
@@ -1053,7 +1232,10 @@ function orient(index, options = {}) {
|
|
|
1053
1232
|
totalKind: stats.hot?.totalKind || 'confirmed',
|
|
1054
1233
|
refined: stats.hot?.refined ?? 0,
|
|
1055
1234
|
...(stats.hot?.budgetExhausted && {
|
|
1056
|
-
budgetExhausted: true,
|
|
1235
|
+
budgetExhausted: true,
|
|
1236
|
+
...(stats.hot.maxRefine !== undefined
|
|
1237
|
+
? { maxRefine: stats.hot.maxRefine }
|
|
1238
|
+
: { workBudget: stats.hot.workBudget, work: stats.hot.work }),
|
|
1057
1239
|
}),
|
|
1058
1240
|
top,
|
|
1059
1241
|
production,
|