ucn 4.1.3 → 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 +51 -6
- package/core/callers.js +1709 -162
- package/core/check.js +107 -19
- package/core/confidence.js +29 -16
- package/core/deadcode.js +1 -2
- 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 +61 -4
- 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
|
@@ -208,8 +208,7 @@ function nameOnlySelfRecursive(index, name) {
|
|
|
208
208
|
}
|
|
209
209
|
const { getCachedCalls } = require('./callers');
|
|
210
210
|
for (const f of files) {
|
|
211
|
-
|
|
212
|
-
try { calls = getCachedCalls(index, f); } catch { return false; }
|
|
211
|
+
const calls = getCachedCalls(index, f);
|
|
213
212
|
if (!calls) return false;
|
|
214
213
|
for (const call of calls) {
|
|
215
214
|
if (call.name !== name && call.resolvedName !== name &&
|
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 };
|
package/core/graph-build.js
CHANGED
|
@@ -260,6 +260,39 @@ function buildInheritanceGraph(index) {
|
|
|
260
260
|
const alias = fileEntry.importAliases.find(a => a.local === parent);
|
|
261
261
|
if (alias && classNames.has(alias.original)) return alias.original;
|
|
262
262
|
}
|
|
263
|
+
// Qualified structural parent: `class CustomCommand(
|
|
264
|
+
// click.Command)`. The symbol table owns bare class names,
|
|
265
|
+
// so keeping `click.Command` makes the subclass invisible
|
|
266
|
+
// to dispatch/reachability. Normalize only when the
|
|
267
|
+
// qualifier is an actual project module import and its
|
|
268
|
+
// bounded import graph reaches exactly one matching class
|
|
269
|
+
// definition—never by suffix alone.
|
|
270
|
+
if (langTraits(fileEntry.language)?.typeSystem === 'structural' && parent.includes('.')) {
|
|
271
|
+
const pieces = parent.split('.');
|
|
272
|
+
const localModule = pieces[0];
|
|
273
|
+
const className = pieces[pieces.length - 1];
|
|
274
|
+
const binding = (fileEntry.importBindings || []).find(b => b.name === localModule);
|
|
275
|
+
const rel = binding && fileEntry.moduleResolved?.[binding.module];
|
|
276
|
+
if (rel && classNames.has(className)) {
|
|
277
|
+
const moduleFile = path.join(index.root, rel);
|
|
278
|
+
const candidateFiles = new Set((index.symbols.get(className) || [])
|
|
279
|
+
.filter(d => ['class', 'interface', 'struct', 'trait', 'record'].includes(d.type))
|
|
280
|
+
.map(d => d.file).filter(Boolean));
|
|
281
|
+
const reached = new Set();
|
|
282
|
+
const queue = [{ file: moduleFile, depth: 0 }];
|
|
283
|
+
const seen = new Set();
|
|
284
|
+
while (queue.length > 0) {
|
|
285
|
+
const cur = queue.shift();
|
|
286
|
+
if (!cur.file || seen.has(cur.file) || cur.depth > 4) continue;
|
|
287
|
+
seen.add(cur.file);
|
|
288
|
+
if (candidateFiles.has(cur.file)) reached.add(cur.file);
|
|
289
|
+
for (const next of index.importGraph.get(cur.file) || []) {
|
|
290
|
+
queue.push({ file: next, depth: cur.depth + 1 });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (reached.size === 1) return className;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
263
296
|
return parent;
|
|
264
297
|
});
|
|
265
298
|
|