ucn 4.2.3 → 5.0.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.
Files changed (72) hide show
  1. package/.claude/skills/ucn/SKILL.md +89 -77
  2. package/.claude/skills/ucn/references/commands.md +62 -68
  3. package/.claude/skills/ucn/references/trust-contract.md +31 -6
  4. package/README.md +438 -305
  5. package/assets/demo.svg +31 -0
  6. package/cli/index.js +430 -1385
  7. package/core/account.js +144 -34
  8. package/core/analysis.js +182 -72
  9. package/core/ast-analysis.js +279 -0
  10. package/core/bridge.js +205 -24
  11. package/core/brief.js +27 -58
  12. package/core/build-worker.js +21 -140
  13. package/core/cache.js +513 -11
  14. package/core/callers.js +4920 -456
  15. package/core/check.js +13 -4
  16. package/core/command-contracts.js +402 -0
  17. package/core/compilation-database.js +276 -0
  18. package/core/confidence.js +4 -1
  19. package/core/deadcode.js +397 -19
  20. package/core/discovery.js +359 -46
  21. package/core/entrypoints.js +195 -41
  22. package/core/execute.js +887 -81
  23. package/core/graph-build.js +162 -7
  24. package/core/graph.js +53 -77
  25. package/core/imports.js +65 -6
  26. package/core/index-ir.js +138 -0
  27. package/core/ir.js +195 -0
  28. package/core/output/analysis.js +212 -22
  29. package/core/output/brief.js +23 -0
  30. package/core/output/check.js +4 -0
  31. package/core/output/doctor.js +37 -6
  32. package/core/output/endpoints.js +5 -2
  33. package/core/output/extraction.js +24 -12
  34. package/core/output/find.js +141 -36
  35. package/core/output/graph.js +11 -5
  36. package/core/output/public.js +462 -0
  37. package/core/output/refactoring.js +42 -10
  38. package/core/output/reporting.js +97 -20
  39. package/core/output/search.js +24 -16
  40. package/core/output/shared.js +22 -1
  41. package/core/output/tracing.js +30 -15
  42. package/core/output-budget.js +295 -0
  43. package/core/output.js +1 -0
  44. package/core/parallel-build.js +44 -11
  45. package/core/parser.js +3 -3
  46. package/core/project.js +384 -187
  47. package/core/public-command.js +47 -0
  48. package/core/registry.js +247 -117
  49. package/core/reporting.js +312 -290
  50. package/core/search.js +317 -185
  51. package/core/semantic-provider.js +110 -0
  52. package/core/stacktrace.js +25 -0
  53. package/core/tracing.js +101 -51
  54. package/core/trust-matrix.js +19 -40
  55. package/core/verify.js +534 -37
  56. package/languages/adapter.js +218 -0
  57. package/languages/c-family.js +2791 -0
  58. package/languages/c.js +3 -0
  59. package/languages/cpp.js +3 -0
  60. package/languages/csharp.js +1402 -0
  61. package/languages/go.js +60 -21
  62. package/languages/html.js +2 -2
  63. package/languages/index.js +85 -7
  64. package/languages/java.js +396 -13
  65. package/languages/javascript.js +199 -19
  66. package/languages/python.js +964 -22
  67. package/languages/rust.js +1317 -152
  68. package/languages/utils.js +40 -3
  69. package/mcp/server.js +254 -636
  70. package/package.json +39 -22
  71. package/eslint.config.js +0 -43
  72. package/jsconfig.json +0 -10
@@ -16,7 +16,7 @@ const fs = require('fs');
16
16
  const { codeUnitCompare, NON_CALLABLE_TYPES } = require('./shared');
17
17
  const path = require('path');
18
18
  const { getCachedCalls } = require('./callers');
19
- const { getLanguageModule } = require('../languages');
19
+ const { getLanguageAdapter } = require('../languages');
20
20
 
21
21
  // ============================================================================
22
22
  // FRAMEWORK PATTERNS
@@ -74,7 +74,10 @@ const FRAMEWORK_PATTERNS = [
74
74
  // Listed BEFORE fastapi: both frameworks share the @app.get/@app.post
75
75
  // shortcuts, and @app.route is Flask-only — a file using @app.route +
76
76
  // @app.post on the SAME app object got split across two frameworks
77
- // (fix #245; first match wins in matchDecoratorOrModifier).
77
+ // (fix #245; first match wins in matchDecoratorOrModifier). When the
78
+ // decorator shape is shared, the file's own imports break the tie
79
+ // (importSignal): a fastapi-importing file's @app.get is fastapi, not
80
+ // flask — the label must agree with what `endpoints` reports.
78
81
  {
79
82
  id: 'flask-route',
80
83
  languages: new Set(['python']),
@@ -82,6 +85,7 @@ const FRAMEWORK_PATTERNS = [
82
85
  framework: 'flask',
83
86
  detection: 'decorator',
84
87
  pattern: /^(app|bp|blueprint)\.(route|get|post|put|delete|patch)/,
88
+ importSignal: /^flask\b/,
85
89
  },
86
90
 
87
91
  // FastAPI (Python) — decorators: @app.get('/path'), @router.post('/path')
@@ -92,6 +96,7 @@ const FRAMEWORK_PATTERNS = [
92
96
  framework: 'fastapi',
93
97
  detection: 'decorator',
94
98
  pattern: /^(app|router)\.(get|post|put|delete|patch|options|head)/,
99
+ importSignal: /^fastapi\b/,
95
100
  },
96
101
 
97
102
  // Django (Python) — decorators: @api_view, @action, @permission_classes
@@ -165,6 +170,34 @@ const FRAMEWORK_PATTERNS = [
165
170
  pattern: /^(get|post|put|delete|patch)$/,
166
171
  },
167
172
 
173
+ // ASP.NET Core controllers and Azure Functions.
174
+ {
175
+ id: 'aspnet-controller-route',
176
+ languages: new Set(['csharp']),
177
+ type: 'http',
178
+ framework: 'aspnet',
179
+ detection: 'decorator',
180
+ pattern: /^(ApiController|Route|Http(Get|Post|Put|Delete|Patch|Head|Options)|AcceptVerbs)$/,
181
+ },
182
+ {
183
+ id: 'azure-function',
184
+ languages: new Set(['csharp']),
185
+ type: 'http',
186
+ framework: 'azure-functions',
187
+ detection: 'decorator',
188
+ pattern: /^(FunctionName|Function|HttpTrigger)$/,
189
+ },
190
+ // ASP.NET minimal APIs: app.MapGet("/path", Handler).
191
+ {
192
+ id: 'aspnet-minimal-route',
193
+ languages: new Set(['csharp']),
194
+ type: 'http',
195
+ framework: 'aspnet-minimal',
196
+ detection: 'callPattern',
197
+ receiverPattern: /^(app|endpoints|routes)$/i,
198
+ methodPattern: /^Map(Get|Post|Put|Delete|Patch|Methods|Fallback)$/,
199
+ },
200
+
168
201
  // ── Dependency Injection ────────────────────────────────────────────
169
202
 
170
203
  // Spring DI (Java)
@@ -252,7 +285,11 @@ const FRAMEWORK_PATTERNS = [
252
285
  type: 'jobs',
253
286
  framework: 'celery',
254
287
  detection: 'decorator',
255
- pattern: /^(app\.task|shared_task|celery\.task)/,
288
+ // Celery instances are conventionally named app, celery, tasks, or
289
+ // something project-specific. The import requirement prevents an
290
+ // arbitrary object's `.task` decorator from becoming a job root.
291
+ pattern: /^(?:[A-Za-z_]\w*\.)?task\b|^shared_task\b/,
292
+ requireImportSignal: /^celery\b/,
256
293
  },
257
294
 
258
295
  // ── Test Frameworks ─────────────────────────────────────────────────
@@ -267,6 +304,30 @@ const FRAMEWORK_PATTERNS = [
267
304
  pattern: /^pytest\.fixture/,
268
305
  },
269
306
 
307
+ // pytest/unittest test functions and plugin hooks — the same name
308
+ // convention the Python language module already uses for deadcode entry
309
+ // points (the entrypoints command must agree with it; Go's Test* pattern
310
+ // set the precedent).
311
+ {
312
+ id: 'pytest-test',
313
+ languages: new Set(['python']),
314
+ type: 'test',
315
+ framework: 'pytest',
316
+ detection: 'namePattern',
317
+ pattern: /^(test_|pytest_)/,
318
+ },
319
+
320
+ // C/C++ test conventions (Unity, CTest, GoogleTest wrappers) — mirrors
321
+ // the c-family language module's own entry-point rule.
322
+ {
323
+ id: 'c-family-test',
324
+ languages: new Set(['c', 'cpp']),
325
+ type: 'test',
326
+ framework: 'native-test',
327
+ detection: 'namePattern',
328
+ pattern: /^(test_|Test|TEST_)/,
329
+ },
330
+
270
331
  // ── Runtime ─────────────────────────────────────────────────────────
271
332
 
272
333
  // Tokio (Rust)
@@ -387,6 +448,34 @@ const FRAMEWORK_PATTERNS = [
387
448
  pattern: /^(test|tokio::test|cfg\(test\))$/,
388
449
  },
389
450
 
451
+ // ── C / C++ / C# runtime and test roots ─────────────────────────
452
+ {
453
+ id: 'c-family-main',
454
+ languages: new Set(['c', 'cpp']),
455
+ type: 'runtime',
456
+ framework: 'native',
457
+ detection: 'namePattern',
458
+ pattern: /^(main|WinMain|wWinMain|DllMain)$/,
459
+ symbolFilter: (s) => !s.className,
460
+ },
461
+ {
462
+ id: 'csharp-main',
463
+ languages: new Set(['csharp']),
464
+ type: 'runtime',
465
+ framework: 'dotnet',
466
+ detection: 'namePattern',
467
+ pattern: /^Main$/,
468
+ symbolFilter: (s) => s.modifiers?.includes('static'),
469
+ },
470
+ {
471
+ id: 'csharp-test',
472
+ languages: new Set(['csharp']),
473
+ type: 'test',
474
+ framework: 'dotnet-test',
475
+ detection: 'decorator',
476
+ pattern: /^(Fact|Theory|Test|TestMethod|TestCase|TestCaseSource|SetUp|OneTimeSetUp)$/,
477
+ },
478
+
390
479
  // ── Go Framework Patterns ─────────────────────────────────────────
391
480
 
392
481
  // Cobra CLI framework — RunE, Run, PreRunE etc. assigned to cobra.Command struct fields
@@ -420,7 +509,7 @@ const FRAMEWORK_PATTERNS = [
420
509
  detection: 'filePath',
421
510
  // Match files under bin/ (any depth), or top-level index/main/cli/server in any directory.
422
511
  // The matcher is run against the project-relative path with forward slashes.
423
- pathPattern: /(^|\/)bin\/[^/]+\.(js|ts|mjs|cjs)$|(^|\/)(index|main|cli|server)\.(js|ts|mjs|cjs)$/,
512
+ pathPattern: /(^|\/)bin\/[^/]+\.(js|ts|mjs|cjs)$|(^|\/)(main|cli|server)\.(js|ts|mjs|cjs)$/,
424
513
  },
425
514
 
426
515
  // Node shebang entry: any file whose first bytes are `#!/usr/bin/env node`
@@ -470,31 +559,40 @@ const FRAMEWORK_PATTERNS = [
470
559
  pathPattern: /(^|\/)__main__\.py$/,
471
560
  },
472
561
 
473
- // ── Catch-all fallbacks ─────────────────────────────────────────────
562
+ // Django management commands — discovered by module path
563
+ // (management/commands/<name>.py); the Command class and its handle()
564
+ // are invoked by `manage.py <name>` (the #253 external-contract family,
565
+ // now visible to the entrypoints command too).
566
+ {
567
+ id: 'django-command',
568
+ languages: new Set(['python']),
569
+ type: 'cli',
570
+ framework: 'django',
571
+ detection: 'filePath',
572
+ pathPattern: /(^|\/)management\/commands\/[^/]+\.py$/,
573
+ },
474
574
 
475
- // Python: any decorator with '.' (attribute access) — framework registration heuristic
476
- // Catches @app.route, @router.get, @celery.task, @something.hook, etc.
477
- // Placed last so specific patterns match first (for better type/framework labeling).
575
+ // Explicit Python event/CLI decorators. Dotted syntax by itself is not
576
+ // registration evidence (`typing.overload`, `property.setter`, and
577
+ // `functools.wraps` are counterexamples).
478
578
  {
479
- id: 'python-dotted-decorator',
579
+ id: 'flask-event-hook',
480
580
  languages: new Set(['python']),
481
581
  type: 'events',
482
- framework: 'unknown',
582
+ framework: 'flask',
483
583
  detection: 'decorator',
484
- pattern: /\./,
584
+ pattern: /^(app|bp|blueprint)\.(before_request|after_request|teardown_request|before_app_request|after_app_request|teardown_app_request|context_processor|url_value_preprocessor|url_defaults|errorhandler)/,
585
+ importSignal: /^flask\b/,
485
586
  },
486
-
487
- // Java: any non-standard annotation (not a keyword modifier or standard JDK annotation)
488
- // Catches @Bean, @Scheduled, @EventListener, @Transactional, etc.
489
- // Placed last so specific patterns match first.
490
587
  {
491
- id: 'java-custom-annotation',
492
- languages: new Set(['java']),
493
- type: 'di',
494
- framework: 'unknown',
495
- detection: 'modifier',
496
- pattern: /^(?!public$|private$|protected$|static$|final$|abstract$|synchronized$|native$|default$|override$|deprecated$|suppresswarnings$|functionalinterface$|safevarargs$)/,
588
+ id: 'click-command',
589
+ languages: new Set(['python']),
590
+ type: 'cli',
591
+ framework: 'click',
592
+ detection: 'decorator',
593
+ pattern: /^(click\.)?(command|group)|^[A-Za-z_]\w*\.(command|group)$/,
497
594
  },
595
+
498
596
  ];
499
597
 
500
598
  // ============================================================================
@@ -507,25 +605,41 @@ const FRAMEWORK_PATTERNS = [
507
605
  * @param {string} language - File language
508
606
  * @returns {{ pattern: object, matchedOn: string }|null}
509
607
  */
510
- function matchDecoratorOrModifier(symbol, language) {
608
+ function matchDecoratorOrModifier(symbol, language, fileEntry = null) {
511
609
  const decorators = symbol.decorators || [];
512
610
  const modifiers = symbol.modifiers || [];
611
+ const modules = (fileEntry?.imports || []).map(imp =>
612
+ typeof imp === 'string' ? imp : String(imp?.module || ''));
513
613
 
614
+ const matches = [];
514
615
  for (const fp of FRAMEWORK_PATTERNS) {
515
616
  if (!fp.languages.has(language)) continue;
617
+ if (fp.requireImportSignal &&
618
+ !modules.some(moduleName => fp.requireImportSignal.test(moduleName))) continue;
516
619
 
517
620
  if (fp.detection === 'decorator') {
518
621
  const matched = decorators.find(d => fp.pattern.test(d));
519
- if (matched) return { pattern: fp, matchedOn: `@${matched}` };
622
+ if (matched) matches.push({ pattern: fp, matchedOn: `@${matched}` });
520
623
  }
521
624
 
522
625
  if (fp.detection === 'modifier') {
523
626
  const matched = modifiers.find(m => fp.pattern.test(m));
524
- if (matched) return { pattern: fp, matchedOn: `@${matched}` };
627
+ if (matched) matches.push({ pattern: fp, matchedOn: `@${matched}` });
525
628
  }
526
629
  }
527
-
528
- return null;
630
+ if (matches.length === 0) return null;
631
+
632
+ // Shared decorator shapes (@app.get is both flask and fastapi): the
633
+ // file's own imports break the tie. Without an import signal, declaration
634
+ // order keeps deciding (the #245 rule).
635
+ if (matches.length > 1 && fileEntry) {
636
+ // fileEntry.imports holds module strings; parser-level records hold
637
+ // { module } objects — accept both shapes.
638
+ const importBacked = matches.find(m => m.pattern.importSignal &&
639
+ modules.some(mod => m.pattern.importSignal.test(mod)));
640
+ if (importBacked) return importBacked;
641
+ }
642
+ return matches[0];
529
643
  }
530
644
 
531
645
  /**
@@ -560,7 +674,8 @@ function buildCallbackEntrypointMap(index) {
560
674
  // registration was never seeded. The registration site is kept as
561
675
  // evidence (registrationFile/registrationLine).
562
676
  const resolveHandlerDef = (name, registrationFile) => {
563
- const defs = index.symbols.get(name);
677
+ const defs = (index.symbols.get(name) || []).filter(definition =>
678
+ !NON_CALLABLE_TYPES.has(definition.type));
564
679
  if (!defs || defs.length === 0) return null;
565
680
  // Prefer a def in the registration file; defs are canonical-sorted,
566
681
  // so falling back to the first is deterministic.
@@ -585,7 +700,8 @@ function buildCallbackEntrypointMap(index) {
585
700
  pattern.methodPattern.test(call.name)) {
586
701
  // BUG M2 (interpolated paths): align with bridge.js's
587
702
  // extractServerRoutes — skip routes whose path is interpolated.
588
- if (pattern.type === 'http' && call.firstStringArg && call.firstStringArgInterp) continue;
703
+ if (pattern.type === 'http' && call.name.toLowerCase() !== 'use' &&
704
+ (!call.firstStringArg || call.firstStringArgInterp)) continue;
589
705
  // BUG M5: Express dual-purpose APIs — 1-arg .get('env') is a
590
706
  // config getter, not a route registration.
591
707
  if (pattern.framework === 'express' &&
@@ -603,6 +719,9 @@ function buildCallbackEntrypointMap(index) {
603
719
  // only visible as the enclosingFunction of the handler
604
720
  // body's own calls (fix #247; the route vanished entirely).
605
721
  const handledLines = new Set();
722
+ // Route lines that produced an actual entry (pass 2 or 2b) —
723
+ // the remainder are anonymous-handler routes for pass 2c.
724
+ const attributedLines = new Set();
606
725
 
607
726
  // Pass 2: find callbacks on route-registration lines
608
727
  for (const call of calls) {
@@ -618,10 +737,11 @@ function buildCallbackEntrypointMap(index) {
618
737
  // not exported handler functions — they must not be marked as
619
738
  // entry points. This aligns the HTTP Routes section with
620
739
  // bridge.js's extractServerRoutes.
621
- if (!index.symbols.has(call.name)) continue;
740
+ const def = resolveHandlerDef(call.name, filePath);
741
+ if (!def) continue;
742
+ attributedLines.add(call.line);
622
743
 
623
744
  if (!result.has(call.name)) {
624
- const def = resolveHandlerDef(call.name, filePath);
625
745
  result.set(call.name, {
626
746
  framework: route.pattern.framework,
627
747
  type: route.pattern.type,
@@ -653,6 +773,7 @@ function buildCallbackEntrypointMap(index) {
653
773
  const inlineDef = encDefs.find(d => d.bodyScopedName &&
654
774
  d.file === filePath && d.startLine === enc.startLine);
655
775
  if (encDefs.length > 0 && !inlineDef) continue;
776
+ attributedLines.add(enc.startLine);
656
777
  if (!result.has(enc.name)) {
657
778
  result.set(enc.name, {
658
779
  framework: route.pattern.framework,
@@ -666,6 +787,33 @@ function buildCallbackEntrypointMap(index) {
666
787
  });
667
788
  }
668
789
  }
790
+
791
+ // Pass 2c: routes with no attributable handler — anonymous
792
+ // arrows (`app.get('/e', (req, res) => …)`) or handlers UCN
793
+ // cannot name. The ROUTE is still a framework entry point:
794
+ // `endpoints` lists it, and `entrypoints` must agree.
795
+ // Literal-path routes only — the same rule that keeps
796
+ // library-internal registration helpers (gin's
797
+ // `group.GET(relativePath, handler)`) out of the route list.
798
+ for (const [line, route] of routeLines) {
799
+ if (attributedLines.has(line)) continue;
800
+ if (!route.call.firstStringArg || route.call.firstStringArgInterp) continue;
801
+ const key = `<anonymous>@${filePath}:${line}`;
802
+ if (!result.has(key)) {
803
+ result.set(key, {
804
+ name: '<anonymous>',
805
+ framework: route.pattern.framework,
806
+ type: route.pattern.type,
807
+ patternId: route.pattern.id,
808
+ method: route.call.name.toUpperCase(),
809
+ file: filePath,
810
+ line,
811
+ registrationFile: filePath,
812
+ registrationLine: line,
813
+ route: route.call.firstStringArg,
814
+ });
815
+ }
816
+ }
669
817
  }
670
818
  }
671
819
 
@@ -805,7 +953,7 @@ function detectEntrypoints(index, options = {}) {
805
953
  if (!fileEntry) continue;
806
954
 
807
955
  // Check decorator/modifier-based patterns
808
- const match = matchDecoratorOrModifier(symbol, fileEntry.language);
956
+ const match = matchDecoratorOrModifier(symbol, fileEntry.language, fileEntry);
809
957
  if (match) {
810
958
  const key = `${symbol.file}:${symbol.startLine}:${name}`;
811
959
  if (seen.has(key)) continue;
@@ -828,6 +976,7 @@ function detectEntrypoints(index, options = {}) {
828
976
  // Check name-based patterns (main, init, TestXxx, etc.)
829
977
  for (const np of namePatterns) {
830
978
  if (!np.languages.has(fileEntry.language)) continue;
979
+ if (NON_CALLABLE_TYPES.has(symbol.type)) continue;
831
980
  // Per-pattern symbol predicate (fix #243) — e.g. main must be
832
981
  // a free function (Rust/Go) or a static method (Java)
833
982
  if (np.symbolFilter && !np.symbolFilter(symbol)) continue;
@@ -856,14 +1005,19 @@ function detectEntrypoints(index, options = {}) {
856
1005
  // 2. Add call-pattern-based entry points (route handlers).
857
1006
  // Run BEFORE file-level patterns so framework labels (express, gin, etc.) win
858
1007
  // over generic file-level labels (e.g. server.js / index.js js-cli-main).
859
- for (const [name, info] of callbackMap) {
1008
+ for (const [mapKey, info] of callbackMap) {
1009
+ // Anonymous-handler routes are stored under a synthetic unique key;
1010
+ // the display name lives in info.name.
1011
+ const name = info.name || mapKey;
860
1012
  const fileEntry = index.files.get(info.file);
861
1013
  const relPath = fileEntry?.relativePath || info.file;
862
1014
  const key = `${info.file}:${info.line}:${name}`;
863
1015
  if (seen.has(key)) continue;
864
1016
  seen.add(key);
865
1017
 
866
- let evidence = `${info.method} route handler`;
1018
+ let evidence = info.route
1019
+ ? `${info.method} ${info.route} — inline anonymous handler`
1020
+ : `${info.method} route handler`;
867
1021
  let registeredAt;
868
1022
  if (info.registrationFile) {
869
1023
  const regEntry = index.files.get(info.registrationFile);
@@ -973,10 +1127,10 @@ function detectEntrypoints(index, options = {}) {
973
1127
  // (3) already covers `if (require.main === module) { main(); }`.
974
1128
  }
975
1129
 
976
- // If we identified at least one specific entry, use that set.
977
- // Otherwise fall back to permissive (null) so a CLI file with neither
978
- // `main()` nor a clear default-export is still seeded somehow.
979
- narrowAllowedByFile.set(filePath, allowed.size > 0 ? allowed : null);
1130
+ // Empty means no callable entry was proven. Do not turn an
1131
+ // ordinary `index.ts`/`server.ts` module into an entry point by
1132
+ // falling back to every symbol in the file.
1133
+ narrowAllowedByFile.set(filePath, allowed);
980
1134
  }
981
1135
  }
982
1136
 
@@ -1047,8 +1201,8 @@ function detectEntrypoints(index, options = {}) {
1047
1201
  const raw = Array.isArray(options.exclude) ? options.exclude : options.exclude.split(',');
1048
1202
  const patterns = raw.map(s => s.trim()).filter(Boolean);
1049
1203
  if (patterns.length > 0) {
1050
- const regexes = patterns.map(p => new RegExp(`(^|[/._-])${p}s?([/._-]|$)`, 'i'));
1051
- filtered = filtered.filter(e => !regexes.some(r => r.test(e.file)));
1204
+ filtered = filtered.filter(e =>
1205
+ index.matchesFilters(e.file, { exclude: patterns }));
1052
1206
  }
1053
1207
  }
1054
1208
 
@@ -1169,7 +1323,7 @@ function computeReachability(index) {
1169
1323
  langModule = langModuleCache.get(lang);
1170
1324
  } else {
1171
1325
  try {
1172
- langModule = getLanguageModule(lang);
1326
+ langModule = getLanguageAdapter(lang);
1173
1327
  } catch (_e) {
1174
1328
  langModule = null;
1175
1329
  }
@@ -1294,7 +1448,7 @@ function isFrameworkEntrypoint(symbol, index) {
1294
1448
  if (!fileEntry) return false;
1295
1449
 
1296
1450
  // Fast path: check decorator/modifier patterns (no index scan needed)
1297
- if (matchDecoratorOrModifier(symbol, fileEntry.language)) {
1451
+ if (matchDecoratorOrModifier(symbol, fileEntry.language, fileEntry)) {
1298
1452
  return true;
1299
1453
  }
1300
1454