ucn 4.1.2 → 4.2.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 +80 -353
- package/.claude/skills/ucn/references/commands.md +93 -0
- package/.claude/skills/ucn/references/trust-contract.md +63 -0
- package/README.md +67 -44
- package/cli/index.js +24 -19
- package/core/account.js +18 -0
- package/core/analysis.js +27 -7
- package/core/bridge.js +0 -1
- package/core/brief.js +2 -3
- package/core/build-worker.js +5 -0
- package/core/cache.js +55 -6
- package/core/callers.js +1709 -162
- package/core/check.js +107 -19
- package/core/confidence.js +29 -16
- package/core/deadcode.js +84 -24
- package/core/entrypoints.js +32 -27
- package/core/execute.js +19 -3
- package/core/graph-build.js +33 -0
- package/core/output/analysis.js +53 -14
- package/core/output/check.js +11 -0
- package/core/output/doctor.js +40 -19
- package/core/output/endpoints.js +0 -2
- package/core/output/search.js +26 -1
- package/core/parser.js +1 -1
- package/core/project.js +25 -4
- package/core/registry.js +2 -2
- package/core/reporting.js +166 -108
- package/core/search.js +271 -49
- package/core/tracing.js +2 -2
- package/core/trust-matrix.js +158 -0
- package/core/verify.js +0 -1
- package/eslint.config.js +2 -2
- package/languages/go.js +143 -13
- package/languages/html.js +5 -0
- package/languages/index.js +5 -0
- package/languages/java.js +79 -12
- package/languages/javascript.js +233 -42
- package/languages/python.js +54 -15
- package/languages/rust.js +180 -22
- package/languages/utils.js +0 -1
- package/mcp/server.js +118 -38
- package/package.json +11 -4
package/core/check.js
CHANGED
|
@@ -11,7 +11,33 @@
|
|
|
11
11
|
|
|
12
12
|
'use strict';
|
|
13
13
|
|
|
14
|
-
const { diffImpact } = require('./analysis');
|
|
14
|
+
const { diffImpact, composeAccount, callNotResolvedEntries } = require('./analysis');
|
|
15
|
+
|
|
16
|
+
function summarizeAccount(account) {
|
|
17
|
+
if (!account) {
|
|
18
|
+
return {
|
|
19
|
+
available: false,
|
|
20
|
+
textComplete: false,
|
|
21
|
+
semanticComplete: false,
|
|
22
|
+
safeToDelete: false,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const contract = account.contract || {};
|
|
26
|
+
return {
|
|
27
|
+
available: true,
|
|
28
|
+
conserved: account.conserved === true,
|
|
29
|
+
textComplete: contract.textComplete === true,
|
|
30
|
+
observedTextZero: contract.observedTextZero === true,
|
|
31
|
+
semanticComplete: false,
|
|
32
|
+
safeToDelete: false,
|
|
33
|
+
unparsedFiles: account.unparsed ? account.unparsed.fileCount || 0 : 0,
|
|
34
|
+
unreadableFiles: Array.isArray(account.unreadableFiles) ? account.unreadableFiles.length : 0,
|
|
35
|
+
unaccounted: account.unaccounted || 0,
|
|
36
|
+
filtered: account.filtered ? account.filtered.total || 0 : 0,
|
|
37
|
+
beyondTextCallers: account.beyondText ? account.beyondText.count || 0 : 0,
|
|
38
|
+
requiresUsageReview: contract.requiresUsageReview === true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
15
41
|
|
|
16
42
|
/**
|
|
17
43
|
* Run the pre-commit check.
|
|
@@ -61,12 +87,11 @@ function check(index, options = {}) {
|
|
|
61
87
|
const changed = limit ? allChanged.slice(0, limit) : allChanged;
|
|
62
88
|
|
|
63
89
|
const items = [];
|
|
64
|
-
const reachable = computeReachableSet(index);
|
|
65
90
|
|
|
66
91
|
// For each changed function, run verify and gather caller summary
|
|
67
92
|
for (const fn of changed) {
|
|
68
93
|
const filePath = fn.relativePath || fn.file || '';
|
|
69
|
-
let verifyResult
|
|
94
|
+
let verifyResult;
|
|
70
95
|
try {
|
|
71
96
|
verifyResult = index.verify(fn.name, { file: filePath });
|
|
72
97
|
} catch (e) {
|
|
@@ -81,12 +106,22 @@ function check(index, options = {}) {
|
|
|
81
106
|
// bands already (confirmed `callers` + visible `unverifiedCallers`).
|
|
82
107
|
let callers = Array.isArray(fn.callers) ? fn.callers : [];
|
|
83
108
|
let unverifiedCallers = Array.isArray(fn.unverifiedCallers) ? fn.unverifiedCallers : [];
|
|
109
|
+
let account = fn.account || null;
|
|
84
110
|
if (callers.length === 0 && fn._kind === 'added') {
|
|
85
111
|
try {
|
|
86
|
-
const
|
|
112
|
+
const definitions = (index.symbols.get(fn.name) || []).filter(d =>
|
|
113
|
+
(d.relativePath === filePath || d.file === fn.filePath) &&
|
|
114
|
+
(!fn.startLine || d.startLine === fn.startLine));
|
|
115
|
+
const raw = index.findCallers(fn.name, {
|
|
116
|
+
includeMethods: true,
|
|
117
|
+
collectAccount: true,
|
|
118
|
+
targetDefinitions: definitions.length > 0 ? definitions : undefined,
|
|
119
|
+
}) || [];
|
|
87
120
|
callers = raw.filter(c => c.tier !== 'unverified');
|
|
88
121
|
unverifiedCallers = raw.filter(c => c.tier === 'unverified')
|
|
89
122
|
.concat(raw.unverifiedEntries || []);
|
|
123
|
+
account = composeAccount(index, fn.name, raw);
|
|
124
|
+
unverifiedCallers = unverifiedCallers.concat(callNotResolvedEntries(index, account));
|
|
90
125
|
} catch (e) { /* skip */ }
|
|
91
126
|
}
|
|
92
127
|
|
|
@@ -98,6 +133,7 @@ function check(index, options = {}) {
|
|
|
98
133
|
callerCount: callers.length,
|
|
99
134
|
unverifiedCallerCount: unverifiedCallers.length,
|
|
100
135
|
signatureMismatches: mismatches.length,
|
|
136
|
+
account: summarizeAccount(account),
|
|
101
137
|
...(mismatches.length > 0 && { mismatches: mismatches.slice(0, 5) }),
|
|
102
138
|
};
|
|
103
139
|
|
|
@@ -134,6 +170,13 @@ function check(index, options = {}) {
|
|
|
134
170
|
callerCount: 0,
|
|
135
171
|
unverifiedCallerCount: (d.remainingCallSites || []).length,
|
|
136
172
|
signatureMismatches: 0,
|
|
173
|
+
account: {
|
|
174
|
+
available: false,
|
|
175
|
+
reason: 'deleted-target-cannot-be-reconciled-against-the-current-index',
|
|
176
|
+
textComplete: false,
|
|
177
|
+
semanticComplete: false,
|
|
178
|
+
safeToDelete: false,
|
|
179
|
+
},
|
|
137
180
|
});
|
|
138
181
|
}
|
|
139
182
|
|
|
@@ -157,6 +200,34 @@ function check(index, options = {}) {
|
|
|
157
200
|
// Action items
|
|
158
201
|
const actions = [];
|
|
159
202
|
for (const it of items) {
|
|
203
|
+
if (!it.account || !it.account.textComplete) {
|
|
204
|
+
actions.push({
|
|
205
|
+
severity: 'error',
|
|
206
|
+
kind: 'incomplete_account',
|
|
207
|
+
message: `${it.name}: caller evidence is not text-complete; inspect warnings and usages manually`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (it.unverifiedCallerCount > 0) {
|
|
211
|
+
actions.push({
|
|
212
|
+
severity: 'warn',
|
|
213
|
+
kind: 'unverified_callers',
|
|
214
|
+
message: `${it.name}: review ${it.unverifiedCallerCount} unverified caller candidate(s)`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (it.account && it.account.filtered > 0) {
|
|
218
|
+
actions.push({
|
|
219
|
+
severity: 'warn',
|
|
220
|
+
kind: 'filtered_evidence',
|
|
221
|
+
message: `${it.name}: ${it.account.filtered} caller edge(s) were hidden by filters`,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (it.account && it.account.requiresUsageReview) {
|
|
225
|
+
actions.push({
|
|
226
|
+
severity: 'warn',
|
|
227
|
+
kind: 'non_call_usages',
|
|
228
|
+
message: `${it.name}: non-call references require a usages review`,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
160
231
|
if (it.signatureMismatches > 0) {
|
|
161
232
|
actions.push({
|
|
162
233
|
severity: 'warn',
|
|
@@ -168,10 +239,17 @@ function check(index, options = {}) {
|
|
|
168
239
|
actions.push({
|
|
169
240
|
severity: 'warn',
|
|
170
241
|
kind: 'orphan_new',
|
|
171
|
-
message: `${it.name} is new
|
|
242
|
+
message: `${it.name} is new with no observed caller candidates and is not a detected entry point; review usages before treating it as orphaned`,
|
|
172
243
|
});
|
|
173
244
|
}
|
|
174
245
|
}
|
|
246
|
+
if (limit && allChanged.length > limit) {
|
|
247
|
+
actions.push({
|
|
248
|
+
severity: 'error',
|
|
249
|
+
kind: 'truncated_change_set',
|
|
250
|
+
message: `${allChanged.length - limit} changed function(s) were not checked; rerun without --limit`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
175
253
|
if (testFiles.length > 0) {
|
|
176
254
|
const filesList = testFiles.slice(0, 5).map(t => t.file).join(' ');
|
|
177
255
|
actions.push({
|
|
@@ -181,6 +259,16 @@ function check(index, options = {}) {
|
|
|
181
259
|
});
|
|
182
260
|
}
|
|
183
261
|
|
|
262
|
+
const incompleteAccounts = items.filter(it => !it.account || !it.account.textComplete).length;
|
|
263
|
+
const unverifiedCallSites = items.reduce((sum, it) => sum + (it.unverifiedCallerCount || 0), 0);
|
|
264
|
+
const signatureMismatches = items.reduce((sum, it) => sum + (it.signatureMismatches || 0), 0);
|
|
265
|
+
const filteredEdges = items.reduce((sum, it) => sum + (it.account ? it.account.filtered || 0 : 0), 0);
|
|
266
|
+
const usageReviewSymbols = items.filter(it => it.account && it.account.requiresUsageReview).length;
|
|
267
|
+
const reviewRequired = incompleteAccounts > 0 || unverifiedCallSites > 0 ||
|
|
268
|
+
signatureMismatches > 0 || filteredEdges > 0 || usageReviewSymbols > 0 ||
|
|
269
|
+
actions.some(a => a.kind === 'orphan_new') ||
|
|
270
|
+
!!(limit && allChanged.length > limit);
|
|
271
|
+
|
|
184
272
|
return {
|
|
185
273
|
base: options.base || 'HEAD',
|
|
186
274
|
staged: !!options.staged,
|
|
@@ -191,21 +279,21 @@ function check(index, options = {}) {
|
|
|
191
279
|
totalTestFiles: testFiles.length,
|
|
192
280
|
totalTests: testCount,
|
|
193
281
|
actions,
|
|
282
|
+
trust: {
|
|
283
|
+
status: incompleteAccounts > 0 || signatureMismatches > 0
|
|
284
|
+
? 'BLOCKED'
|
|
285
|
+
: reviewRequired ? 'REVIEW_REQUIRED' : 'READY_FOR_TOOLCHAIN',
|
|
286
|
+
accountsChecked: items.filter(it => it.account && it.account.available).length,
|
|
287
|
+
incompleteAccounts,
|
|
288
|
+
unverifiedCallSites,
|
|
289
|
+
signatureMismatches,
|
|
290
|
+
filteredEdges,
|
|
291
|
+
usageReviewSymbols,
|
|
292
|
+
semanticComplete: false,
|
|
293
|
+
safeToDelete: false,
|
|
294
|
+
requiresCompilerAndTests: true,
|
|
295
|
+
},
|
|
194
296
|
};
|
|
195
297
|
}
|
|
196
298
|
|
|
197
|
-
function symbolKey(file, line) {
|
|
198
|
-
return `${file}:${line}`;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function computeReachableSet(index) {
|
|
202
|
-
try {
|
|
203
|
-
const ep = require('./entrypoints');
|
|
204
|
-
if (typeof ep.computeReachability === 'function') {
|
|
205
|
-
return ep.computeReachability(index);
|
|
206
|
-
}
|
|
207
|
-
} catch (e) { /* fall through */ }
|
|
208
|
-
return null;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
299
|
module.exports = { check };
|
package/core/confidence.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* core/confidence.js - Deterministic edge confidence scoring
|
|
3
3
|
*
|
|
4
|
-
* Assigns
|
|
5
|
-
* how the call was resolved. Rule-based, no ML.
|
|
4
|
+
* Assigns an ordinal 0.0-1.0 evidence weight to each caller/callee edge based
|
|
5
|
+
* on how the call was resolved. Rule-based, no ML. These values rank evidence;
|
|
6
|
+
* they are NOT probabilities and are NOT calibrated accuracy estimates.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
'use strict';
|
|
@@ -19,7 +20,8 @@ const RESOLUTION = {
|
|
|
19
20
|
UNCERTAIN: 'uncertain',
|
|
20
21
|
};
|
|
21
22
|
|
|
22
|
-
//
|
|
23
|
+
// Ordinal evidence weights per resolution type. The legacy public field is
|
|
24
|
+
// named `confidence` for compatibility; do not interpret 0.98 as "98% likely".
|
|
23
25
|
const SCORES = {
|
|
24
26
|
[RESOLUTION.EXACT_BINDING]: 0.98,
|
|
25
27
|
[RESOLUTION.SAME_CLASS]: 0.92,
|
|
@@ -59,6 +61,17 @@ function tierForResolution(resolution) {
|
|
|
59
61
|
return RESOLUTION_TIER[resolution] || TIER.UNVERIFIED;
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
function scored(resolution, reasons) {
|
|
65
|
+
const evidenceScore = SCORES[resolution];
|
|
66
|
+
return {
|
|
67
|
+
confidence: evidenceScore,
|
|
68
|
+
evidenceScore,
|
|
69
|
+
scoreKind: 'ordinal-evidence-not-probability',
|
|
70
|
+
resolution,
|
|
71
|
+
evidence: reasons,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
62
75
|
/**
|
|
63
76
|
* Score a caller/callee edge based on resolution evidence.
|
|
64
77
|
*
|
|
@@ -71,7 +84,7 @@ function tierForResolution(resolution) {
|
|
|
71
84
|
* @param {boolean} [evidence.isUncertain] - Marked uncertain by resolution logic
|
|
72
85
|
* @param {boolean} [evidence.isFunctionReference] - Passed as callback argument
|
|
73
86
|
* @param {boolean} [evidence.hasReceiverType] - Go/Java/Rust parser-inferred receiverType
|
|
74
|
-
* @returns {{ confidence: number, resolution: string, evidence: string[] }}
|
|
87
|
+
* @returns {{ confidence: number, evidenceScore: number, scoreKind: string, resolution: string, evidence: string[] }}
|
|
75
88
|
*/
|
|
76
89
|
function scoreEdge(evidence) {
|
|
77
90
|
const reasons = [];
|
|
@@ -81,38 +94,38 @@ function scoreEdge(evidence) {
|
|
|
81
94
|
// (without this, a known mismatch would score receiver-hint 0.80).
|
|
82
95
|
if (evidence.typeMismatch) {
|
|
83
96
|
reasons.push('receiver type mismatch');
|
|
84
|
-
return
|
|
97
|
+
return scored(RESOLUTION.UNCERTAIN, reasons);
|
|
85
98
|
}
|
|
86
99
|
|
|
87
100
|
// Nominal dispatch tiering (contract surface only — callers.js sets these
|
|
88
101
|
// flags exclusively under collectAccount, so legacy paths never see them).
|
|
89
102
|
if (evidence.possibleDispatch) {
|
|
90
103
|
reasons.push('interface/supertype dispatch');
|
|
91
|
-
return
|
|
104
|
+
return scored(RESOLUTION.POSSIBLE_DISPATCH, reasons);
|
|
92
105
|
}
|
|
93
106
|
if (evidence.methodAmbiguous) {
|
|
94
107
|
reasons.push('untyped receiver, multiple same-name definitions');
|
|
95
|
-
return
|
|
108
|
+
return scored(RESOLUTION.METHOD_AMBIGUOUS, reasons);
|
|
96
109
|
}
|
|
97
110
|
|
|
98
111
|
// Exact binding match (highest confidence)
|
|
99
112
|
if (evidence.hasBindingId) {
|
|
100
113
|
reasons.push('binding-id match');
|
|
101
114
|
if (evidence.hasImportEvidence) reasons.push('import-verified');
|
|
102
|
-
return
|
|
115
|
+
return scored(RESOLUTION.EXACT_BINDING, reasons);
|
|
103
116
|
}
|
|
104
117
|
|
|
105
118
|
// Same-class resolution (self/this/super/cls)
|
|
106
119
|
if (evidence.resolvedBySameClass) {
|
|
107
120
|
reasons.push('same-class method');
|
|
108
121
|
if (evidence.hasInheritanceChain) reasons.push('via inheritance');
|
|
109
|
-
return
|
|
122
|
+
return scored(RESOLUTION.SAME_CLASS, reasons);
|
|
110
123
|
}
|
|
111
124
|
|
|
112
125
|
// Receiver hint narrowed to specific type
|
|
113
126
|
if (evidence.resolvedByReceiverHint || evidence.hasReceiverType) {
|
|
114
127
|
reasons.push(evidence.hasReceiverType ? 'parser receiver-type' : 'local type inference');
|
|
115
|
-
return
|
|
128
|
+
return scored(RESOLUTION.RECEIVER_HINT, reasons);
|
|
116
129
|
}
|
|
117
130
|
|
|
118
131
|
// Function reference (callback / passed-as-argument). Argument position is
|
|
@@ -123,10 +136,10 @@ function scoreEdge(evidence) {
|
|
|
123
136
|
reasons.push('function reference');
|
|
124
137
|
if (evidence.hasImportEvidence || evidence.hasSamePackageEvidence) {
|
|
125
138
|
reasons.push(evidence.hasImportEvidence ? 'import-supported' : 'same package/module');
|
|
126
|
-
return
|
|
139
|
+
return scored(RESOLUTION.SCOPE_MATCH, reasons);
|
|
127
140
|
}
|
|
128
141
|
reasons.push('no import evidence');
|
|
129
|
-
return
|
|
142
|
+
return scored(RESOLUTION.NAME_ONLY, reasons);
|
|
130
143
|
}
|
|
131
144
|
|
|
132
145
|
// Scope/import-supported match
|
|
@@ -134,22 +147,22 @@ function scoreEdge(evidence) {
|
|
|
134
147
|
if (evidence.hasImportEvidence) reasons.push('import-supported');
|
|
135
148
|
if (evidence.hasReceiverEvidence) reasons.push('receiver binding in scope');
|
|
136
149
|
if (evidence.hasSamePackageEvidence) reasons.push('same package/module');
|
|
137
|
-
return
|
|
150
|
+
return scored(RESOLUTION.SCOPE_MATCH, reasons);
|
|
138
151
|
}
|
|
139
152
|
|
|
140
153
|
// Uncertain
|
|
141
154
|
if (evidence.isUncertain) {
|
|
142
155
|
reasons.push('ambiguous resolution');
|
|
143
|
-
return
|
|
156
|
+
return scored(RESOLUTION.UNCERTAIN, reasons);
|
|
144
157
|
}
|
|
145
158
|
|
|
146
159
|
// Name-only match (no additional evidence)
|
|
147
160
|
reasons.push('name match only');
|
|
148
|
-
return
|
|
161
|
+
return scored(RESOLUTION.NAME_ONLY, reasons);
|
|
149
162
|
}
|
|
150
163
|
|
|
151
164
|
/**
|
|
152
|
-
* Filter edges by minimum
|
|
165
|
+
* Filter edges by minimum ordinal evidence threshold (legacy name retained).
|
|
153
166
|
* @param {Array} edges - Array of objects with .confidence property
|
|
154
167
|
* @param {number} minConfidence - Minimum confidence (0.0-1.0)
|
|
155
168
|
* @returns {{ kept: Array, filtered: number }}
|
package/core/deadcode.js
CHANGED
|
@@ -53,32 +53,90 @@ function _baseIsExternal(index, bare, lang) {
|
|
|
53
53
|
return !(defs && defs.some(d => _CLASS_KINDS.includes(d.type)));
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const supers = Array.isArray(classDef.extends) ? classDef.extends : splitParentList(classDef.extends);
|
|
60
|
-
return supers.some(raw => _baseIsExternal(index, _bareBaseName(raw), lang));
|
|
61
|
-
}
|
|
56
|
+
// Bounded heritage-closure depth (fix #270) — matches the engine's other
|
|
57
|
+
// inheritance walks.
|
|
58
|
+
const _HERITAGE_WALK_DEPTH = 8;
|
|
62
59
|
|
|
63
60
|
/**
|
|
64
|
-
* Does
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
61
|
+
* Does this CLASS DEF reach at least one base that is NOT in the project
|
|
62
|
+
* index, walking `extends` chains transitively THROUGH resolved project types
|
|
63
|
+
* (fix #270)? One level was not enough — fastify-measured: CustomLoggerImpl
|
|
64
|
+
* implements CustomLogger (project) extends FastifyBaseLogger (project,
|
|
65
|
+
* .d.ts) extends Pick<BaseLogger, ...'silent'> (pino — external). The member
|
|
66
|
+
* surface the class must provide comes from the OUT-OF-TREE end of the chain,
|
|
67
|
+
* invisible without tsc, so a zero-usage impl member (`silent`) is contract
|
|
68
|
+
* surface — deleting it breaks the build. Same dispatch physics as one level;
|
|
69
|
+
* only the verdict moved from "direct parent external" to "heritage closure
|
|
70
|
+
* reaches external".
|
|
69
71
|
*
|
|
70
|
-
* `
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* `
|
|
72
|
+
* `followImplements` additionally walks the def's OWN `implements` clause
|
|
73
|
+
* (first hop only — the measured family; deeper implements hops are
|
|
74
|
+
* classified-deferred). Method claims pass true only when the member sits
|
|
75
|
+
* INSIDE the def's source range: TS/Java members live in the class body, so
|
|
76
|
+
* the implements contract constrains them. Rust surfaces trait impls as
|
|
77
|
+
* `implements` on the struct (rust.js), but inherent methods live in separate
|
|
78
|
+
* `impl X` blocks OUTSIDE the struct's range — an unrelated `impl Display
|
|
79
|
+
* for X` must never shield a genuinely-dead inherent method (the original
|
|
80
|
+
* reason implements was not consulted here at all; trait-impl members
|
|
81
|
+
* themselves carry traitImpl/@Override markers → Rule A). Class-kind claims
|
|
82
|
+
* pass false: deleting the whole class removes its implements clause with it.
|
|
83
|
+
*/
|
|
84
|
+
function _heritageReachesExternalBase(index, classDef, lang, followImplements) {
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
let frontier = [classDef];
|
|
87
|
+
for (let depth = 0; depth < _HERITAGE_WALK_DEPTH && frontier.length; depth++) {
|
|
88
|
+
const next = [];
|
|
89
|
+
for (const def of frontier) {
|
|
90
|
+
// Copy array-shaped extends — the implements push below must
|
|
91
|
+
// never mutate the symbol's own heritage data in the index.
|
|
92
|
+
const parents = def.extends
|
|
93
|
+
? (Array.isArray(def.extends) ? [...def.extends] : splitParentList(def.extends))
|
|
94
|
+
: [];
|
|
95
|
+
if (depth === 0 && followImplements && Array.isArray(def.implements)) {
|
|
96
|
+
parents.push(...def.implements);
|
|
97
|
+
}
|
|
98
|
+
for (const raw of parents) {
|
|
99
|
+
const bare = _bareBaseName(raw);
|
|
100
|
+
if (!bare || seen.has(bare)) continue;
|
|
101
|
+
seen.add(bare);
|
|
102
|
+
if (_baseIsExternal(index, bare, lang)) return true;
|
|
103
|
+
for (const pd of index.symbols.get(bare) || []) {
|
|
104
|
+
if (_CLASS_KINDS.includes(pd.type)) next.push(pd);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
frontier = next;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Does the method's enclosing class reach at least one base that is NOT in
|
|
115
|
+
* the project index through its heritage closure? An out-of-tree base is a
|
|
116
|
+
* framework/library type UCN can't see; via inheritance it may dispatch into
|
|
117
|
+
* a public method of the subclass polymorphically (Starlette →
|
|
118
|
+
* build_middleware_stack), by name convention (Pydantic → bytes_schema), or
|
|
119
|
+
* REQUIRE the member outright (fastify → CustomLoggerImpl.silent, via an
|
|
120
|
+
* implements chain ending in pino). The class def is matched in the method's
|
|
121
|
+
* own file; `implements` is walked only when the member sits inside the class
|
|
122
|
+
* def's own range (see _heritageReachesExternalBase — the Rust guard).
|
|
76
123
|
*/
|
|
77
124
|
function _classHasExternalBase(index, symbol) {
|
|
78
125
|
const lang = index.files.get(symbol.file)?.language;
|
|
126
|
+
// The implements hop shields only contract-SATISFIABLE members: an
|
|
127
|
+
// interface implementation must be public, so in explicit-visibility
|
|
128
|
+
// languages a package-private/non-pub member provably cannot implement
|
|
129
|
+
// any interface member (javac rejects it) — `implements Runnable` never
|
|
130
|
+
// shields a package-private helper. Implicit-public languages satisfy
|
|
131
|
+
// contracts with unmarked members; the caller's public-by-shape check
|
|
132
|
+
// already screened those. Compiler physics, not a heuristic.
|
|
133
|
+
const contractSatisfiable = langTraits(lang)?.implicitlyPublicMembers ||
|
|
134
|
+
(symbol.modifiers || []).includes('public');
|
|
79
135
|
const classDefs = (index.symbols.get(symbol.className) || []).filter(c =>
|
|
80
136
|
c.file === symbol.file && _CLASS_KINDS.includes(c.type));
|
|
81
|
-
return classDefs.some(cd =>
|
|
137
|
+
return classDefs.some(cd => _heritageReachesExternalBase(index, cd, lang,
|
|
138
|
+
contractSatisfiable &&
|
|
139
|
+
symbol.startLine >= cd.startLine && symbol.startLine <= cd.endLine));
|
|
82
140
|
}
|
|
83
141
|
|
|
84
142
|
/**
|
|
@@ -92,9 +150,12 @@ function _classHasExternalBase(index, symbol) {
|
|
|
92
150
|
* Rust `impl Trait for X`) AND a single project-wide method owner of the
|
|
93
151
|
* name (no in-project supertype defines it → the contract is external —
|
|
94
152
|
* the #210 ownerCount===1 rule).
|
|
95
|
-
* (B) a public-by-shape method whose class
|
|
96
|
-
*
|
|
97
|
-
*
|
|
153
|
+
* (B) a public-by-shape method whose class reaches an unresolved base
|
|
154
|
+
* through its heritage closure (extends chains walked transitively,
|
|
155
|
+
* plus the class's own `implements` clause when the member sits in the
|
|
156
|
+
* class body — fix #270). Private/underscore members are never
|
|
157
|
+
* external-contract surface, so a genuinely-dead one stays claimable
|
|
158
|
+
* (the fix #211 shape predicate).
|
|
98
159
|
* Data-driven, not language-keyed: classes without an `extends` clause (Go
|
|
99
160
|
* embedding, Rust structs / inherent impls) never trip (B), and Rust trait
|
|
100
161
|
* impls trip (A) via traitImpl — so new languages inherit correct behavior.
|
|
@@ -147,8 +208,7 @@ function nameOnlySelfRecursive(index, name) {
|
|
|
147
208
|
}
|
|
148
209
|
const { getCachedCalls } = require('./callers');
|
|
149
210
|
for (const f of files) {
|
|
150
|
-
|
|
151
|
-
try { calls = getCachedCalls(index, f); } catch { return false; }
|
|
211
|
+
const calls = getCachedCalls(index, f);
|
|
152
212
|
if (!calls) return false;
|
|
153
213
|
for (const call of calls) {
|
|
154
214
|
if (call.name !== name && call.resolvedName !== name &&
|
|
@@ -950,7 +1010,7 @@ function deadcode(index, options = {}) {
|
|
|
950
1010
|
// by module path, pytest plugins by registration) — zero
|
|
951
1011
|
// in-project references is not evidence of deadness there.
|
|
952
1012
|
const isExternalContract = classAuditSet.has(symbol.type)
|
|
953
|
-
?
|
|
1013
|
+
? _heritageReachesExternalBase(index, symbol, lang, false)
|
|
954
1014
|
: overridesOutOfTreeBase(index, symbol);
|
|
955
1015
|
if (isExternalContract && !options.includeExported) {
|
|
956
1016
|
excludedExternalContract++;
|
package/core/entrypoints.js
CHANGED
|
@@ -943,29 +943,27 @@ function detectEntrypoints(index, options = {}) {
|
|
|
943
943
|
// (3) Top-level invocation targets: any non-method call with no
|
|
944
944
|
// enclosingFunction is module-load-time, so its callee is reachable.
|
|
945
945
|
// We treat the callee as an entry point (the function being kicked off).
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
if (index.symbols.has(n)) allowed.add(n);
|
|
956
|
-
}
|
|
946
|
+
const calls = getCachedCalls(index, filePath);
|
|
947
|
+
if (calls && calls.length > 0) {
|
|
948
|
+
for (const c of calls) {
|
|
949
|
+
if (c.enclosingFunction != null) continue;
|
|
950
|
+
if (c.isMethod) continue;
|
|
951
|
+
// Resolve callee names (handles aliased imports)
|
|
952
|
+
const names = c.resolvedNames || (c.resolvedName ? [c.resolvedName] : [c.name]);
|
|
953
|
+
for (const n of names) {
|
|
954
|
+
if (index.symbols.has(n)) allowed.add(n);
|
|
957
955
|
}
|
|
958
|
-
|
|
959
|
-
// (4) Function containing a top-level `if (require.main === module) { ... }`
|
|
960
|
-
// wrapping calls — captured by treating any non-method call whose
|
|
961
|
-
// enclosingFunction body is at top level. The simplest detection:
|
|
962
|
-
// scan calls inside any function whose body contains the require.main
|
|
963
|
-
// guard. We approximate via the same `enclosingFunction` data: if a
|
|
964
|
-
// function contains top-level invocation-style entries, it's typically
|
|
965
|
-
// identified by the (3) check above. Skip explicit (4) detection here —
|
|
966
|
-
// (3) already covers `if (require.main === module) { main(); }`.
|
|
967
956
|
}
|
|
968
|
-
|
|
957
|
+
|
|
958
|
+
// (4) Function containing a top-level `if (require.main === module) { ... }`
|
|
959
|
+
// wrapping calls — captured by treating any non-method call whose
|
|
960
|
+
// enclosingFunction body is at top level. The simplest detection:
|
|
961
|
+
// scan calls inside any function whose body contains the require.main
|
|
962
|
+
// guard. We approximate via the same `enclosingFunction` data: if a
|
|
963
|
+
// function contains top-level invocation-style entries, it's typically
|
|
964
|
+
// identified by the (3) check above. Skip explicit (4) detection here —
|
|
965
|
+
// (3) already covers `if (require.main === module) { main(); }`.
|
|
966
|
+
}
|
|
969
967
|
|
|
970
968
|
// If we identified at least one specific entry, use that set.
|
|
971
969
|
// Otherwise fall back to permissive (null) so a CLI file with neither
|
|
@@ -1143,6 +1141,18 @@ function computeReachability(index) {
|
|
|
1143
1141
|
const langModuleCache = new Map();
|
|
1144
1142
|
for (const [, symbols] of index.symbols) {
|
|
1145
1143
|
for (const symbol of symbols) {
|
|
1144
|
+
// JS/TS module-scope object registries are invoked through dynamic
|
|
1145
|
+
// property access (HANDLERS[command](...)). There may be no static
|
|
1146
|
+
// incoming edge to discover. Treat these members as conservative
|
|
1147
|
+
// roots so deadcode never presents a dynamically reachable handler
|
|
1148
|
+
// as safe to remove; callees then flow through the normal BFS.
|
|
1149
|
+
if (symbol.registryMember) {
|
|
1150
|
+
const registryKey = symbolKey(symbol.file, symbol.startLine);
|
|
1151
|
+
if (!reachable.has(registryKey)) {
|
|
1152
|
+
reachable.add(registryKey);
|
|
1153
|
+
queue.push(symbol);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1146
1156
|
const fileEntry = index.files.get(symbol.file);
|
|
1147
1157
|
if (!fileEntry) continue;
|
|
1148
1158
|
const lang = fileEntry.language;
|
|
@@ -1183,12 +1193,7 @@ function computeReachability(index) {
|
|
|
1183
1193
|
for (const [filePath, fileEntry] of index.files) {
|
|
1184
1194
|
const lang = fileEntry.language;
|
|
1185
1195
|
if (lang !== 'javascript' && lang !== 'typescript' && lang !== 'tsx') continue;
|
|
1186
|
-
|
|
1187
|
-
try {
|
|
1188
|
-
calls = getCachedCalls(index, filePath);
|
|
1189
|
-
} catch (_e) {
|
|
1190
|
-
continue;
|
|
1191
|
-
}
|
|
1196
|
+
const calls = getCachedCalls(index, filePath);
|
|
1192
1197
|
if (!calls || calls.length === 0) continue;
|
|
1193
1198
|
for (const call of calls) {
|
|
1194
1199
|
// Top-level call: AST recorded enclosingFunction === null/undefined.
|
package/core/execute.js
CHANGED
|
@@ -241,7 +241,6 @@ function checkFilePatternMatch(index, filePattern) {
|
|
|
241
241
|
if (fileEntry.relativePath.includes(filePattern)) return null;
|
|
242
242
|
}
|
|
243
243
|
// Suggest similar directories/files to help user refine
|
|
244
|
-
const patternLower = filePattern.toLowerCase();
|
|
245
244
|
const basename = filePattern.split('/').pop().toLowerCase();
|
|
246
245
|
const suggestions = new Set();
|
|
247
246
|
for (const [, fileEntry] of index.files) {
|
|
@@ -493,6 +492,7 @@ const HANDLERS = {
|
|
|
493
492
|
example: (index, p) => {
|
|
494
493
|
const err = requireName(p.name);
|
|
495
494
|
if (err) return { ok: false, error: err };
|
|
495
|
+
const targetWasHandle = looksLikeHandle(p.name);
|
|
496
496
|
applyClassMethodSyntax(p);
|
|
497
497
|
const fileErr = checkFilePatternMatch(index, p.file);
|
|
498
498
|
if (fileErr) return { ok: false, error: fileErr };
|
|
@@ -501,6 +501,12 @@ const HANDLERS = {
|
|
|
501
501
|
const result = index.example(p.name, {
|
|
502
502
|
file: p.file,
|
|
503
503
|
className: p.className,
|
|
504
|
+
line: p.line,
|
|
505
|
+
// Preserve legacy plain-name `--file` as a call-site scope while
|
|
506
|
+
// letting a stable handle use its file component purely as exact
|
|
507
|
+
// target identity. Without this distinction, handles could never
|
|
508
|
+
// find examples outside the definition file.
|
|
509
|
+
callSiteFile: !targetWasHandle ? p.file : null,
|
|
504
510
|
diverse: !!p.diverse,
|
|
505
511
|
top: num(p.top, undefined),
|
|
506
512
|
// MEDIUM-8: thread includeTests so test-file callers are included
|
|
@@ -583,7 +589,6 @@ const HANDLERS = {
|
|
|
583
589
|
in: p.in,
|
|
584
590
|
file: p.file,
|
|
585
591
|
deep: !!p.deep,
|
|
586
|
-
sampleSize: num(p.limit, undefined),
|
|
587
592
|
});
|
|
588
593
|
return { ok: true, result };
|
|
589
594
|
},
|
|
@@ -832,10 +837,21 @@ const HANDLERS = {
|
|
|
832
837
|
// splitting would shear the filename at the dot into
|
|
833
838
|
// className='helper', name='go' and silently return nothing
|
|
834
839
|
// (fix #239, G3-go-measured). Mirror the engine's file-path test.
|
|
835
|
-
const
|
|
840
|
+
const testsTargetIsHandle = looksLikeHandle(p.name);
|
|
841
|
+
const testsTargetIsFile = !testsTargetIsHandle && typeof p.name === 'string' && (
|
|
836
842
|
p.name.includes('/') || p.name.includes('\\') ||
|
|
837
843
|
/\.(js|ts|py|go|java|rs)$/.test(p.name));
|
|
838
844
|
if (!testsTargetIsFile) applyClassMethodSyntax(p);
|
|
845
|
+
if (testsTargetIsHandle && p.line) {
|
|
846
|
+
const line = Number(p.line);
|
|
847
|
+
const pinned = (index.symbols.get(p.name) || []).filter(d =>
|
|
848
|
+
d.startLine === line && (!p.file || d.relativePath?.includes(p.file)));
|
|
849
|
+
if (pinned.length === 1 && !p.className && pinned[0].className) {
|
|
850
|
+
p.className = pinned[0].className;
|
|
851
|
+
}
|
|
852
|
+
const pinErr = checkDefinitionPin(index, p);
|
|
853
|
+
if (pinErr) return { ok: false, error: pinErr };
|
|
854
|
+
}
|
|
839
855
|
if (p.file) {
|
|
840
856
|
const fileErr = checkFilePatternMatch(index, p.file);
|
|
841
857
|
if (fileErr) return { ok: false, error: fileErr };
|