ucn 3.8.1 → 3.8.3

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.
@@ -83,11 +83,23 @@ ucn class MarketDataFetcher
83
83
 
84
84
  ### 6. `deadcode` — Find unused code
85
85
 
86
- Lists all functions and classes with zero callers across the project.
86
+ Lists all functions and classes with zero callers across the project. Framework entry points (Express routes, Spring controllers, Celery tasks, etc.) are automatically excluded.
87
87
 
88
88
  ```bash
89
89
  ucn deadcode # Everything
90
90
  ucn deadcode --exclude=test # Skip test files (most useful)
91
+ ucn deadcode --include-decorated # Include framework-registered functions
92
+ ```
93
+
94
+ ### 7. `entrypoints` — Detect framework entry points
95
+
96
+ Lists functions registered as framework handlers (HTTP routes, DI beans, job schedulers, etc.). Detects patterns across Express, FastAPI, Flask, Spring, Gin, Actix, Celery, pytest, and more.
97
+
98
+ ```bash
99
+ ucn entrypoints # All detected entry points
100
+ ucn entrypoints --type=http # HTTP routes only
101
+ ucn entrypoints --framework=express # Specific framework
102
+ ucn entrypoints --file=routes/ # Scoped to files
91
103
  ```
92
104
 
93
105
  ## When to Use the Other Commands
@@ -120,6 +132,7 @@ ucn deadcode --exclude=test # Skip test files (most useful)
120
132
  | Preview a rename or param change | `ucn plan <name> --rename-to=new_name` | Shows what would change without doing it |
121
133
  | File-level dependency tree | `ucn graph <file> --depth=1` | Visual import tree. Setting `--depth=N` expands all children. Can be noisy — use depth=1 for large projects. For function-level flow, use `trace` instead |
122
134
  | Are there circular dependencies? | `ucn circular-deps` | Detect circular import chains. `--file=<pattern>` filters to cycles involving a file. `--exclude=test` skips test files |
135
+ | What are the framework entry points? | `ucn entrypoints` | Lists all detected routes, DI beans, tasks, etc. Filter: `--type=http`, `--framework=express` |
123
136
  | Find which tests cover a function | `ucn tests <name>` | Test files and test function names |
124
137
  | Extract specific lines from a file | `ucn lines --file=<file> --range=10-20` | Pull a line range without reading the whole file |
125
138
  | Find type definitions | `ucn typedef <name>` | Interfaces, enums, structs, traits, type aliases |
@@ -170,8 +183,11 @@ ucn [target] <command> [name] [--flags]
170
183
  | `--case-sensitive` | Case-sensitive text search (default: case-insensitive) |
171
184
  | `--exact` | Exact name match only in `find`/`typedef` (no substring) |
172
185
  | `--include-uncertain` | Include ambiguous/uncertain matches in `context`/`smart`/`about` |
186
+ | `--show-confidence` | Show confidence scores (0.0–1.0) per caller/callee edge in `context`/`about` |
187
+ | `--min-confidence=N` | Filter edges below confidence threshold (e.g., `--min-confidence=0.7` keeps only high-confidence edges) |
173
188
  | `--include-exported` | Include exported symbols in `deadcode` results |
174
189
  | `--include-decorated` | Include decorated/annotated symbols in `deadcode` results |
190
+ | `--framework=X` | Filter `entrypoints` by framework (e.g., `express`, `spring`, `celery`) |
175
191
  | `--type=<kind>` | Structural search: `function`, `class`, `call`, `method`, `type`. Triggers index query instead of text grep |
176
192
  | `--param=<name>` | Structural search: filter by parameter name or type (e.g., `--param=Request`) |
177
193
  | `--receiver=<name>` | Structural search: filter calls by receiver (e.g., `--receiver=db` for all db.* calls) |
package/cli/index.js CHANGED
@@ -112,6 +112,9 @@ function parseFlags(tokens) {
112
112
  decorator: getValueFlag('--decorator'),
113
113
  exported: tokens.includes('--exported'),
114
114
  unused: tokens.includes('--unused'),
115
+ showConfidence: tokens.includes('--show-confidence'),
116
+ minConfidence: parseFloat(getValueFlag('--min-confidence') || '0') || 0,
117
+ framework: getValueFlag('--framework'),
115
118
  };
116
119
  }
117
120
 
@@ -137,7 +140,9 @@ const knownFlags = new Set([
137
140
  '--base', '--staged', '--stack',
138
141
  '--regex', '--no-regex', '--functions',
139
142
  '--max-lines', '--class-name', '--limit', '--max-files',
140
- '--type', '--param', '--receiver', '--returns', '--decorator', '--exported', '--unused'
143
+ '--type', '--param', '--receiver', '--returns', '--decorator', '--exported', '--unused',
144
+ '--show-confidence', '--min-confidence',
145
+ '--framework'
141
146
  ]);
142
147
 
143
148
  // Handle help flag
@@ -476,7 +481,8 @@ function runProjectCommand(rootDir, command, arg) {
476
481
  const { text, expandable } = output.formatContext(ctx, {
477
482
  methodsHint: 'Note: obj.method() calls excluded — use --include-methods to include them',
478
483
  expandHint: 'Use "ucn . expand <N>" to see code for item N',
479
- uncertainHint: 'use --include-uncertain to include all'
484
+ uncertainHint: 'use --include-uncertain to include all',
485
+ showConfidence: flags.showConfidence,
480
486
  });
481
487
  console.log(text);
482
488
 
@@ -540,7 +546,7 @@ function runProjectCommand(rootDir, command, arg) {
540
546
  if (!ok) fail(error);
541
547
  printOutput(result,
542
548
  output.formatAboutJson,
543
- r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth })
549
+ r => output.formatAbout(r, { expand: flags.expand, root: index.root, depth: flags.depth, showConfidence: flags.showConfidence })
544
550
  );
545
551
  break;
546
552
  }
@@ -751,6 +757,16 @@ function runProjectCommand(rootDir, command, arg) {
751
757
  break;
752
758
  }
753
759
 
760
+ case 'entrypoints': {
761
+ const { ok, result, error } = execute(index, 'entrypoints', { type: flags.type, framework: flags.framework, file: flags.file });
762
+ if (!ok) fail(error);
763
+ printOutput(result,
764
+ output.formatEntrypointsJson,
765
+ r => output.formatEntrypoints(r)
766
+ );
767
+ break;
768
+ }
769
+
754
770
  case 'stats': {
755
771
  const { ok, result, error } = execute(index, 'stats', { functions: flags.functions });
756
772
  if (!ok) fail(error);
@@ -1036,7 +1052,7 @@ Usage:
1036
1052
  ═══════════════════════════════════════════════════════════════════════════════
1037
1053
  UNDERSTAND CODE
1038
1054
  ═══════════════════════════════════════════════════════════════════════════════
1039
- about <name> RECOMMENDED: Full picture (definition, callers, callees, tests, code)
1055
+ about <name> Full picture (definition, callers, callees, tests, code)
1040
1056
  context <name> Who calls this + what it calls (numbered for expand)
1041
1057
  smart <name> Function + all dependencies inline
1042
1058
  impact <name> What breaks if changed (call sites grouped by file)
@@ -1081,6 +1097,7 @@ REFACTORING HELPERS
1081
1097
  verify <name> Check all call sites match signature
1082
1098
  diff-impact What changed in git diff and who calls it (--base, --staged)
1083
1099
  deadcode Find unused functions/classes
1100
+ entrypoints Detect framework entry points (routes, DI, tasks)
1084
1101
 
1085
1102
  ═══════════════════════════════════════════════════════════════════════════════
1086
1103
  OTHER
@@ -1107,10 +1124,13 @@ Common Flags:
1107
1124
  --include-tests Include test files
1108
1125
  --include-methods Include method calls (obj.fn) in caller/callee analysis
1109
1126
  --include-uncertain Include ambiguous/uncertain matches
1127
+ --show-confidence Show confidence scores per caller/callee edge
1128
+ --min-confidence=N Filter edges below confidence threshold (0.0-1.0)
1110
1129
  --include-exported Include exported symbols in deadcode
1111
1130
  --no-regex Force plain text search (regex is default)
1112
1131
  --functions Show per-function line counts (stats command)
1113
1132
  --include-decorated Include decorated/annotated symbols in deadcode
1133
+ --framework=X Filter entrypoints by framework (e.g., --framework=express,spring)
1114
1134
  --exact Exact name match only (find)
1115
1135
  --calls-only Only show call/test-case matches (tests)
1116
1136
  --case-sensitive Case-sensitive text search (search)
@@ -1340,7 +1360,8 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1340
1360
  const { text, expandable } = output.formatContext(result, {
1341
1361
  methodsHint: 'Note: obj.method() calls excluded — use --include-methods to include them',
1342
1362
  expandHint: 'Use "expand <N>" to see code for item N',
1343
- uncertainHint: 'use --include-uncertain to include all'
1363
+ uncertainHint: 'use --include-uncertain to include all',
1364
+ showConfidence: iflags.showConfidence,
1344
1365
  });
1345
1366
  console.log(text);
1346
1367
  if (cache) {
@@ -1364,6 +1385,13 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1364
1385
  break;
1365
1386
  }
1366
1387
 
1388
+ case 'entrypoints': {
1389
+ const { ok, result, error } = execute(index, 'entrypoints', { type: iflags.type, framework: iflags.framework, file: iflags.file });
1390
+ if (!ok) { console.log(error); return; }
1391
+ console.log(output.formatEntrypoints(result));
1392
+ break;
1393
+ }
1394
+
1367
1395
  // ── Standard commands routed through execute() ───────────────────
1368
1396
 
1369
1397
  case 'toc': {
@@ -1379,7 +1407,7 @@ function executeInteractiveCommand(index, command, arg, iflags = {}, cache = nul
1379
1407
  case 'about': {
1380
1408
  const { ok, result, error } = execute(index, 'about', { name: arg, ...iflags });
1381
1409
  if (!ok) { console.log(error); return; }
1382
- console.log(output.formatAbout(result, { expand: iflags.expand, root: index.root, showAll: iflags.all, depth: iflags.depth }));
1410
+ console.log(output.formatAbout(result, { expand: iflags.expand, root: index.root, showAll: iflags.all, depth: iflags.depth, showConfidence: iflags.showConfidence }));
1383
1411
  break;
1384
1412
  }
1385
1413
 
package/core/callers.js CHANGED
@@ -11,6 +11,7 @@ const crypto = require('crypto');
11
11
  const { detectLanguage, getParser, getLanguageModule } = require('../languages');
12
12
  const { isTestFile } = require('./discovery');
13
13
  const { NON_CALLABLE_TYPES } = require('./shared');
14
+ const { scoreEdge } = require('./confidence');
14
15
 
15
16
  /**
16
17
  * Extract a single line from content without splitting the entire string.
@@ -191,7 +192,8 @@ function findCallers(index, name, options = {}) {
191
192
  if (!pendingByFile.has(filePath)) pendingByFile.set(filePath, []);
192
193
  pendingByFile.get(filePath).push({
193
194
  call, fileEntry, callerSymbol,
194
- isMethod: false, isFunctionReference: true, receiver: undefined
195
+ isMethod: false, isFunctionReference: true, receiver: undefined,
196
+ _evidence: { isFunctionReference: true }
195
197
  });
196
198
  pendingCount++;
197
199
  continue;
@@ -422,12 +424,13 @@ function findCallers(index, name, options = {}) {
422
424
  }
423
425
  }
424
426
 
425
- // Java/Go/Rust receiver-class disambiguation:
427
+ // Receiver-class disambiguation:
426
428
  // When the target definition has a class/receiver type, filter callers
427
429
  // whose receiverType is known to be a different type.
428
- // Go uses inferred receiverType; Java/Rust fall back to variable name matching.
430
+ // All languages use receiverType when available (constructor/annotation inference).
431
+ // Go/Java/Rust additionally fall back to variable name matching.
429
432
  if (call.isMethod && call.receiver && !resolvedBySameClass && !bindingId &&
430
- (fileEntry.language === 'java' || fileEntry.language === 'go' || fileEntry.language === 'rust')) {
433
+ (call.receiverType || fileEntry.language === 'java' || fileEntry.language === 'go' || fileEntry.language === 'rust')) {
431
434
  // Build target type set from both className (Java) and receiver (Go/Rust)
432
435
  const targetTypes = new Set();
433
436
  for (const td of targetDefs) {
@@ -474,12 +477,43 @@ function findCallers(index, name, options = {}) {
474
477
  // Find the enclosing function (get full symbol info)
475
478
  const callerSymbol = index.findEnclosingFunction(filePath, call.line, true);
476
479
 
480
+ // Check import graph evidence: does this file import from the target definition's file?
481
+ const targetDefs2 = options.targetDefinitions || definitions;
482
+ const targetFiles2 = new Set(targetDefs2.map(d => d.file).filter(Boolean));
483
+ const callerImports = index.importGraph.get(filePath) || [];
484
+ let hasImportLink = targetFiles2.has(filePath) || callerImports.some(imp => targetFiles2.has(imp));
485
+ // Check one level of re-exports (barrel files) for import evidence
486
+ if (!hasImportLink) {
487
+ for (const imp of callerImports) {
488
+ const transImports = index.importGraph.get(imp) || [];
489
+ if (transImports.some(ti => targetFiles2.has(ti))) {
490
+ hasImportLink = true;
491
+ break;
492
+ }
493
+ }
494
+ }
495
+
477
496
  if (!pendingByFile.has(filePath)) pendingByFile.set(filePath, []);
478
497
  pendingByFile.get(filePath).push({
479
498
  call, fileEntry, callerSymbol,
480
499
  isMethod: call.isMethod || false, isFunctionReference: false,
481
500
  receiver: call.receiver,
482
- receiverType: call.receiverType
501
+ receiverType: call.receiverType,
502
+ _evidence: {
503
+ hasBindingId: !!bindingId,
504
+ resolvedBySameClass: !!resolvedBySameClass,
505
+ isUncertain: !!isUncertain || (
506
+ // Method calls where binding resolution was skipped (non-self receiver)
507
+ // and the receiver has no binding evidence → uncertain (JS/TS/Python only)
508
+ skipLocalBinding && call.isMethod && !resolvedBySameClass &&
509
+ fileEntry.language !== 'go' && fileEntry.language !== 'java' && fileEntry.language !== 'rust' &&
510
+ !(call.receiver && (fileEntry.bindings || []).some(b => b.name === call.receiver))
511
+ ),
512
+ hasReceiverType: !!call.receiverType,
513
+ hasReceiverEvidence: !!(call.receiver &&
514
+ (fileEntry.bindings || []).some(b => b.name === call.receiver)),
515
+ hasImportEvidence: !!bindingId || hasImportLink,
516
+ }
483
517
  });
484
518
  pendingCount++;
485
519
  }
@@ -493,7 +527,8 @@ function findCallers(index, name, options = {}) {
493
527
  for (const [filePath, pending] of pendingByFile) {
494
528
  try {
495
529
  const content = fs.readFileSync(filePath, 'utf-8');
496
- for (const { call, fileEntry, callerSymbol, isMethod, isFunctionReference, receiver, receiverType } of pending) {
530
+ for (const { call, fileEntry, callerSymbol, isMethod, isFunctionReference, receiver, receiverType, _evidence } of pending) {
531
+ const scored = scoreEdge(_evidence || {});
497
532
  callers.push({
498
533
  file: filePath,
499
534
  relativePath: fileEntry.relativePath,
@@ -506,7 +541,9 @@ function findCallers(index, name, options = {}) {
506
541
  isMethod,
507
542
  ...(isFunctionReference && { isFunctionReference: true }),
508
543
  ...(receiver !== undefined && { receiver }),
509
- ...(receiverType && { receiverType })
544
+ ...(receiverType && { receiverType }),
545
+ confidence: scored.confidence,
546
+ resolution: scored.resolution,
510
547
  });
511
548
  }
512
549
  } catch (e) {
@@ -612,9 +649,11 @@ function findCallees(index, def, options = {}) {
612
649
  }
613
650
  }
614
651
  continue;
615
- } else if (call.receiverType && (language === 'go' || language === 'java' || language === 'rust')) {
652
+ } else if (call.receiverType) {
616
653
  // Use parser-inferred receiverType for method resolution
617
- // e.g., f.RunFilter() where f is *Framework → resolve to Framework.RunFilter
654
+ // Go/Java/Rust: from param/receiver type declarations
655
+ // JS/TS: from `new Foo()` assignments or TypeScript type annotations
656
+ // Python: from constructor calls or type annotations
618
657
  const typeName = call.receiverType;
619
658
  const symbols = index.symbols.get(call.name);
620
659
  const isCallableRT = (s) => !NON_CALLABLE_TYPES.has(s.type) ||
@@ -981,6 +1020,8 @@ function findCallees(index, def, options = {}) {
981
1020
  const defReceiver = def.receiver;
982
1021
  const defFileEntry = fileEntry;
983
1022
  const callerIsTest = defFileEntry && isTestFile(defFileEntry.relativePath, defFileEntry.language);
1023
+ // Pre-compute import graph for callee confidence scoring
1024
+ const callerImportSet = new Set(index.importGraph.get(def.file) || []);
984
1025
 
985
1026
  for (const { name: calleeName, bindingId, count } of callees.values()) {
986
1027
  const symbols = index.symbols.get(calleeName);
@@ -1009,9 +1050,7 @@ function findCallees(index, def, options = {}) {
1009
1050
  } else {
1010
1051
  // Priority 2.5: Imported file — check if the caller's file imports
1011
1052
  // from any of the candidate callee files (using importGraph)
1012
- const callerImportedFiles = index.importGraph.get(def.file) || [];
1013
- const importedFileSet = new Set(callerImportedFiles);
1014
- const importedCallee = symbols.find(s => importedFileSet.has(s.file));
1053
+ const importedCallee = symbols.find(s => callerImportSet.has(s.file));
1015
1054
  if (importedCallee) {
1016
1055
  callee = importedCallee;
1017
1056
  } else if (defReceiver) {
@@ -1095,10 +1134,18 @@ function findCallees(index, def, options = {}) {
1095
1134
  }
1096
1135
  }
1097
1136
 
1137
+ const calleeScored = scoreEdge({
1138
+ hasBindingId: !!bindingId,
1139
+ hasImportEvidence: !!bindingId || (symbols && symbols.length === 1) ||
1140
+ (callee.file === def.file) || callerImportSet.has(callee.file),
1141
+ isUncertain: false, // uncertain callees already filtered above
1142
+ });
1098
1143
  result.push({
1099
1144
  ...callee,
1100
1145
  callCount: count,
1101
- weight: index.calculateWeight(count)
1146
+ weight: index.calculateWeight(count),
1147
+ confidence: calleeScored.confidence,
1148
+ resolution: calleeScored.resolution,
1102
1149
  });
1103
1150
  }
1104
1151
  }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * core/confidence.js - Deterministic edge confidence scoring
3
+ *
4
+ * Assigns a 0.0-1.0 confidence score to each caller/callee edge based on
5
+ * how the call was resolved. Rule-based, no ML.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ // Resolution types ordered from most to least confident
11
+ const RESOLUTION = {
12
+ EXACT_BINDING: 'exact-binding',
13
+ SAME_CLASS: 'same-class',
14
+ RECEIVER_HINT: 'receiver-hint',
15
+ SCOPE_MATCH: 'scope-match',
16
+ NAME_ONLY: 'name-only',
17
+ UNCERTAIN: 'uncertain',
18
+ };
19
+
20
+ // Seed scores per resolution type (tunable)
21
+ const SCORES = {
22
+ [RESOLUTION.EXACT_BINDING]: 0.98,
23
+ [RESOLUTION.SAME_CLASS]: 0.92,
24
+ [RESOLUTION.RECEIVER_HINT]: 0.80,
25
+ [RESOLUTION.SCOPE_MATCH]: 0.65,
26
+ [RESOLUTION.NAME_ONLY]: 0.40,
27
+ [RESOLUTION.UNCERTAIN]: 0.25,
28
+ };
29
+
30
+ /**
31
+ * Score a caller/callee edge based on resolution evidence.
32
+ *
33
+ * @param {object} evidence - Resolution evidence collected during call resolution
34
+ * @param {boolean} [evidence.hasBindingId] - Call resolved to a specific bindingId
35
+ * @param {boolean} [evidence.resolvedBySameClass] - Resolved via self/this/super/cls
36
+ * @param {boolean} [evidence.resolvedByReceiverHint] - Receiver type narrowed via local hints
37
+ * @param {boolean} [evidence.hasImportEvidence] - File imports the target definition
38
+ * @param {boolean} [evidence.hasReceiverEvidence] - Receiver variable has binding in file scope
39
+ * @param {boolean} [evidence.isUncertain] - Marked uncertain by resolution logic
40
+ * @param {boolean} [evidence.isFunctionReference] - Passed as callback argument
41
+ * @param {boolean} [evidence.hasReceiverType] - Go/Java/Rust parser-inferred receiverType
42
+ * @returns {{ confidence: number, resolution: string, evidence: string[] }}
43
+ */
44
+ function scoreEdge(evidence) {
45
+ const reasons = [];
46
+
47
+ // Exact binding match (highest confidence)
48
+ if (evidence.hasBindingId) {
49
+ reasons.push('binding-id match');
50
+ if (evidence.hasImportEvidence) reasons.push('import-verified');
51
+ return { confidence: SCORES[RESOLUTION.EXACT_BINDING], resolution: RESOLUTION.EXACT_BINDING, evidence: reasons };
52
+ }
53
+
54
+ // Same-class resolution (self/this/super/cls)
55
+ if (evidence.resolvedBySameClass) {
56
+ reasons.push('same-class method');
57
+ if (evidence.hasInheritanceChain) reasons.push('via inheritance');
58
+ return { confidence: SCORES[RESOLUTION.SAME_CLASS], resolution: RESOLUTION.SAME_CLASS, evidence: reasons };
59
+ }
60
+
61
+ // Receiver hint narrowed to specific type
62
+ if (evidence.resolvedByReceiverHint || evidence.hasReceiverType) {
63
+ reasons.push(evidence.hasReceiverType ? 'parser receiver-type' : 'local type inference');
64
+ return { confidence: SCORES[RESOLUTION.RECEIVER_HINT], resolution: RESOLUTION.RECEIVER_HINT, evidence: reasons };
65
+ }
66
+
67
+ // Scope/import-supported match
68
+ if (evidence.hasImportEvidence || evidence.hasReceiverEvidence) {
69
+ if (evidence.hasImportEvidence) reasons.push('import-supported');
70
+ if (evidence.hasReceiverEvidence) reasons.push('receiver binding in scope');
71
+ return { confidence: SCORES[RESOLUTION.SCOPE_MATCH], resolution: RESOLUTION.SCOPE_MATCH, evidence: reasons };
72
+ }
73
+
74
+ // Function reference (callback)
75
+ if (evidence.isFunctionReference) {
76
+ reasons.push('function reference');
77
+ return { confidence: SCORES[RESOLUTION.SCOPE_MATCH], resolution: RESOLUTION.SCOPE_MATCH, evidence: reasons };
78
+ }
79
+
80
+ // Uncertain
81
+ if (evidence.isUncertain) {
82
+ reasons.push('ambiguous resolution');
83
+ return { confidence: SCORES[RESOLUTION.UNCERTAIN], resolution: RESOLUTION.UNCERTAIN, evidence: reasons };
84
+ }
85
+
86
+ // Name-only match (no additional evidence)
87
+ reasons.push('name match only');
88
+ return { confidence: SCORES[RESOLUTION.NAME_ONLY], resolution: RESOLUTION.NAME_ONLY, evidence: reasons };
89
+ }
90
+
91
+ /**
92
+ * Filter edges by minimum confidence threshold.
93
+ * @param {Array} edges - Array of objects with .confidence property
94
+ * @param {number} minConfidence - Minimum confidence (0.0-1.0)
95
+ * @returns {{ kept: Array, filtered: number }}
96
+ */
97
+ function filterByConfidence(edges, minConfidence) {
98
+ if (!minConfidence || minConfidence <= 0) return { kept: edges, filtered: 0 };
99
+ const kept = [];
100
+ let filtered = 0;
101
+ for (const edge of edges) {
102
+ if ((edge.confidence || 0) >= minConfidence) {
103
+ kept.push(edge);
104
+ } else {
105
+ filtered++;
106
+ }
107
+ }
108
+ return { kept, filtered };
109
+ }
110
+
111
+ module.exports = {
112
+ RESOLUTION,
113
+ SCORES,
114
+ scoreEdge,
115
+ filterByConfidence,
116
+ };
package/core/deadcode.js CHANGED
@@ -8,6 +8,7 @@
8
8
  const { detectLanguage, getParser, getLanguageModule, safeParse } = require('../languages');
9
9
  const { isTestFile } = require('./discovery');
10
10
  const { escapeRegExp } = require('./shared');
11
+ const { isFrameworkEntrypoint } = require('./entrypoints');
11
12
 
12
13
  /** Check if a position in a line is inside a string literal (quotes/backticks) */
13
14
  function isInsideString(line, pos) {
@@ -369,23 +370,13 @@ function deadcode(index, options = {}) {
369
370
  continue;
370
371
  }
371
372
 
372
- // Framework registration decorators — excluded by default to reduce noise
373
- // Python: decorators with '.' (attribute access) like @router.get, @app.route, @celery.task
374
- // Java: non-standard annotations like @Bean, @Scheduled, @GetMapping
375
- // These functions are invoked by frameworks, not by user code — AST can't see the call path
376
- const javaKeywords = new Set(['public', 'private', 'protected', 'static', 'final', 'abstract', 'synchronized', 'native', 'default']);
377
- const hasRegistrationDecorator = (() => {
378
- if (lang === 'python') {
379
- const decorators = symbol.decorators || [];
380
- return decorators.some(d => d.includes('.'));
381
- }
382
- if (lang === 'java') {
383
- return mods.some(m => !javaKeywords.has(m));
384
- }
385
- return false;
386
- })();
373
+ // Framework entry point detection — excluded by default to reduce noise
374
+ // Detects decorator/annotation patterns (Python, Java, Rust, JS/TS) and
375
+ // call-pattern-based registration (Express routes, Gin handlers, etc.)
376
+ // These functions are invoked by frameworks, not by user code.
377
+ const hasFrameworkEntrypoint = isFrameworkEntrypoint(symbol, index);
387
378
 
388
- if (hasRegistrationDecorator && !options.includeDecorated) {
379
+ if (hasFrameworkEntrypoint && !options.includeDecorated) {
389
380
  excludedDecorated++;
390
381
  continue;
391
382
  }
@@ -447,9 +438,10 @@ function deadcode(index, options = {}) {
447
438
  // Python: symbol.decorators (e.g., ['app.route("/path")', 'login_required'])
448
439
  // Java/Rust/Go: symbol.modifiers may contain annotations (e.g., 'bean', 'scheduled')
449
440
  const decorators = symbol.decorators || [];
450
- // For Java, extract annotation-like modifiers (javaKeywords defined above)
441
+ // For Java, extract annotation-like modifiers
442
+ const javaKw = new Set(['public', 'private', 'protected', 'static', 'final', 'abstract', 'synchronized', 'native', 'default']);
451
443
  const annotations = lang === 'java'
452
- ? mods.filter(m => !javaKeywords.has(m))
444
+ ? mods.filter(m => !javaKw.has(m))
453
445
  : [];
454
446
 
455
447
  results.push({