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.
@@ -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 ${result.totalFiles} files.`);
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' : ''} out of ${result.totalFiles} total.`);
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
  };