ucn 3.8.0 → 3.8.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 +17 -1
- package/cli/index.js +34 -6
- package/core/callers.js +60 -13
- package/core/confidence.js +116 -0
- package/core/deadcode.js +10 -18
- package/core/entrypoints.js +416 -0
- package/core/execute.js +16 -2
- package/core/output.js +105 -5
- package/core/project.js +42 -3
- package/core/registry.js +3 -1
- package/languages/go.js +20 -0
- package/languages/javascript.js +48 -0
- package/languages/python.js +40 -0
- package/languages/rust.js +62 -0
- package/mcp/server.js +13 -2
- package/package.json +1 -1
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/entrypoints.js - Framework entry point detection
|
|
3
|
+
*
|
|
4
|
+
* Detects functions registered as framework handlers (HTTP routes, DI beans,
|
|
5
|
+
* job schedulers, etc.) that are invoked by the framework at runtime, not by
|
|
6
|
+
* user code. These functions should never be flagged as dead code.
|
|
7
|
+
*
|
|
8
|
+
* Two detection methods:
|
|
9
|
+
* 1. Decorator/modifier matching (Python, Java, Rust, JS/TS decorators)
|
|
10
|
+
* 2. Call-pattern matching (Express routes, Gin handlers, Go http.HandleFunc)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const { getCachedCalls } = require('./callers');
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// FRAMEWORK PATTERNS
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
const JS_LANGS = new Set(['javascript', 'typescript', 'tsx']);
|
|
22
|
+
|
|
23
|
+
const FRAMEWORK_PATTERNS = [
|
|
24
|
+
// ── HTTP Routes ─────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
// Express / Fastify / Koa (JS/TS) — call-pattern: app.get('/path', handler)
|
|
27
|
+
{
|
|
28
|
+
id: 'express-route',
|
|
29
|
+
languages: JS_LANGS,
|
|
30
|
+
type: 'http',
|
|
31
|
+
framework: 'express',
|
|
32
|
+
detection: 'callPattern',
|
|
33
|
+
receiverPattern: /^(app|router|server|fastify)$/i,
|
|
34
|
+
methodPattern: /^(get|post|put|delete|patch|all|use|options|head)$/,
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// NestJS (JS/TS) — decorators: @Get(), @Post(), @Controller(), etc.
|
|
38
|
+
{
|
|
39
|
+
id: 'nestjs-handler',
|
|
40
|
+
languages: JS_LANGS,
|
|
41
|
+
type: 'http',
|
|
42
|
+
framework: 'nestjs',
|
|
43
|
+
detection: 'decorator',
|
|
44
|
+
pattern: /^(Get|Post|Put|Delete|Patch|Options|Head|All|Controller|Injectable|Module)$/,
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// FastAPI (Python) — decorators: @app.get('/path'), @router.post('/path')
|
|
48
|
+
{
|
|
49
|
+
id: 'fastapi-route',
|
|
50
|
+
languages: new Set(['python']),
|
|
51
|
+
type: 'http',
|
|
52
|
+
framework: 'fastapi',
|
|
53
|
+
detection: 'decorator',
|
|
54
|
+
pattern: /^(app|router)\.(get|post|put|delete|patch|options|head)/,
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// Flask (Python) — decorators: @app.route('/path'), @bp.get('/path')
|
|
58
|
+
{
|
|
59
|
+
id: 'flask-route',
|
|
60
|
+
languages: new Set(['python']),
|
|
61
|
+
type: 'http',
|
|
62
|
+
framework: 'flask',
|
|
63
|
+
detection: 'decorator',
|
|
64
|
+
pattern: /^(app|bp|blueprint)\.(route|get|post|put|delete|patch)/,
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// Django (Python) — decorators: @api_view, @action, @permission_classes
|
|
68
|
+
{
|
|
69
|
+
id: 'django-view',
|
|
70
|
+
languages: new Set(['python']),
|
|
71
|
+
type: 'http',
|
|
72
|
+
framework: 'django',
|
|
73
|
+
detection: 'decorator',
|
|
74
|
+
pattern: /^(api_view|action|permission_classes|login_required|csrf_exempt)/,
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// Spring HTTP (Java) — modifiers (lowercased annotations)
|
|
78
|
+
{
|
|
79
|
+
id: 'spring-mapping',
|
|
80
|
+
languages: new Set(['java']),
|
|
81
|
+
type: 'http',
|
|
82
|
+
framework: 'spring',
|
|
83
|
+
detection: 'modifier',
|
|
84
|
+
pattern: /^(getmapping|postmapping|putmapping|deletemapping|patchmapping|requestmapping)$/,
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
// Gin (Go) — call-pattern: router.GET('/path', handler)
|
|
88
|
+
{
|
|
89
|
+
id: 'gin-route',
|
|
90
|
+
languages: new Set(['go']),
|
|
91
|
+
type: 'http',
|
|
92
|
+
framework: 'gin',
|
|
93
|
+
detection: 'callPattern',
|
|
94
|
+
receiverPattern: /^(router|r|g|group|engine|api|v\d+)$/i,
|
|
95
|
+
methodPattern: /^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|Any|Handle)$/,
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// Go net/http — call-pattern: http.HandleFunc('/path', handler)
|
|
99
|
+
{
|
|
100
|
+
id: 'go-http',
|
|
101
|
+
languages: new Set(['go']),
|
|
102
|
+
type: 'http',
|
|
103
|
+
framework: 'net/http',
|
|
104
|
+
detection: 'callPattern',
|
|
105
|
+
receiverPattern: /^(http|mux|serveMux)$/i,
|
|
106
|
+
methodPattern: /^(HandleFunc|Handle)$/,
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// Actix (Rust) — modifiers from #[get("/path")], #[post("/path")]
|
|
110
|
+
{
|
|
111
|
+
id: 'actix-route',
|
|
112
|
+
languages: new Set(['rust']),
|
|
113
|
+
type: 'http',
|
|
114
|
+
framework: 'actix',
|
|
115
|
+
detection: 'modifier',
|
|
116
|
+
pattern: /^(get|post|put|delete|patch|actix_web::main|actix_web::test)$/,
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// ── Dependency Injection ────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
// Spring DI (Java)
|
|
122
|
+
{
|
|
123
|
+
id: 'spring-di',
|
|
124
|
+
languages: new Set(['java']),
|
|
125
|
+
type: 'di',
|
|
126
|
+
framework: 'spring',
|
|
127
|
+
detection: 'modifier',
|
|
128
|
+
pattern: /^(bean|component|service|controller|repository|configuration|restcontroller)$/,
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
// ── Job Schedulers ──────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
// Spring Scheduled (Java)
|
|
134
|
+
{
|
|
135
|
+
id: 'spring-jobs',
|
|
136
|
+
languages: new Set(['java']),
|
|
137
|
+
type: 'jobs',
|
|
138
|
+
framework: 'spring',
|
|
139
|
+
detection: 'modifier',
|
|
140
|
+
pattern: /^(scheduled|eventlistener|async)$/,
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
// Celery (Python)
|
|
144
|
+
{
|
|
145
|
+
id: 'celery-task',
|
|
146
|
+
languages: new Set(['python']),
|
|
147
|
+
type: 'jobs',
|
|
148
|
+
framework: 'celery',
|
|
149
|
+
detection: 'decorator',
|
|
150
|
+
pattern: /^(app\.task|shared_task|celery\.task)/,
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// ── Test Frameworks ─────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
// pytest fixtures (Python)
|
|
156
|
+
{
|
|
157
|
+
id: 'pytest-fixture',
|
|
158
|
+
languages: new Set(['python']),
|
|
159
|
+
type: 'test',
|
|
160
|
+
framework: 'pytest',
|
|
161
|
+
detection: 'decorator',
|
|
162
|
+
pattern: /^pytest\.fixture/,
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
// ── Runtime ─────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
// Tokio (Rust)
|
|
168
|
+
{
|
|
169
|
+
id: 'tokio-main',
|
|
170
|
+
languages: new Set(['rust']),
|
|
171
|
+
type: 'runtime',
|
|
172
|
+
framework: 'tokio',
|
|
173
|
+
detection: 'modifier',
|
|
174
|
+
pattern: /^tokio::main$/,
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
// ── Catch-all fallbacks ─────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
// Python: any decorator with '.' (attribute access) — framework registration heuristic
|
|
180
|
+
// Catches @app.route, @router.get, @celery.task, @something.hook, etc.
|
|
181
|
+
// Placed last so specific patterns match first (for better type/framework labeling).
|
|
182
|
+
{
|
|
183
|
+
id: 'python-dotted-decorator',
|
|
184
|
+
languages: new Set(['python']),
|
|
185
|
+
type: 'events',
|
|
186
|
+
framework: 'unknown',
|
|
187
|
+
detection: 'decorator',
|
|
188
|
+
pattern: /\./,
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
// Java: any non-standard annotation (not a keyword modifier or standard JDK annotation)
|
|
192
|
+
// Catches @Bean, @Scheduled, @EventListener, @Transactional, etc.
|
|
193
|
+
// Placed last so specific patterns match first.
|
|
194
|
+
{
|
|
195
|
+
id: 'java-custom-annotation',
|
|
196
|
+
languages: new Set(['java']),
|
|
197
|
+
type: 'di',
|
|
198
|
+
framework: 'unknown',
|
|
199
|
+
detection: 'modifier',
|
|
200
|
+
pattern: /^(?!public$|private$|protected$|static$|final$|abstract$|synchronized$|native$|default$|override$|deprecated$|suppresswarnings$|functionalinterface$|safevarargs$)/,
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
// ============================================================================
|
|
205
|
+
// DETECTION
|
|
206
|
+
// ============================================================================
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Check if a symbol matches any decorator/modifier-based framework pattern.
|
|
210
|
+
* @param {object} symbol - Symbol from the symbol table
|
|
211
|
+
* @param {string} language - File language
|
|
212
|
+
* @returns {{ pattern: object, matchedOn: string }|null}
|
|
213
|
+
*/
|
|
214
|
+
function matchDecoratorOrModifier(symbol, language) {
|
|
215
|
+
const decorators = symbol.decorators || [];
|
|
216
|
+
const modifiers = symbol.modifiers || [];
|
|
217
|
+
|
|
218
|
+
for (const fp of FRAMEWORK_PATTERNS) {
|
|
219
|
+
if (!fp.languages.has(language)) continue;
|
|
220
|
+
|
|
221
|
+
if (fp.detection === 'decorator') {
|
|
222
|
+
const matched = decorators.find(d => fp.pattern.test(d));
|
|
223
|
+
if (matched) return { pattern: fp, matchedOn: `@${matched}` };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (fp.detection === 'modifier') {
|
|
227
|
+
const matched = modifiers.find(m => fp.pattern.test(m));
|
|
228
|
+
if (matched) return { pattern: fp, matchedOn: `@${matched}` };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Build a map of symbol names used as callbacks in framework route-registration calls.
|
|
237
|
+
* Scans the calls cache for call-pattern-based framework detection.
|
|
238
|
+
* @param {object} index - ProjectIndex
|
|
239
|
+
* @returns {Map<string, { framework, type, patternId, method, file, line }>}
|
|
240
|
+
*/
|
|
241
|
+
function buildCallbackEntrypointMap(index) {
|
|
242
|
+
const callPatterns = FRAMEWORK_PATTERNS.filter(p => p.detection === 'callPattern');
|
|
243
|
+
if (callPatterns.length === 0) return new Map();
|
|
244
|
+
|
|
245
|
+
const result = new Map(); // name -> info
|
|
246
|
+
|
|
247
|
+
for (const [filePath, fileEntry] of index.files) {
|
|
248
|
+
const lang = fileEntry.language;
|
|
249
|
+
const relevantPatterns = callPatterns.filter(p => p.languages.has(lang));
|
|
250
|
+
if (relevantPatterns.length === 0) continue;
|
|
251
|
+
|
|
252
|
+
const calls = getCachedCalls(index, filePath);
|
|
253
|
+
if (!calls) continue;
|
|
254
|
+
|
|
255
|
+
// Pass 1: find route-registration calls, index by line
|
|
256
|
+
// Match both method calls (obj.method) and package-qualified calls (pkg.Func)
|
|
257
|
+
const routeLines = new Map(); // line -> { pattern, call }
|
|
258
|
+
for (const call of calls) {
|
|
259
|
+
if (!call.receiver) continue;
|
|
260
|
+
for (const pattern of relevantPatterns) {
|
|
261
|
+
if (pattern.receiverPattern.test(call.receiver) &&
|
|
262
|
+
pattern.methodPattern.test(call.name)) {
|
|
263
|
+
routeLines.set(call.line, { pattern, call });
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (routeLines.size === 0) continue;
|
|
270
|
+
|
|
271
|
+
// Pass 2: find callbacks on route-registration lines
|
|
272
|
+
for (const call of calls) {
|
|
273
|
+
if (!call.isFunctionReference && !call.isPotentialCallback) continue;
|
|
274
|
+
const route = routeLines.get(call.line);
|
|
275
|
+
if (!route) continue;
|
|
276
|
+
|
|
277
|
+
// This callback is registered as a framework handler
|
|
278
|
+
if (!result.has(call.name)) {
|
|
279
|
+
result.set(call.name, {
|
|
280
|
+
framework: route.pattern.framework,
|
|
281
|
+
type: route.pattern.type,
|
|
282
|
+
patternId: route.pattern.id,
|
|
283
|
+
method: route.call.name.toUpperCase(),
|
|
284
|
+
file: filePath,
|
|
285
|
+
line: call.line,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Detect all framework entry points in the project.
|
|
296
|
+
*
|
|
297
|
+
* @param {object} index - ProjectIndex
|
|
298
|
+
* @param {object} [options]
|
|
299
|
+
* @param {string} [options.type] - Filter by type (http, jobs, di, test, runtime)
|
|
300
|
+
* @param {string} [options.framework] - Filter by framework name(s), comma-separated
|
|
301
|
+
* @param {string} [options.file] - Filter by file path pattern
|
|
302
|
+
* @returns {Array<{ name, file, line, type, framework, patternId, evidence, confidence }>}
|
|
303
|
+
*/
|
|
304
|
+
function detectEntrypoints(index, options = {}) {
|
|
305
|
+
// Build callback entrypoint map (call-pattern detection)
|
|
306
|
+
const callbackMap = buildCallbackEntrypointMap(index);
|
|
307
|
+
|
|
308
|
+
const results = [];
|
|
309
|
+
const seen = new Set(); // file:line:name dedup key
|
|
310
|
+
|
|
311
|
+
// 1. Scan all symbols for decorator/modifier-based patterns
|
|
312
|
+
for (const [name, symbols] of index.symbols) {
|
|
313
|
+
for (const symbol of symbols) {
|
|
314
|
+
const fileEntry = index.files.get(symbol.file);
|
|
315
|
+
if (!fileEntry) continue;
|
|
316
|
+
|
|
317
|
+
const match = matchDecoratorOrModifier(symbol, fileEntry.language);
|
|
318
|
+
if (match) {
|
|
319
|
+
const key = `${symbol.file}:${symbol.startLine}:${name}`;
|
|
320
|
+
if (seen.has(key)) continue;
|
|
321
|
+
seen.add(key);
|
|
322
|
+
|
|
323
|
+
results.push({
|
|
324
|
+
name,
|
|
325
|
+
file: symbol.relativePath || symbol.file,
|
|
326
|
+
absoluteFile: symbol.file,
|
|
327
|
+
line: symbol.startLine,
|
|
328
|
+
type: match.pattern.type,
|
|
329
|
+
framework: match.pattern.framework,
|
|
330
|
+
patternId: match.pattern.id,
|
|
331
|
+
evidence: [match.matchedOn],
|
|
332
|
+
confidence: 0.95,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// 2. Add call-pattern-based entry points (route handlers)
|
|
339
|
+
for (const [name, info] of callbackMap) {
|
|
340
|
+
const fileEntry = index.files.get(info.file);
|
|
341
|
+
const relPath = fileEntry?.relativePath || info.file;
|
|
342
|
+
const key = `${info.file}:${info.line}:${name}`;
|
|
343
|
+
if (seen.has(key)) continue;
|
|
344
|
+
seen.add(key);
|
|
345
|
+
|
|
346
|
+
results.push({
|
|
347
|
+
name,
|
|
348
|
+
file: relPath,
|
|
349
|
+
absoluteFile: info.file,
|
|
350
|
+
line: info.line,
|
|
351
|
+
type: info.type,
|
|
352
|
+
framework: info.framework,
|
|
353
|
+
patternId: info.patternId,
|
|
354
|
+
evidence: [`${info.method} route handler`],
|
|
355
|
+
confidence: 0.90,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Apply filters
|
|
360
|
+
let filtered = results;
|
|
361
|
+
|
|
362
|
+
if (options.type) {
|
|
363
|
+
filtered = filtered.filter(e => e.type === options.type);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (options.framework) {
|
|
367
|
+
const frameworks = new Set(options.framework.split(',').map(s => s.trim().toLowerCase()));
|
|
368
|
+
filtered = filtered.filter(e => frameworks.has(e.framework.toLowerCase()));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (options.file) {
|
|
372
|
+
filtered = filtered.filter(e => e.file.includes(options.file));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Sort by file, then line
|
|
376
|
+
filtered.sort((a, b) => {
|
|
377
|
+
if (a.file !== b.file) return a.file.localeCompare(b.file);
|
|
378
|
+
return a.line - b.line;
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
return filtered;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Check if a specific symbol is a framework entry point.
|
|
386
|
+
* Used by deadcode to exclude framework-registered functions.
|
|
387
|
+
*
|
|
388
|
+
* @param {object} symbol - Symbol from the symbol table
|
|
389
|
+
* @param {object} index - ProjectIndex
|
|
390
|
+
* @returns {boolean}
|
|
391
|
+
*/
|
|
392
|
+
function isFrameworkEntrypoint(symbol, index) {
|
|
393
|
+
const fileEntry = index.files.get(symbol.file);
|
|
394
|
+
if (!fileEntry) return false;
|
|
395
|
+
|
|
396
|
+
// Fast path: check decorator/modifier patterns (no index scan needed)
|
|
397
|
+
if (matchDecoratorOrModifier(symbol, fileEntry.language)) {
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Slow path: check call-pattern patterns (needs callback map)
|
|
402
|
+
// Build and cache on first use
|
|
403
|
+
if (!index._callbackEntrypointMap) {
|
|
404
|
+
index._callbackEntrypointMap = buildCallbackEntrypointMap(index);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return index._callbackEntrypointMap.has(symbol.name);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
module.exports = {
|
|
411
|
+
FRAMEWORK_PATTERNS,
|
|
412
|
+
detectEntrypoints,
|
|
413
|
+
isFrameworkEntrypoint,
|
|
414
|
+
matchDecoratorOrModifier,
|
|
415
|
+
buildCallbackEntrypointMap,
|
|
416
|
+
};
|
package/core/execute.js
CHANGED
|
@@ -182,6 +182,7 @@ const HANDLERS = {
|
|
|
182
182
|
exclude: toExcludeArray(p.exclude),
|
|
183
183
|
maxCallers: num(p.top, undefined),
|
|
184
184
|
maxCallees: num(p.top, undefined),
|
|
185
|
+
minConfidence: num(p.minConfidence, 0),
|
|
185
186
|
});
|
|
186
187
|
if (!result) {
|
|
187
188
|
// Give better error if file/className filter is the problem
|
|
@@ -201,7 +202,7 @@ const HANDLERS = {
|
|
|
201
202
|
}
|
|
202
203
|
return { ok: false, error: `Symbol "${p.name}" not found.` };
|
|
203
204
|
}
|
|
204
|
-
return { ok: true, result };
|
|
205
|
+
return { ok: true, result, showConfidence: !!p.showConfidence };
|
|
205
206
|
},
|
|
206
207
|
|
|
207
208
|
context: (index, p) => {
|
|
@@ -218,9 +219,10 @@ const HANDLERS = {
|
|
|
218
219
|
file: p.file,
|
|
219
220
|
className: p.className,
|
|
220
221
|
exclude: toExcludeArray(p.exclude),
|
|
222
|
+
minConfidence: num(p.minConfidence, 0),
|
|
221
223
|
});
|
|
222
224
|
if (!result) return { ok: false, error: `Symbol "${p.name}" not found.` };
|
|
223
|
-
return { ok: true, result };
|
|
225
|
+
return { ok: true, result, showConfidence: !!p.showConfidence };
|
|
224
226
|
},
|
|
225
227
|
|
|
226
228
|
impact: (index, p) => {
|
|
@@ -568,6 +570,18 @@ const HANDLERS = {
|
|
|
568
570
|
return { ok: true, result, note };
|
|
569
571
|
},
|
|
570
572
|
|
|
573
|
+
entrypoints: (index, p) => {
|
|
574
|
+
const fileErr = checkFilePatternMatch(index, p.file);
|
|
575
|
+
if (fileErr) return { ok: false, error: fileErr };
|
|
576
|
+
const { detectEntrypoints } = require('./entrypoints');
|
|
577
|
+
const result = detectEntrypoints(index, {
|
|
578
|
+
type: p.type,
|
|
579
|
+
framework: p.framework,
|
|
580
|
+
file: p.file,
|
|
581
|
+
});
|
|
582
|
+
return { ok: true, result };
|
|
583
|
+
},
|
|
584
|
+
|
|
571
585
|
// ── Extracting Code ─────────────────────────────────────────────────
|
|
572
586
|
|
|
573
587
|
fn: (index, p) => {
|
package/core/output.js
CHANGED
|
@@ -348,7 +348,8 @@ function formatContextJson(context) {
|
|
|
348
348
|
file: c.relativePath || c.file,
|
|
349
349
|
line: c.line,
|
|
350
350
|
expression: c.content, // FULL expression
|
|
351
|
-
callerName: c.callerName
|
|
351
|
+
callerName: c.callerName,
|
|
352
|
+
...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
|
|
352
353
|
})),
|
|
353
354
|
callees: callees.map(c => ({
|
|
354
355
|
name: c.name,
|
|
@@ -356,7 +357,8 @@ function formatContextJson(context) {
|
|
|
356
357
|
file: c.relativePath || c.file,
|
|
357
358
|
line: c.startLine,
|
|
358
359
|
params: c.params, // FULL params
|
|
359
|
-
weight: c.weight || 'normal' // Dependency weight: core, setup, utility
|
|
360
|
+
weight: c.weight || 'normal', // Dependency weight: core, setup, utility
|
|
361
|
+
...(c.confidence != null && { confidence: c.confidence, resolution: c.resolution }),
|
|
360
362
|
})),
|
|
361
363
|
...(context.warnings && { warnings: context.warnings })
|
|
362
364
|
}
|
|
@@ -1512,6 +1514,9 @@ function formatAbout(about, options = {}) {
|
|
|
1512
1514
|
lines.push(` Note: ${w.message}`);
|
|
1513
1515
|
}
|
|
1514
1516
|
}
|
|
1517
|
+
if (about.confidenceFiltered) {
|
|
1518
|
+
lines.push(` Note: ${about.confidenceFiltered} edge(s) below confidence threshold hidden`);
|
|
1519
|
+
}
|
|
1515
1520
|
|
|
1516
1521
|
// Usage summary
|
|
1517
1522
|
lines.push('');
|
|
@@ -1519,6 +1524,7 @@ function formatAbout(about, options = {}) {
|
|
|
1519
1524
|
lines.push(` ${about.usages.calls} calls, ${about.usages.imports} imports, ${about.usages.references} references`);
|
|
1520
1525
|
|
|
1521
1526
|
// Callers
|
|
1527
|
+
const showConf = options.showConfidence || false;
|
|
1522
1528
|
let aboutTruncated = false;
|
|
1523
1529
|
if (about.callers.total > 0) {
|
|
1524
1530
|
lines.push('');
|
|
@@ -1532,6 +1538,9 @@ function formatAbout(about, options = {}) {
|
|
|
1532
1538
|
const caller = c.callerName ? `[${c.callerName}]` : '';
|
|
1533
1539
|
lines.push(` ${c.file}:${c.line} ${caller}`);
|
|
1534
1540
|
lines.push(` ${c.expression}`);
|
|
1541
|
+
if (showConf && c.confidence != null) {
|
|
1542
|
+
lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
|
|
1543
|
+
}
|
|
1535
1544
|
}
|
|
1536
1545
|
}
|
|
1537
1546
|
|
|
@@ -1547,6 +1556,9 @@ function formatAbout(about, options = {}) {
|
|
|
1547
1556
|
for (const c of about.callees.top) {
|
|
1548
1557
|
const weight = c.weight && c.weight !== 'normal' ? ` [${c.weight}]` : '';
|
|
1549
1558
|
lines.push(` ${c.name}${weight} - ${c.file}:${c.line} (${c.callCount}x)`);
|
|
1559
|
+
if (showConf && c.confidence != null) {
|
|
1560
|
+
lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
|
|
1561
|
+
}
|
|
1550
1562
|
|
|
1551
1563
|
// Inline expansion: show first 3 lines of callee code
|
|
1552
1564
|
if (expand && root && c.file && c.startLine) {
|
|
@@ -1954,6 +1966,7 @@ function formatContext(ctx, options = {}) {
|
|
|
1954
1966
|
const notes = [];
|
|
1955
1967
|
if (ctx.meta.dynamicImports) { const dn = dynamicImportsNote(ctx.meta.dynamicImports, ctx.meta); if (dn) notes.push(dn); }
|
|
1956
1968
|
if (ctx.meta.uncertain) notes.push(`${ctx.meta.uncertain} uncertain call(s) skipped`);
|
|
1969
|
+
if (ctx.meta.confidenceFiltered) notes.push(`${ctx.meta.confidenceFiltered} edge(s) below confidence threshold hidden`);
|
|
1957
1970
|
if (notes.length) {
|
|
1958
1971
|
const uncertainSuffix = ctx.meta.uncertain && options.uncertainHint ? ` — ${options.uncertainHint}` : '';
|
|
1959
1972
|
lines.push(` Note: ${notes.join(', ')}${uncertainSuffix}`);
|
|
@@ -1970,12 +1983,16 @@ function formatContext(ctx, options = {}) {
|
|
|
1970
1983
|
}
|
|
1971
1984
|
}
|
|
1972
1985
|
|
|
1986
|
+
const showConf = options.showConfidence || false;
|
|
1973
1987
|
const callers = ctx.callers || [];
|
|
1974
1988
|
lines.push(`\nCALLERS (${callers.length}):`);
|
|
1975
1989
|
for (const c of callers) {
|
|
1976
1990
|
const callerName = c.callerName ? ` [${c.callerName}]` : '';
|
|
1977
1991
|
lines.push(` [${itemNum}] ${c.relativePath}:${c.line}${callerName}`);
|
|
1978
1992
|
lines.push(` ${c.content.trim()}`);
|
|
1993
|
+
if (showConf && c.confidence != null) {
|
|
1994
|
+
lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
|
|
1995
|
+
}
|
|
1979
1996
|
expandable.push({
|
|
1980
1997
|
num: itemNum++,
|
|
1981
1998
|
type: 'caller',
|
|
@@ -1999,6 +2016,9 @@ function formatContext(ctx, options = {}) {
|
|
|
1999
2016
|
for (const c of callees) {
|
|
2000
2017
|
const weight = c.weight && c.weight !== 'normal' ? ` [${c.weight}]` : '';
|
|
2001
2018
|
lines.push(` [${itemNum}] ${c.name}${weight} - ${c.relativePath}:${c.startLine}`);
|
|
2019
|
+
if (showConf && c.confidence != null) {
|
|
2020
|
+
lines.push(` confidence: ${c.confidence.toFixed(2)} (${c.resolution})`);
|
|
2021
|
+
}
|
|
2002
2022
|
expandable.push({
|
|
2003
2023
|
num: itemNum++,
|
|
2004
2024
|
type: 'callee',
|
|
@@ -2302,10 +2322,12 @@ function formatCircularDeps(result) {
|
|
|
2302
2322
|
lines.push(`Filtered to cycles involving: ${result.fileFilter}`);
|
|
2303
2323
|
}
|
|
2304
2324
|
|
|
2325
|
+
const scannedCount = result.filesWithImports || result.totalFiles;
|
|
2326
|
+
|
|
2305
2327
|
if (result.cycles.length === 0) {
|
|
2306
2328
|
lines.push('');
|
|
2307
2329
|
lines.push('No circular dependencies found.');
|
|
2308
|
-
lines.push(`Scanned ${
|
|
2330
|
+
lines.push(`Scanned ${scannedCount} files with import relationships.`);
|
|
2309
2331
|
return lines.join('\n');
|
|
2310
2332
|
}
|
|
2311
2333
|
|
|
@@ -2318,7 +2340,7 @@ function formatCircularDeps(result) {
|
|
|
2318
2340
|
|
|
2319
2341
|
lines.push('');
|
|
2320
2342
|
const { totalCycles, filesInCycles } = result.summary;
|
|
2321
|
-
lines.push(`Summary: ${totalCycles} circular dependency chain${totalCycles !== 1 ? 's' : ''} involving ${filesInCycles} file${filesInCycles !== 1 ? 's' : ''}
|
|
2343
|
+
lines.push(`Summary: ${totalCycles} circular dependency chain${totalCycles !== 1 ? 's' : ''} involving ${filesInCycles} file${filesInCycles !== 1 ? 's' : ''} (${scannedCount} files with imports scanned).`);
|
|
2322
2344
|
|
|
2323
2345
|
return lines.join('\n');
|
|
2324
2346
|
}
|
|
@@ -3063,6 +3085,80 @@ function formatLinesJson(result) {
|
|
|
3063
3085
|
}, null, 2);
|
|
3064
3086
|
}
|
|
3065
3087
|
|
|
3088
|
+
// ============================================================================
|
|
3089
|
+
// Entrypoints command formatters
|
|
3090
|
+
// ============================================================================
|
|
3091
|
+
|
|
3092
|
+
/**
|
|
3093
|
+
* Format entrypoints command output (text)
|
|
3094
|
+
*/
|
|
3095
|
+
function formatEntrypoints(results, options = {}) {
|
|
3096
|
+
if (!results || results.length === 0) {
|
|
3097
|
+
return 'No framework entry points detected.';
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
const lines = [];
|
|
3101
|
+
lines.push(`Framework Entry Points: ${results.length} detected\n`);
|
|
3102
|
+
|
|
3103
|
+
// Group by type
|
|
3104
|
+
const byType = new Map();
|
|
3105
|
+
for (const ep of results) {
|
|
3106
|
+
if (!byType.has(ep.type)) byType.set(ep.type, []);
|
|
3107
|
+
byType.get(ep.type).push(ep);
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
const typeLabels = {
|
|
3111
|
+
http: 'HTTP Routes',
|
|
3112
|
+
di: 'Dependency Injection',
|
|
3113
|
+
jobs: 'Job Schedulers',
|
|
3114
|
+
test: 'Test Fixtures',
|
|
3115
|
+
runtime: 'Runtime Entry Points',
|
|
3116
|
+
ui: 'UI Handlers',
|
|
3117
|
+
events: 'Event Handlers',
|
|
3118
|
+
};
|
|
3119
|
+
|
|
3120
|
+
let itemNum = 0;
|
|
3121
|
+
for (const [type, entries] of byType) {
|
|
3122
|
+
const label = typeLabels[type] || type;
|
|
3123
|
+
lines.push(`${label} (${entries.length}):`);
|
|
3124
|
+
|
|
3125
|
+
let currentFile = null;
|
|
3126
|
+
for (const ep of entries) {
|
|
3127
|
+
if (ep.file !== currentFile) {
|
|
3128
|
+
currentFile = ep.file;
|
|
3129
|
+
lines.push(` ${ep.file}`);
|
|
3130
|
+
}
|
|
3131
|
+
itemNum++;
|
|
3132
|
+
const evidence = ep.evidence.join(', ');
|
|
3133
|
+
lines.push(` [${itemNum}] ${ep.name} (${ep.framework}) — ${evidence}${' '.repeat(Math.max(0, 40 - ep.name.length - ep.framework.length - evidence.length))}:${ep.line}`);
|
|
3134
|
+
}
|
|
3135
|
+
lines.push('');
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
return lines.join('\n').trimEnd();
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
/**
|
|
3142
|
+
* Format entrypoints command output (JSON)
|
|
3143
|
+
*/
|
|
3144
|
+
function formatEntrypointsJson(results) {
|
|
3145
|
+
return JSON.stringify({
|
|
3146
|
+
meta: { total: results.length },
|
|
3147
|
+
data: {
|
|
3148
|
+
entrypoints: results.map(ep => ({
|
|
3149
|
+
name: ep.name,
|
|
3150
|
+
file: ep.file,
|
|
3151
|
+
line: ep.line,
|
|
3152
|
+
type: ep.type,
|
|
3153
|
+
framework: ep.framework,
|
|
3154
|
+
patternId: ep.patternId,
|
|
3155
|
+
evidence: ep.evidence,
|
|
3156
|
+
confidence: ep.confidence,
|
|
3157
|
+
}))
|
|
3158
|
+
}
|
|
3159
|
+
}, null, 2);
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3066
3162
|
module.exports = {
|
|
3067
3163
|
// Utilities
|
|
3068
3164
|
normalizeParams,
|
|
@@ -3182,5 +3278,9 @@ module.exports = {
|
|
|
3182
3278
|
formatClassResult,
|
|
3183
3279
|
formatClassResultJson,
|
|
3184
3280
|
formatLines,
|
|
3185
|
-
formatLinesJson
|
|
3281
|
+
formatLinesJson,
|
|
3282
|
+
|
|
3283
|
+
// Entrypoints command
|
|
3284
|
+
formatEntrypoints,
|
|
3285
|
+
formatEntrypointsJson,
|
|
3186
3286
|
};
|