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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Optional compiler/LSP semantic-provider contract.
|
|
5
|
+
*
|
|
6
|
+
* Portable AST analysis remains available without a provider. Hybrid mode
|
|
7
|
+
* calls this interface explicitly and must surface `unavailable`,
|
|
8
|
+
* `unsupported`, or `failed`; callers are never allowed to silently relabel
|
|
9
|
+
* an AST fallback as provider-backed evidence.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const PROVIDER_OPERATIONS = Object.freeze([
|
|
13
|
+
'definitions',
|
|
14
|
+
'references',
|
|
15
|
+
'callers',
|
|
16
|
+
'callees',
|
|
17
|
+
'types',
|
|
18
|
+
'diagnostics',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
function createSemanticProvider(spec) {
|
|
22
|
+
if (!spec?.id) throw new Error('Semantic provider id is required');
|
|
23
|
+
if (!Array.isArray(spec.languages) || spec.languages.length === 0) {
|
|
24
|
+
throw new Error(`${spec.id}: at least one language is required`);
|
|
25
|
+
}
|
|
26
|
+
const operations = {};
|
|
27
|
+
for (const operation of PROVIDER_OPERATIONS) {
|
|
28
|
+
if (typeof spec[operation] === 'function') operations[operation] = spec[operation];
|
|
29
|
+
}
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
id: spec.id,
|
|
32
|
+
languages: Object.freeze([...spec.languages]),
|
|
33
|
+
operations: Object.freeze(Object.keys(operations)),
|
|
34
|
+
prepare: typeof spec.prepare === 'function' ? spec.prepare : async () => null,
|
|
35
|
+
close: typeof spec.close === 'function' ? spec.close : async () => {},
|
|
36
|
+
...operations,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function validateSemanticProvider(provider) {
|
|
41
|
+
const failures = [];
|
|
42
|
+
if (!provider?.id) failures.push('provider id is required');
|
|
43
|
+
if (!Array.isArray(provider?.languages) || provider.languages.length === 0) {
|
|
44
|
+
failures.push(`${provider?.id || 'provider'}: languages are required`);
|
|
45
|
+
}
|
|
46
|
+
if (!Array.isArray(provider?.operations)) {
|
|
47
|
+
failures.push(`${provider?.id || 'provider'}: operations are required`);
|
|
48
|
+
} else {
|
|
49
|
+
for (const operation of provider.operations) {
|
|
50
|
+
if (!PROVIDER_OPERATIONS.includes(operation)) {
|
|
51
|
+
failures.push(`${provider.id}: unknown operation ${operation}`);
|
|
52
|
+
} else if (typeof provider[operation] !== 'function') {
|
|
53
|
+
failures.push(`${provider.id}: ${operation} is not callable`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (typeof provider?.prepare !== 'function') {
|
|
58
|
+
failures.push(`${provider?.id || 'provider'}: prepare() is required`);
|
|
59
|
+
}
|
|
60
|
+
if (typeof provider?.close !== 'function') {
|
|
61
|
+
failures.push(`${provider?.id || 'provider'}: close() is required`);
|
|
62
|
+
}
|
|
63
|
+
return failures;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function invokeSemanticProvider(provider, operation, request) {
|
|
67
|
+
if (!provider) {
|
|
68
|
+
return {
|
|
69
|
+
status: 'unavailable',
|
|
70
|
+
provider: null,
|
|
71
|
+
operation,
|
|
72
|
+
reason: 'no semantic provider configured',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (!PROVIDER_OPERATIONS.includes(operation)) {
|
|
76
|
+
throw new Error(`Unknown semantic provider operation: ${operation}`);
|
|
77
|
+
}
|
|
78
|
+
if (!provider.operations.includes(operation) ||
|
|
79
|
+
typeof provider[operation] !== 'function') {
|
|
80
|
+
return {
|
|
81
|
+
status: 'unsupported',
|
|
82
|
+
provider: provider.id,
|
|
83
|
+
operation,
|
|
84
|
+
reason: `${provider.id} does not implement ${operation}`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const data = await provider[operation](request);
|
|
89
|
+
return {
|
|
90
|
+
status: 'ok',
|
|
91
|
+
provider: provider.id,
|
|
92
|
+
operation,
|
|
93
|
+
data,
|
|
94
|
+
};
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return {
|
|
97
|
+
status: 'failed',
|
|
98
|
+
provider: provider.id,
|
|
99
|
+
operation,
|
|
100
|
+
reason: error.message || String(error),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
PROVIDER_OPERATIONS,
|
|
107
|
+
createSemanticProvider,
|
|
108
|
+
validateSemanticProvider,
|
|
109
|
+
invokeSemanticProvider,
|
|
110
|
+
};
|
package/core/stacktrace.js
CHANGED
|
@@ -276,6 +276,15 @@ function parseStackTrace(index, stackText) {
|
|
|
276
276
|
// Stack trace patterns for different languages/runtimes
|
|
277
277
|
// Order matters - more specific patterns first
|
|
278
278
|
const patterns = [
|
|
279
|
+
// .NET: "at Namespace.Type.Method(...) in /src/File.cs:line 42"
|
|
280
|
+
// and Windows drive-letter paths.
|
|
281
|
+
{ regex: /at\s+([^\s(]+)(?:\([^)]*\))?\s+in\s+(.+?):line\s+(\d+)/, extract: (m) => ({ funcName: m[1].split('.').pop(), file: m[2], line: parseInt(m[3]), col: null }) },
|
|
282
|
+
// Native sanitizers: "#0 0x... in ns::func /src/file.cpp:42:7"
|
|
283
|
+
{ regex: /#\d+\s+(?:0x[0-9a-f]+\s+)?in\s+([^\s(]+)(?:\([^)]*\))?\s+(.+\.(?:c|cc|cpp|cxx|h|hpp)):(\d+)(?::(\d+))?/i, extract: (m) => ({ funcName: m[1].split('::').pop(), file: m[2], line: parseInt(m[3]), col: m[4] ? parseInt(m[4]) : null }) },
|
|
284
|
+
// GDB/lldb: "#0 ns::func (...) at src/file.cpp:42"
|
|
285
|
+
{ regex: /#\d+\s+(?:0x[0-9a-f]+\s+in\s+)?([^\s(]+).*?\bat\s+(.+\.(?:c|cc|cpp|cxx|h|hpp)):(\d+)/i, extract: (m) => ({ funcName: m[1].split('::').pop(), file: m[2], line: parseInt(m[3]), col: null }) },
|
|
286
|
+
// MSVC diagnostics and native exception locations: "src\\file.cpp(42)"
|
|
287
|
+
{ regex: /(.+\.(?:c|cc|cpp|cxx|h|hpp|cs))\((\d+)(?:,(\d+))?\)/i, extract: (m) => ({ file: m[1], line: parseInt(m[2]), col: m[3] ? parseInt(m[3]) : null, funcName: null }) },
|
|
279
288
|
// Rust pre-1.65 panic header: "panicked at 'message', src/main.rs:150:9"
|
|
280
289
|
// MUST precede the Node pattern — its [^():]+ file group has no
|
|
281
290
|
// space/comma guard, so the quoted message glued into the file field
|
|
@@ -314,12 +323,22 @@ function parseStackTrace(index, stackText) {
|
|
|
314
323
|
|
|
315
324
|
// Track Go function names that appear on a line before the file:line
|
|
316
325
|
let pendingGoFuncName = null;
|
|
326
|
+
// Rust backtraces likewise split `N: crate::path` and `at file.rs:L:C`
|
|
327
|
+
// across two physical lines.
|
|
328
|
+
let pendingRustFuncName = null;
|
|
317
329
|
let skippedFrames = 0;
|
|
318
330
|
|
|
319
331
|
for (const line of lines) {
|
|
320
332
|
const trimmed = line.trim();
|
|
321
333
|
if (!trimmed) continue;
|
|
322
334
|
|
|
335
|
+
const rustName = trimmed.match(/^\d+:\s+(.+)$/);
|
|
336
|
+
if (rustName && !/\.rs:\d+/.test(rustName[1])) {
|
|
337
|
+
pendingRustFuncName = rustName[1].trim();
|
|
338
|
+
pendingGoFuncName = null;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
323
342
|
// Try each pattern until one matches
|
|
324
343
|
let matched = false;
|
|
325
344
|
for (const pattern of patterns) {
|
|
@@ -349,7 +368,12 @@ function parseStackTrace(index, stackText) {
|
|
|
349
368
|
if (!extracted.funcName && pendingGoFuncName) {
|
|
350
369
|
extracted.funcName = pendingGoFuncName;
|
|
351
370
|
}
|
|
371
|
+
if (!extracted.funcName && pendingRustFuncName &&
|
|
372
|
+
/\.rs$/i.test(extracted.file)) {
|
|
373
|
+
extracted.funcName = pendingRustFuncName;
|
|
374
|
+
}
|
|
352
375
|
pendingGoFuncName = null;
|
|
376
|
+
pendingRustFuncName = null;
|
|
353
377
|
frames.push(createStackFrame(
|
|
354
378
|
index,
|
|
355
379
|
extracted.file,
|
|
@@ -370,6 +394,7 @@ function parseStackTrace(index, stackText) {
|
|
|
370
394
|
// unfound-file frames, which render found=false).
|
|
371
395
|
if (/^at\s/.test(trimmed)) skippedFrames++;
|
|
372
396
|
pendingGoFuncName = null; // Reset if line doesn't match any pattern
|
|
397
|
+
pendingRustFuncName = null;
|
|
373
398
|
}
|
|
374
399
|
}
|
|
375
400
|
|
package/core/tracing.js
CHANGED
|
@@ -23,7 +23,7 @@ const path = require('path');
|
|
|
23
23
|
const { escapeRegExp, codeUnitCompare, inlineTestRanges, lineInRanges, classDispatchNames } = require('./shared');
|
|
24
24
|
const { isTestFile } = require('./discovery');
|
|
25
25
|
const { getCachedCalls } = require('./callers');
|
|
26
|
-
const {
|
|
26
|
+
const { getLanguageAdapter } = require('../languages');
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Contract-mode caller expansion for the tree commands. Memoizes the full
|
|
@@ -86,20 +86,23 @@ function _aggregateExcluded(treeAccount, raw) {
|
|
|
86
86
|
function _resolveCallerEntries(index, callers, exclude) {
|
|
87
87
|
const uniqueCallers = new Map();
|
|
88
88
|
for (const c of callers) {
|
|
89
|
-
if (!c.callerName) continue; // skip module-level code
|
|
90
89
|
if (exclude.length > 0 && !index.matchesFilters(c.relativePath, { exclude })) continue;
|
|
91
|
-
const
|
|
90
|
+
const hasNamedCaller = !!c.callerName;
|
|
91
|
+
const callerName = c.callerName || '(module/anonymous scope)';
|
|
92
|
+
const callerStartLine = hasNamedCaller ? c.callerStartLine : c.line;
|
|
93
|
+
const callerKey = hasNamedCaller && c.callerStartLine
|
|
92
94
|
? `${c.callerFile}:${c.callerStartLine}`
|
|
93
|
-
: `${c.callerFile}:${c.
|
|
95
|
+
: `${c.callerFile}:${callerName}:${callerStartLine || c.line || 0}`;
|
|
94
96
|
if (!uniqueCallers.has(callerKey)) {
|
|
95
97
|
uniqueCallers.set(callerKey, {
|
|
96
|
-
name:
|
|
98
|
+
name: callerName,
|
|
97
99
|
file: c.callerFile,
|
|
98
100
|
relativePath: c.relativePath,
|
|
99
|
-
startLine:
|
|
100
|
-
endLine: c.callerEndLine,
|
|
101
|
+
startLine: callerStartLine,
|
|
102
|
+
endLine: hasNamedCaller ? c.callerEndLine : c.line,
|
|
101
103
|
callSites: 1,
|
|
102
104
|
reason: c.reason,
|
|
105
|
+
syntheticScope: !hasNamedCaller,
|
|
103
106
|
});
|
|
104
107
|
} else {
|
|
105
108
|
uniqueCallers.get(callerKey).callSites++;
|
|
@@ -121,6 +124,7 @@ function _resolveCallerEntries(index, callers, exclude) {
|
|
|
121
124
|
type: 'function'
|
|
122
125
|
};
|
|
123
126
|
}
|
|
127
|
+
if (caller.syntheticScope) callerDef.syntheticScope = true;
|
|
124
128
|
callerEntries.push({ def: callerDef, callSites: caller.callSites, reason: caller.reason });
|
|
125
129
|
}
|
|
126
130
|
|
|
@@ -195,6 +199,11 @@ function trace(index, name, options = {}) {
|
|
|
195
199
|
children: []
|
|
196
200
|
};
|
|
197
201
|
|
|
202
|
+
// A depth frontier is a node label, not a partially analyzed node.
|
|
203
|
+
// Do not expose only its unverified callee band while suppressing its
|
|
204
|
+
// confirmed children; both evidence tiers expand at the same depth.
|
|
205
|
+
if (currentDepth >= maxDepth) return node;
|
|
206
|
+
|
|
198
207
|
if (dir === 'down' || dir === 'both') {
|
|
199
208
|
let callees = calleeCache.get(key);
|
|
200
209
|
if (!callees) {
|
|
@@ -347,15 +356,18 @@ function blast(index, name, options = {}) {
|
|
|
347
356
|
let maxDepthReached = 0;
|
|
348
357
|
let rootRaw = null;
|
|
349
358
|
let rootFiltered = 0;
|
|
359
|
+
let rootSelfRecursive = false;
|
|
350
360
|
const treeAccount = {
|
|
351
361
|
nodesExpanded: 0,
|
|
352
362
|
confirmedEdges: 0,
|
|
363
|
+
recursiveEdges: 0,
|
|
353
364
|
unverifiedEdges: 0,
|
|
354
365
|
unverifiedByReason: {},
|
|
355
366
|
excludedTotal: 0,
|
|
356
367
|
excludedByReason: {},
|
|
357
368
|
filteredEdges: 0,
|
|
358
369
|
depthLimitNodes: 0,
|
|
370
|
+
truncatedChildren: 0,
|
|
359
371
|
};
|
|
360
372
|
|
|
361
373
|
const buildCallerTree = (funcDef, currentDepth, chainUnverified) => {
|
|
@@ -407,6 +419,15 @@ function blast(index, name, options = {}) {
|
|
|
407
419
|
treeAccount.filteredEdges += before - callers.length;
|
|
408
420
|
if (currentDepth === 0) rootFiltered += before - callers.length;
|
|
409
421
|
}
|
|
422
|
+
const recursiveCallers = callers.filter(c =>
|
|
423
|
+
c.callerFile === funcDef.file &&
|
|
424
|
+
c.callerStartLine === funcDef.startLine);
|
|
425
|
+
if (recursiveCallers.length > 0) {
|
|
426
|
+
callers = callers.filter(c => !recursiveCallers.includes(c));
|
|
427
|
+
node.selfRecursive = true;
|
|
428
|
+
treeAccount.recursiveEdges += recursiveCallers.length;
|
|
429
|
+
if (currentDepth === 0) rootSelfRecursive = true;
|
|
430
|
+
}
|
|
410
431
|
treeAccount.confirmedEdges += callers.length;
|
|
411
432
|
if (collect && !chainUnverified) {
|
|
412
433
|
for (const c of callers) collect.onConfirmed?.(funcDef, c);
|
|
@@ -445,6 +466,7 @@ function blast(index, name, options = {}) {
|
|
|
445
466
|
|
|
446
467
|
if (callerEntries.length > maxChildren) {
|
|
447
468
|
node.truncatedChildren = callerEntries.length - maxChildren;
|
|
469
|
+
treeAccount.truncatedChildren += node.truncatedChildren;
|
|
448
470
|
// Count truncated callers in summary
|
|
449
471
|
for (const { def: cDef } of callerEntries.slice(maxChildren)) {
|
|
450
472
|
const tKey = `${cDef.file}:${cDef.startLine}`;
|
|
@@ -536,6 +558,7 @@ function blast(index, name, options = {}) {
|
|
|
536
558
|
maxDepthReached,
|
|
537
559
|
unverifiedEdges: treeAccount.unverifiedEdges,
|
|
538
560
|
...(expandUnverified && { possiblyAffected: possiblyAffectedSet.size }),
|
|
561
|
+
...(rootSelfRecursive && { selfRecursive: true }),
|
|
539
562
|
},
|
|
540
563
|
warnings: warnings.length > 0 ? warnings : undefined
|
|
541
564
|
};
|
|
@@ -581,6 +604,7 @@ function reverseTrace(index, name, options = {}) {
|
|
|
581
604
|
const treeAccount = {
|
|
582
605
|
nodesExpanded: 0,
|
|
583
606
|
confirmedEdges: 0,
|
|
607
|
+
recursiveEdges: 0,
|
|
584
608
|
unverifiedEdges: 0,
|
|
585
609
|
unverifiedByReason: {},
|
|
586
610
|
excludedTotal: 0,
|
|
@@ -600,7 +624,14 @@ function reverseTrace(index, name, options = {}) {
|
|
|
600
624
|
unv = unv.filter(c => index.matchesFilters(c.relativePath, { exclude }));
|
|
601
625
|
if (isExpansion) treeAccount.filteredEdges += before - conf.length - unv.length;
|
|
602
626
|
}
|
|
603
|
-
|
|
627
|
+
const recursive = conf.filter(c =>
|
|
628
|
+
c.callerFile === funcDef.file &&
|
|
629
|
+
c.callerStartLine === funcDef.startLine);
|
|
630
|
+
if (recursive.length > 0) {
|
|
631
|
+
conf = conf.filter(c => !recursive.includes(c));
|
|
632
|
+
if (isExpansion) treeAccount.recursiveEdges += recursive.length;
|
|
633
|
+
}
|
|
634
|
+
return { confirmed: conf, unverified: unv, recursive, raw };
|
|
604
635
|
};
|
|
605
636
|
|
|
606
637
|
const buildCallerTree = (funcDef, currentDepth, chainUnverified) => {
|
|
@@ -629,7 +660,8 @@ function reverseTrace(index, name, options = {}) {
|
|
|
629
660
|
};
|
|
630
661
|
|
|
631
662
|
if (currentDepth < maxDepth) {
|
|
632
|
-
const { confirmed, unverified, raw } = nodeCallers(funcDef, true);
|
|
663
|
+
const { confirmed, unverified, recursive, raw } = nodeCallers(funcDef, true);
|
|
664
|
+
if (recursive.length > 0) node.selfRecursive = true;
|
|
633
665
|
treeAccount.nodesExpanded++;
|
|
634
666
|
_aggregateExcluded(treeAccount, raw);
|
|
635
667
|
if (currentDepth === 0) {
|
|
@@ -666,7 +698,8 @@ function reverseTrace(index, name, options = {}) {
|
|
|
666
698
|
const cKey = `${cDef.file}:${cDef.startLine}`;
|
|
667
699
|
if (!visited.has(cKey)) {
|
|
668
700
|
const tiers = nodeCallers(cDef, false);
|
|
669
|
-
if (tiers.confirmed.length === 0 && tiers.unverified.length === 0
|
|
701
|
+
if (tiers.confirmed.length === 0 && tiers.unverified.length === 0 &&
|
|
702
|
+
tiers.recursive.length === 0) {
|
|
670
703
|
entryPoints.push({ name: cDef.name, file: cDef.relativePath || path.relative(index.root, cDef.file), line: cDef.startLine });
|
|
671
704
|
}
|
|
672
705
|
}
|
|
@@ -688,9 +721,11 @@ function reverseTrace(index, name, options = {}) {
|
|
|
688
721
|
// Entry point only when BOTH tiers are empty; unverified-only
|
|
689
722
|
// nodes are visibly not-confirmed instead.
|
|
690
723
|
if (callerEntries.length === 0 && currentDepth > 0) {
|
|
691
|
-
if (unverified.length === 0) {
|
|
724
|
+
if (unverified.length === 0 && recursive.length === 0) {
|
|
692
725
|
node.entryPoint = true;
|
|
693
726
|
entryPoints.push({ name: funcDef.name, file: funcDef.relativePath, line: funcDef.startLine });
|
|
727
|
+
} else if (recursive.length > 0) {
|
|
728
|
+
node.selfRecursive = true;
|
|
694
729
|
} else {
|
|
695
730
|
node.unverifiedCallerCount = unverified.length;
|
|
696
731
|
}
|
|
@@ -699,10 +734,12 @@ function reverseTrace(index, name, options = {}) {
|
|
|
699
734
|
// At depth limit: check if this node is an entry point
|
|
700
735
|
treeAccount.depthLimitNodes++;
|
|
701
736
|
const tiers = nodeCallers(funcDef, false);
|
|
702
|
-
if (tiers.confirmed.
|
|
703
|
-
if (tiers.unverified.length === 0) {
|
|
737
|
+
if (tiers.confirmed.length === 0) {
|
|
738
|
+
if (tiers.unverified.length === 0 && tiers.recursive.length === 0) {
|
|
704
739
|
node.entryPoint = true;
|
|
705
740
|
entryPoints.push({ name: funcDef.name, file: funcDef.relativePath, line: funcDef.startLine });
|
|
741
|
+
} else if (tiers.recursive.length > 0) {
|
|
742
|
+
node.selfRecursive = true;
|
|
706
743
|
} else {
|
|
707
744
|
node.unverifiedCallerCount = tiers.unverified.length;
|
|
708
745
|
}
|
|
@@ -716,7 +753,10 @@ function reverseTrace(index, name, options = {}) {
|
|
|
716
753
|
|
|
717
754
|
// Also mark root as entry point if it has no callers in either tier
|
|
718
755
|
if (tree && tree.children.length === 0 && maxDepth > 0) {
|
|
719
|
-
|
|
756
|
+
const rootConfirmedCount = rootRaw
|
|
757
|
+
? rootRaw.filter(c => c.tier !== 'unverified').length
|
|
758
|
+
: 0;
|
|
759
|
+
if (rootConfirmedCount === 0 && rootUnverifiedCount === 0 && !tree.selfRecursive) {
|
|
720
760
|
tree.entryPoint = true;
|
|
721
761
|
entryPoints.push({ name: def.name, file: def.relativePath, line: def.startLine });
|
|
722
762
|
} else {
|
|
@@ -771,6 +811,7 @@ function reverseTrace(index, name, options = {}) {
|
|
|
771
811
|
totalFunctions: visited.size - 1, // exclude root
|
|
772
812
|
maxDepthReached,
|
|
773
813
|
unverifiedEdges: treeAccount.unverifiedEdges,
|
|
814
|
+
...(tree?.selfRecursive && { selfRecursive: true }),
|
|
774
815
|
},
|
|
775
816
|
warnings: warnings.length > 0 ? warnings : undefined
|
|
776
817
|
};
|
|
@@ -784,12 +825,12 @@ function reverseTrace(index, name, options = {}) {
|
|
|
784
825
|
* Two bands (tree contract): `affectedFunctions`/`testFiles` come from the
|
|
785
826
|
* confirmed-chain closure; names reachable only through >= 1 unverified hop
|
|
786
827
|
* land in `possiblyAffected`, their additional test files in
|
|
787
|
-
* `possiblyAffectedTests`.
|
|
828
|
+
* `possiblyAffectedTests`. Static-linkage claims are confirmed-band only.
|
|
788
829
|
*
|
|
789
830
|
* @param {object} index - ProjectIndex instance
|
|
790
831
|
* @param {string} name - Function name
|
|
791
832
|
* @param {object} options - { depth, file, className, exclude, includeMethods }
|
|
792
|
-
* @returns {object|null} Affected test files with
|
|
833
|
+
* @returns {object|null} Affected test files with static-linkage stats
|
|
793
834
|
*/
|
|
794
835
|
function affectedTests(index, name, options = {}) {
|
|
795
836
|
index._beginOp();
|
|
@@ -898,7 +939,7 @@ function affectedTests(index, name, options = {}) {
|
|
|
898
939
|
if (!isTestCaller && fe.callerName) {
|
|
899
940
|
const defs = index.symbols.get(fe.callerName);
|
|
900
941
|
const d = defs?.find(x => x.file === fe.callerFile && x.startLine === fe.callerStartLine);
|
|
901
|
-
const kindOf =
|
|
942
|
+
const kindOf = getLanguageAdapter(cfe.language)?.getEntryPointKind;
|
|
902
943
|
if (d && kindOf && kindOf(d) === 'test') isTestCaller = true;
|
|
903
944
|
}
|
|
904
945
|
if (!isTestCaller && fe.line != null) {
|
|
@@ -1130,7 +1171,7 @@ function affectedTests(index, name, options = {}) {
|
|
|
1130
1171
|
}
|
|
1131
1172
|
|
|
1132
1173
|
if (fileMatches.size > 0) {
|
|
1133
|
-
const
|
|
1174
|
+
const linkedFunctions = [...fileMatches.keys()];
|
|
1134
1175
|
const allMatches = [];
|
|
1135
1176
|
for (const matches of fileMatches.values()) allMatches.push(...matches);
|
|
1136
1177
|
// Deduplicate same line+function (test-case line might overlap with call line)
|
|
@@ -1144,40 +1185,41 @@ function affectedTests(index, name, options = {}) {
|
|
|
1144
1185
|
}
|
|
1145
1186
|
const deduped = [...dedupMap.values()].sort((a, b) => a.line - b.line);
|
|
1146
1187
|
|
|
1147
|
-
// Only count functions with call or test-case matches as
|
|
1148
|
-
// Import-only or reference-only
|
|
1149
|
-
|
|
1188
|
+
// Only count functions with call or test-case matches as
|
|
1189
|
+
// statically linked. Import-only or reference-only records
|
|
1190
|
+
// are useful evidence, but are not an exercising path.
|
|
1191
|
+
const realLinkedAll = linkedFunctions.filter(fn => {
|
|
1150
1192
|
const fnMatches = deduped.filter(m => m.functionName === fn);
|
|
1151
1193
|
return fnMatches.some(m => m.matchType === 'call' || m.matchType === 'test-case');
|
|
1152
1194
|
});
|
|
1153
|
-
const
|
|
1154
|
-
const
|
|
1195
|
+
const staticallyLinkedFunctions = realLinkedAll.filter(fn => affectedNames.has(fn));
|
|
1196
|
+
const possiblyLinkedFunctions = realLinkedAll.filter(fn => possiblyNames.has(fn));
|
|
1155
1197
|
|
|
1156
|
-
if (
|
|
1157
|
-
// Confirmed band: matches for confirmed-
|
|
1198
|
+
if (staticallyLinkedFunctions.length > 0) {
|
|
1199
|
+
// Confirmed band: matches for confirmed-linked names
|
|
1158
1200
|
const realMatches = deduped.filter(m =>
|
|
1159
1201
|
affectedNames.has(m.functionName) &&
|
|
1160
1202
|
(m.matchType === 'call' || m.matchType === 'test-case' ||
|
|
1161
|
-
|
|
1203
|
+
staticallyLinkedFunctions.includes(m.functionName))
|
|
1162
1204
|
);
|
|
1163
1205
|
results.push({
|
|
1164
1206
|
file: fileEntry.relativePath,
|
|
1165
|
-
|
|
1166
|
-
...(
|
|
1207
|
+
linkedFunctions: staticallyLinkedFunctions,
|
|
1208
|
+
...(possiblyLinkedFunctions.length > 0 && { possiblyLinkedFunctions }),
|
|
1167
1209
|
matchCount: realMatches.length,
|
|
1168
1210
|
matches: realMatches
|
|
1169
1211
|
});
|
|
1170
|
-
} else if (
|
|
1212
|
+
} else if (possiblyLinkedFunctions.length > 0) {
|
|
1171
1213
|
// Possible band: file reaches the change only through
|
|
1172
1214
|
// unverified chains.
|
|
1173
1215
|
const possibleMatches = deduped.filter(m =>
|
|
1174
1216
|
possiblyNames.has(m.functionName) &&
|
|
1175
1217
|
(m.matchType === 'call' || m.matchType === 'test-case' ||
|
|
1176
|
-
|
|
1218
|
+
possiblyLinkedFunctions.includes(m.functionName))
|
|
1177
1219
|
);
|
|
1178
1220
|
possibleResults.push({
|
|
1179
1221
|
file: fileEntry.relativePath,
|
|
1180
|
-
|
|
1222
|
+
linkedFunctions: possiblyLinkedFunctions,
|
|
1181
1223
|
matchCount: possibleMatches.length,
|
|
1182
1224
|
matches: possibleMatches
|
|
1183
1225
|
});
|
|
@@ -1199,22 +1241,25 @@ function affectedTests(index, name, options = {}) {
|
|
|
1199
1241
|
if (inResults || inPossible) {
|
|
1200
1242
|
const existing = inResults || inPossible;
|
|
1201
1243
|
const have = new Set([
|
|
1202
|
-
...existing.
|
|
1203
|
-
...(existing.
|
|
1244
|
+
...existing.linkedFunctions,
|
|
1245
|
+
...(existing.possiblyLinkedFunctions || []),
|
|
1204
1246
|
]);
|
|
1205
1247
|
const extra = [...entry.byName.keys()].filter(n => !have.has(n)).sort(codeUnitCompare);
|
|
1206
1248
|
if (extra.length > 0) {
|
|
1207
1249
|
if (inResults) {
|
|
1208
|
-
existing.
|
|
1250
|
+
existing.possiblyLinkedFunctions = [
|
|
1251
|
+
...(existing.possiblyLinkedFunctions || []),
|
|
1252
|
+
...extra,
|
|
1253
|
+
].sort(codeUnitCompare);
|
|
1209
1254
|
} else {
|
|
1210
|
-
existing.
|
|
1255
|
+
existing.linkedFunctions = [...existing.linkedFunctions, ...extra].sort(codeUnitCompare);
|
|
1211
1256
|
}
|
|
1212
1257
|
}
|
|
1213
1258
|
continue;
|
|
1214
1259
|
}
|
|
1215
|
-
const
|
|
1260
|
+
const linked = [...entry.byName.keys()].sort(codeUnitCompare);
|
|
1216
1261
|
const matches = [];
|
|
1217
|
-
for (const fn of
|
|
1262
|
+
for (const fn of linked) {
|
|
1218
1263
|
for (const ln of [...entry.byName.get(fn)].sort((a, b) => a - b)) {
|
|
1219
1264
|
matches.push({
|
|
1220
1265
|
line: ln,
|
|
@@ -1226,17 +1271,17 @@ function affectedTests(index, name, options = {}) {
|
|
|
1226
1271
|
}
|
|
1227
1272
|
possibleResults.push({
|
|
1228
1273
|
file: entry.rel,
|
|
1229
|
-
|
|
1274
|
+
linkedFunctions: linked,
|
|
1230
1275
|
matchCount: matches.length,
|
|
1231
1276
|
matches,
|
|
1232
1277
|
});
|
|
1233
1278
|
}
|
|
1234
1279
|
|
|
1235
|
-
// Sort by
|
|
1236
|
-
results.sort((a, b) => b.
|
|
1237
|
-
possibleResults.sort((a, b) => b.
|
|
1280
|
+
// Sort by linkage breadth then alphabetically
|
|
1281
|
+
results.sort((a, b) => b.linkedFunctions.length - a.linkedFunctions.length || codeUnitCompare(a.file, b.file));
|
|
1282
|
+
possibleResults.sort((a, b) => b.linkedFunctions.length - a.linkedFunctions.length || codeUnitCompare(a.file, b.file));
|
|
1238
1283
|
|
|
1239
|
-
// Compute
|
|
1284
|
+
// Compute static-linkage stats.
|
|
1240
1285
|
// Filter out test function names from affectedNames — they are callers,
|
|
1241
1286
|
// not production symbols that need test coverage.
|
|
1242
1287
|
const isProductionName = (n) => {
|
|
@@ -1246,7 +1291,7 @@ function affectedTests(index, name, options = {}) {
|
|
|
1246
1291
|
// language's getEntryPointKind says so; they need no coverage.
|
|
1247
1292
|
for (const [, fe] of index.files) {
|
|
1248
1293
|
if (isTestFile(fe.relativePath, fe.language)) continue;
|
|
1249
|
-
const langModule =
|
|
1294
|
+
const langModule = getLanguageAdapter(fe.language);
|
|
1250
1295
|
const kindOf = langModule?.getEntryPointKind;
|
|
1251
1296
|
if (fe.symbols?.some(s => s.name === n && (!kindOf || kindOf(s) !== 'test'))) {
|
|
1252
1297
|
return true;
|
|
@@ -1262,15 +1307,20 @@ function affectedTests(index, name, options = {}) {
|
|
|
1262
1307
|
const namesForCoverage = productionNames.size > 0 ? productionNames : affectedNames;
|
|
1263
1308
|
const possiblyProduction = [...possiblyNames].filter(isProductionName);
|
|
1264
1309
|
|
|
1265
|
-
const
|
|
1266
|
-
for (const r of results) for (const f of r.
|
|
1267
|
-
if (namesForCoverage.has(f))
|
|
1310
|
+
const linkedSet = new Set();
|
|
1311
|
+
for (const r of results) for (const f of r.linkedFunctions) {
|
|
1312
|
+
if (namesForCoverage.has(f)) linkedSet.add(f);
|
|
1268
1313
|
}
|
|
1269
|
-
const
|
|
1314
|
+
const notStaticallyLinked = [...namesForCoverage].filter(n => !linkedSet.has(n));
|
|
1270
1315
|
|
|
1271
1316
|
return {
|
|
1272
1317
|
root: blastResult.root, file: blastResult.file, line: blastResult.line,
|
|
1273
1318
|
depth: blastResult.maxDepth,
|
|
1319
|
+
selection: {
|
|
1320
|
+
basis: 'static-call-and-reference-links',
|
|
1321
|
+
runtimeCoverage: false,
|
|
1322
|
+
absenceProvesUntested: false,
|
|
1323
|
+
},
|
|
1274
1324
|
affectedFunctions: [...namesForCoverage],
|
|
1275
1325
|
possiblyAffected: possiblyProduction,
|
|
1276
1326
|
testFiles: results,
|
|
@@ -1280,13 +1330,13 @@ function affectedTests(index, name, options = {}) {
|
|
|
1280
1330
|
summary: {
|
|
1281
1331
|
totalAffected: namesForCoverage.size,
|
|
1282
1332
|
totalTestFiles: results.length,
|
|
1283
|
-
|
|
1284
|
-
|
|
1333
|
+
staticallyLinkedFunctions: linkedSet.size,
|
|
1334
|
+
notStaticallyLinkedCount: notStaticallyLinked.length,
|
|
1285
1335
|
possiblyAffected: possiblyProduction.length,
|
|
1286
1336
|
possiblyAffectedTests: possibleResults.length,
|
|
1287
1337
|
unverifiedEdges: blastResult.summary ? blastResult.summary.unverifiedEdges : 0,
|
|
1288
1338
|
},
|
|
1289
|
-
|
|
1339
|
+
notStaticallyLinked,
|
|
1290
1340
|
warnings: blastResult.warnings,
|
|
1291
1341
|
};
|
|
1292
1342
|
} finally { index._endOp(); }
|
|
@@ -1329,7 +1379,7 @@ function _addAffectedTestCases(index, filePath, fileEntry, funcName, fileMatches
|
|
|
1329
1379
|
} else {
|
|
1330
1380
|
if (!fileEntry.symbols) return;
|
|
1331
1381
|
try {
|
|
1332
|
-
const langModule =
|
|
1382
|
+
const langModule = getLanguageAdapter(lang);
|
|
1333
1383
|
if (!langModule) return;
|
|
1334
1384
|
// Prefer the kinded predicate so we don't mis-tag fn main() / fn init()
|
|
1335
1385
|
// (runtime entries) as test cases (BUG-CX). Fall back to isEntryPoint
|