ucn 4.0.2 → 4.1.0
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 +31 -7
- package/README.md +89 -50
- package/cli/index.js +199 -94
- package/core/account.js +3 -1
- package/core/analysis.js +322 -304
- package/core/bridge.js +13 -8
- package/core/cache.js +109 -19
- package/core/callers.js +969 -77
- package/core/check.js +19 -8
- package/core/deadcode.js +368 -40
- package/core/discovery.js +31 -11
- package/core/entrypoints.js +149 -17
- package/core/execute.js +330 -43
- package/core/graph-build.js +61 -10
- package/core/graph.js +282 -61
- package/core/imports.js +72 -3
- package/core/output/analysis-ext.js +70 -10
- package/core/output/analysis.js +52 -33
- package/core/output/check.js +4 -1
- package/core/output/endpoints.js +8 -3
- package/core/output/extraction.js +12 -1
- package/core/output/find.js +23 -9
- package/core/output/graph.js +32 -9
- package/core/output/refactoring.js +147 -49
- package/core/output/reporting.js +104 -5
- package/core/output/search.js +30 -3
- package/core/output/shared.js +31 -4
- package/core/output/tracing.js +22 -11
- package/core/parser.js +8 -6
- package/core/project.js +167 -7
- package/core/registry.js +20 -16
- package/core/reporting.js +131 -13
- package/core/search.js +270 -55
- package/core/shared.js +240 -1
- package/core/stacktrace.js +23 -1
- package/core/tracing.js +278 -36
- package/core/verify.js +352 -349
- package/languages/go.js +29 -17
- package/languages/index.js +56 -0
- package/languages/java.js +133 -16
- package/languages/javascript.js +215 -27
- package/languages/python.js +113 -47
- package/languages/rust.js +89 -26
- package/languages/utils.js +35 -7
- package/mcp/server.js +69 -30
- package/package.json +4 -1
package/core/discovery.js
CHANGED
|
@@ -148,11 +148,19 @@ function parseGitignore(projectRoot) {
|
|
|
148
148
|
// Strip trailing slash (directory indicator) — shouldIgnore checks names, not types
|
|
149
149
|
let pattern = line.endsWith('/') ? line.slice(0, -1) : line;
|
|
150
150
|
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
//
|
|
155
|
-
//
|
|
151
|
+
// Leading slash = ANCHORED to the .gitignore's directory (fix #226).
|
|
152
|
+
// git semantics: `/locale` ignores only the root-level locale, never
|
|
153
|
+
// src/locale. Stripping the slash and matching by bare name silently
|
|
154
|
+
// excluded real source trees (dayjs ignores its BUILD outputs /locale
|
|
155
|
+
// /plugin — src/locale and src/plugin are ~160 tracked source files
|
|
156
|
+
// UCN never indexed, with no warning). Anchored patterns keep the
|
|
157
|
+
// slash; shouldIgnore applies them only at the walk's anchor root.
|
|
158
|
+
const anchored = pattern.startsWith('/');
|
|
159
|
+
if (anchored) pattern = pattern.slice(1);
|
|
160
|
+
|
|
161
|
+
// Skip patterns with interior path separators — shouldIgnore matches
|
|
162
|
+
// single name segments, not full paths. Patterns like "foo/bar" would
|
|
163
|
+
// need walkDir-level support.
|
|
156
164
|
if (pattern.includes('/')) continue;
|
|
157
165
|
|
|
158
166
|
// Skip empty after stripping
|
|
@@ -161,7 +169,7 @@ function parseGitignore(projectRoot) {
|
|
|
161
169
|
// Avoid duplicating built-in ignores
|
|
162
170
|
if (DEFAULT_IGNORES.includes(pattern)) continue;
|
|
163
171
|
|
|
164
|
-
patterns.push(pattern);
|
|
172
|
+
patterns.push(anchored ? '/' + pattern : pattern);
|
|
165
173
|
}
|
|
166
174
|
|
|
167
175
|
return patterns;
|
|
@@ -211,6 +219,9 @@ function expandGlob(pattern, options = {}) {
|
|
|
211
219
|
ignores,
|
|
212
220
|
maxDepth,
|
|
213
221
|
followSymlinks,
|
|
222
|
+
// Anchored gitignore patterns ('/name') apply only to entries directly
|
|
223
|
+
// under the project root — the .gitignore's own directory (fix #226).
|
|
224
|
+
anchorRoot: root,
|
|
214
225
|
onFile: (filePath) => {
|
|
215
226
|
if (files.length < maxFiles) {
|
|
216
227
|
files.push(filePath);
|
|
@@ -309,7 +320,7 @@ function walkDir(dir, options, depth = 0, visited = new Set()) {
|
|
|
309
320
|
for (const entry of entries) {
|
|
310
321
|
const fullPath = path.join(dir, entry.name);
|
|
311
322
|
|
|
312
|
-
if (shouldIgnore(entry.name, options.ignores, dir)) continue;
|
|
323
|
+
if (shouldIgnore(entry.name, options.ignores, dir, dir === options.anchorRoot)) continue;
|
|
313
324
|
|
|
314
325
|
let isDir = entry.isDirectory();
|
|
315
326
|
let isFile = entry.isFile();
|
|
@@ -340,14 +351,22 @@ function walkDir(dir, options, depth = 0, visited = new Set()) {
|
|
|
340
351
|
/**
|
|
341
352
|
* Check if a file/directory name should be ignored
|
|
342
353
|
* @param {string} name - File/directory name
|
|
343
|
-
* @param {string[]} ignores - Patterns to always ignore
|
|
354
|
+
* @param {string[]} ignores - Patterns to always ignore. Patterns with a
|
|
355
|
+
* leading '/' are ANCHORED (git semantics, fix #226): they match only when
|
|
356
|
+
* `atAnchorRoot` is true — i.e. the entry sits directly in the directory
|
|
357
|
+
* the .gitignore belongs to. Default false errs toward KEEPING files.
|
|
344
358
|
* @param {string} [parentDir] - Parent directory path (for conditional checks)
|
|
359
|
+
* @param {boolean} [atAnchorRoot] - Whether parentDir IS the anchor root
|
|
345
360
|
*/
|
|
346
361
|
const _globRegexCache = new Map();
|
|
347
362
|
|
|
348
|
-
function shouldIgnore(name, ignores, parentDir) {
|
|
363
|
+
function shouldIgnore(name, ignores, parentDir, atAnchorRoot = false) {
|
|
349
364
|
// Check unconditional ignores
|
|
350
|
-
for (
|
|
365
|
+
for (let pattern of ignores) {
|
|
366
|
+
if (pattern.charCodeAt(0) === 47 /* '/' */) {
|
|
367
|
+
if (!atAnchorRoot) continue;
|
|
368
|
+
pattern = pattern.slice(1);
|
|
369
|
+
}
|
|
351
370
|
if (pattern.includes('*')) {
|
|
352
371
|
let regex = _globRegexCache.get(pattern);
|
|
353
372
|
if (!regex) {
|
|
@@ -570,5 +589,6 @@ module.exports = {
|
|
|
570
589
|
PROJECT_MARKERS,
|
|
571
590
|
TEST_PATTERNS,
|
|
572
591
|
ALL_SUPPORTED_EXTENSIONS,
|
|
573
|
-
MANIFEST_HINTS
|
|
592
|
+
MANIFEST_HINTS,
|
|
593
|
+
compareNames
|
|
574
594
|
};
|
package/core/entrypoints.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
'use strict';
|
|
14
14
|
|
|
15
15
|
const fs = require('fs');
|
|
16
|
+
const { codeUnitCompare, NON_CALLABLE_TYPES } = require('./shared');
|
|
16
17
|
const path = require('path');
|
|
17
18
|
const { getCachedCalls } = require('./callers');
|
|
18
19
|
const { getLanguageModule } = require('../languages');
|
|
@@ -69,24 +70,28 @@ const FRAMEWORK_PATTERNS = [
|
|
|
69
70
|
pattern: /^(Get|Post|Put|Delete|Patch|Options|Head|All|Controller|Injectable|Module)$/,
|
|
70
71
|
},
|
|
71
72
|
|
|
72
|
-
//
|
|
73
|
+
// Flask (Python) — decorators: @app.route('/path'), @bp.get('/path').
|
|
74
|
+
// Listed BEFORE fastapi: both frameworks share the @app.get/@app.post
|
|
75
|
+
// shortcuts, and @app.route is Flask-only — a file using @app.route +
|
|
76
|
+
// @app.post on the SAME app object got split across two frameworks
|
|
77
|
+
// (fix #245; first match wins in matchDecoratorOrModifier).
|
|
73
78
|
{
|
|
74
|
-
id: '
|
|
79
|
+
id: 'flask-route',
|
|
75
80
|
languages: new Set(['python']),
|
|
76
81
|
type: 'http',
|
|
77
|
-
framework: '
|
|
82
|
+
framework: 'flask',
|
|
78
83
|
detection: 'decorator',
|
|
79
|
-
pattern: /^(app|
|
|
84
|
+
pattern: /^(app|bp|blueprint)\.(route|get|post|put|delete|patch)/,
|
|
80
85
|
},
|
|
81
86
|
|
|
82
|
-
//
|
|
87
|
+
// FastAPI (Python) — decorators: @app.get('/path'), @router.post('/path')
|
|
83
88
|
{
|
|
84
|
-
id: '
|
|
89
|
+
id: 'fastapi-route',
|
|
85
90
|
languages: new Set(['python']),
|
|
86
91
|
type: 'http',
|
|
87
|
-
framework: '
|
|
92
|
+
framework: 'fastapi',
|
|
88
93
|
detection: 'decorator',
|
|
89
|
-
pattern: /^(app|
|
|
94
|
+
pattern: /^(app|router)\.(get|post|put|delete|patch|options|head)/,
|
|
90
95
|
},
|
|
91
96
|
|
|
92
97
|
// Django (Python) — decorators: @api_view, @action, @permission_classes
|
|
@@ -131,6 +136,25 @@ const FRAMEWORK_PATTERNS = [
|
|
|
131
136
|
methodPattern: /^(HandleFunc|Handle)$/,
|
|
132
137
|
},
|
|
133
138
|
|
|
139
|
+
// Actix (Rust) — the runtime/test macros are NOT routes (fix #243:
|
|
140
|
+
// #[actix_web::main] was typed http, so --type runtime missed main on
|
|
141
|
+
// actix apps and --type http listed it as a route).
|
|
142
|
+
{
|
|
143
|
+
id: 'actix-main',
|
|
144
|
+
languages: new Set(['rust']),
|
|
145
|
+
type: 'runtime',
|
|
146
|
+
framework: 'actix',
|
|
147
|
+
detection: 'modifier',
|
|
148
|
+
pattern: /^actix_web::main$/,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: 'actix-test',
|
|
152
|
+
languages: new Set(['rust']),
|
|
153
|
+
type: 'test',
|
|
154
|
+
framework: 'actix',
|
|
155
|
+
detection: 'modifier',
|
|
156
|
+
pattern: /^actix_web::test$/,
|
|
157
|
+
},
|
|
134
158
|
// Actix (Rust) — modifiers from #[get("/path")], #[post("/path")]
|
|
135
159
|
{
|
|
136
160
|
id: 'actix-route',
|
|
@@ -138,7 +162,7 @@ const FRAMEWORK_PATTERNS = [
|
|
|
138
162
|
type: 'http',
|
|
139
163
|
framework: 'actix',
|
|
140
164
|
detection: 'modifier',
|
|
141
|
-
pattern: /^(get|post|put|delete|patch
|
|
165
|
+
pattern: /^(get|post|put|delete|patch)$/,
|
|
142
166
|
},
|
|
143
167
|
|
|
144
168
|
// ── Dependency Injection ────────────────────────────────────────────
|
|
@@ -257,7 +281,8 @@ const FRAMEWORK_PATTERNS = [
|
|
|
257
281
|
|
|
258
282
|
// ── Go Runtime Entry Points ─────────────────────────────────────────
|
|
259
283
|
|
|
260
|
-
// Go main function (program entry)
|
|
284
|
+
// Go main function (program entry). symbolFilter: only FREE functions —
|
|
285
|
+
// a method named main on a receiver is an ordinary method (fix #243).
|
|
261
286
|
{
|
|
262
287
|
id: 'go-main',
|
|
263
288
|
languages: new Set(['go']),
|
|
@@ -265,6 +290,7 @@ const FRAMEWORK_PATTERNS = [
|
|
|
265
290
|
framework: 'go',
|
|
266
291
|
detection: 'namePattern',
|
|
267
292
|
pattern: /^main$/,
|
|
293
|
+
symbolFilter: (s) => !s.className && !s.receiver,
|
|
268
294
|
},
|
|
269
295
|
|
|
270
296
|
// Go init functions (package initialization, called by runtime)
|
|
@@ -275,6 +301,7 @@ const FRAMEWORK_PATTERNS = [
|
|
|
275
301
|
framework: 'go',
|
|
276
302
|
detection: 'namePattern',
|
|
277
303
|
pattern: /^init$/,
|
|
304
|
+
symbolFilter: (s) => !s.className && !s.receiver,
|
|
278
305
|
},
|
|
279
306
|
|
|
280
307
|
// Go test functions (called by go test)
|
|
@@ -289,7 +316,8 @@ const FRAMEWORK_PATTERNS = [
|
|
|
289
316
|
|
|
290
317
|
// ── Java entry points ─────────────────────────────────────────────
|
|
291
318
|
|
|
292
|
-
// Java main(String[] args) — JVM entry point
|
|
319
|
+
// Java main(String[] args) — JVM entry point. Java main IS a method, but
|
|
320
|
+
// only a STATIC one is JVM-invocable (fix #243).
|
|
293
321
|
{
|
|
294
322
|
id: 'java-main',
|
|
295
323
|
languages: new Set(['java']),
|
|
@@ -297,6 +325,7 @@ const FRAMEWORK_PATTERNS = [
|
|
|
297
325
|
framework: 'java',
|
|
298
326
|
detection: 'namePattern',
|
|
299
327
|
pattern: /^main$/,
|
|
328
|
+
symbolFilter: (s) => (s.modifiers || []).includes('static'),
|
|
300
329
|
},
|
|
301
330
|
|
|
302
331
|
// JUnit @Test family — Java parser lowercases annotations into `modifiers`,
|
|
@@ -334,7 +363,8 @@ const FRAMEWORK_PATTERNS = [
|
|
|
334
363
|
|
|
335
364
|
// ── Rust entry points ─────────────────────────────────────────────
|
|
336
365
|
|
|
337
|
-
// Rust main() — fn main() is the binary entry point
|
|
366
|
+
// Rust main() — the FREE function fn main() is the binary entry point;
|
|
367
|
+
// an impl method named main is an ordinary method (fix #243)
|
|
338
368
|
{
|
|
339
369
|
id: 'rust-main',
|
|
340
370
|
languages: new Set(['rust']),
|
|
@@ -342,6 +372,7 @@ const FRAMEWORK_PATTERNS = [
|
|
|
342
372
|
framework: 'rust',
|
|
343
373
|
detection: 'namePattern',
|
|
344
374
|
pattern: /^main$/,
|
|
375
|
+
symbolFilter: (s) => !s.className && !s.receiver,
|
|
345
376
|
},
|
|
346
377
|
|
|
347
378
|
// Rust #[test] attribute — Rust parser stores attributes as `modifiers`,
|
|
@@ -522,6 +553,20 @@ function buildCallbackEntrypointMap(index) {
|
|
|
522
553
|
|
|
523
554
|
const result = new Map(); // name -> info
|
|
524
555
|
|
|
556
|
+
// Attribute the entry point to the handler's DEFINITION, not the
|
|
557
|
+
// registration call site: `about handler --file X --line N` handles must
|
|
558
|
+
// resolve, and reachability seeding matches (absoluteFile, line) against
|
|
559
|
+
// symbol defs — a handler defined in a different file than its
|
|
560
|
+
// registration was never seeded. The registration site is kept as
|
|
561
|
+
// evidence (registrationFile/registrationLine).
|
|
562
|
+
const resolveHandlerDef = (name, registrationFile) => {
|
|
563
|
+
const defs = index.symbols.get(name);
|
|
564
|
+
if (!defs || defs.length === 0) return null;
|
|
565
|
+
// Prefer a def in the registration file; defs are canonical-sorted,
|
|
566
|
+
// so falling back to the first is deterministic.
|
|
567
|
+
return defs.find(d => d.file === registrationFile) || defs[0];
|
|
568
|
+
};
|
|
569
|
+
|
|
525
570
|
for (const [filePath, fileEntry] of index.files) {
|
|
526
571
|
const lang = fileEntry.language;
|
|
527
572
|
|
|
@@ -552,11 +597,19 @@ function buildCallbackEntrypointMap(index) {
|
|
|
552
597
|
}
|
|
553
598
|
|
|
554
599
|
if (routeLines.size > 0) {
|
|
600
|
+
// Pass 2b prep: routes whose handler is an inline NAMED
|
|
601
|
+
// function expression (`app.post('/x', function createUser()
|
|
602
|
+
// {...})`) have no function-reference record — the name is
|
|
603
|
+
// only visible as the enclosingFunction of the handler
|
|
604
|
+
// body's own calls (fix #247; the route vanished entirely).
|
|
605
|
+
const handledLines = new Set();
|
|
606
|
+
|
|
555
607
|
// Pass 2: find callbacks on route-registration lines
|
|
556
608
|
for (const call of calls) {
|
|
557
609
|
if (!call.isFunctionReference && !call.isPotentialCallback) continue;
|
|
558
610
|
const route = routeLines.get(call.line);
|
|
559
611
|
if (!route) continue;
|
|
612
|
+
handledLines.add(call.line);
|
|
560
613
|
|
|
561
614
|
// BUG M2: only treat as a handler if the name resolves to a
|
|
562
615
|
// project-defined symbol. Library code like gin's
|
|
@@ -568,13 +621,40 @@ function buildCallbackEntrypointMap(index) {
|
|
|
568
621
|
if (!index.symbols.has(call.name)) continue;
|
|
569
622
|
|
|
570
623
|
if (!result.has(call.name)) {
|
|
624
|
+
const def = resolveHandlerDef(call.name, filePath);
|
|
571
625
|
result.set(call.name, {
|
|
626
|
+
framework: route.pattern.framework,
|
|
627
|
+
type: route.pattern.type,
|
|
628
|
+
patternId: route.pattern.id,
|
|
629
|
+
method: route.call.name.toUpperCase(),
|
|
630
|
+
file: def ? def.file : filePath,
|
|
631
|
+
line: def ? def.startLine : call.line,
|
|
632
|
+
registrationFile: filePath,
|
|
633
|
+
registrationLine: call.line,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Pass 2b: inline NAMED function-expression handlers. The
|
|
639
|
+
// function is defined IN argument position, so the only
|
|
640
|
+
// evidence is the enclosingFunction of calls inside its body,
|
|
641
|
+
// anchored at the registration line (fix #247).
|
|
642
|
+
for (const call of calls) {
|
|
643
|
+
const enc = call.enclosingFunction;
|
|
644
|
+
if (!enc || !enc.name || enc.name === '<anonymous>') continue;
|
|
645
|
+
const route = routeLines.get(enc.startLine);
|
|
646
|
+
if (!route || handledLines.has(enc.startLine)) continue;
|
|
647
|
+
if (index.symbols.has(enc.name)) continue; // real defs took pass 2
|
|
648
|
+
if (!result.has(enc.name)) {
|
|
649
|
+
result.set(enc.name, {
|
|
572
650
|
framework: route.pattern.framework,
|
|
573
651
|
type: route.pattern.type,
|
|
574
652
|
patternId: route.pattern.id,
|
|
575
653
|
method: route.call.name.toUpperCase(),
|
|
576
654
|
file: filePath,
|
|
577
|
-
line:
|
|
655
|
+
line: enc.startLine,
|
|
656
|
+
registrationFile: filePath,
|
|
657
|
+
registrationLine: enc.startLine,
|
|
578
658
|
});
|
|
579
659
|
}
|
|
580
660
|
}
|
|
@@ -592,13 +672,16 @@ function buildCallbackEntrypointMap(index) {
|
|
|
592
672
|
if (pattern.typePattern.test(call.compositeType) &&
|
|
593
673
|
pattern.fieldPattern.test(call.fieldName)) {
|
|
594
674
|
if (!result.has(call.name)) {
|
|
675
|
+
const def = resolveHandlerDef(call.name, filePath);
|
|
595
676
|
result.set(call.name, {
|
|
596
677
|
framework: pattern.framework,
|
|
597
678
|
type: pattern.type,
|
|
598
679
|
patternId: pattern.id,
|
|
599
680
|
method: call.fieldName,
|
|
600
|
-
file: filePath,
|
|
601
|
-
line: call.line,
|
|
681
|
+
file: def ? def.file : filePath,
|
|
682
|
+
line: def ? def.startLine : call.line,
|
|
683
|
+
registrationFile: filePath,
|
|
684
|
+
registrationLine: call.line,
|
|
602
685
|
});
|
|
603
686
|
}
|
|
604
687
|
break;
|
|
@@ -622,6 +705,32 @@ function buildCallbackEntrypointMap(index) {
|
|
|
622
705
|
* @returns {Array<{ name, file, line, type, framework, patternId, evidence, confidence }>}
|
|
623
706
|
*/
|
|
624
707
|
function detectEntrypoints(index, options = {}) {
|
|
708
|
+
// Validate --type against the pattern registry up front — an unknown
|
|
709
|
+
// value used to fall through to the filter and silently return nothing.
|
|
710
|
+
if (options.type) {
|
|
711
|
+
const validTypes = new Set(FRAMEWORK_PATTERNS.map(p => p.type));
|
|
712
|
+
if (!validTypes.has(options.type)) {
|
|
713
|
+
return {
|
|
714
|
+
error: 'invalid-type',
|
|
715
|
+
message: `Unknown type "${options.type}". Valid: ${[...validTypes].sort().join(', ')}.`,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Same discipline for --framework (fix #243) — a typo like 'flsk'
|
|
721
|
+
// silently filtered everything to an empty result.
|
|
722
|
+
if (options.framework) {
|
|
723
|
+
const validFrameworks = new Set(FRAMEWORK_PATTERNS.map(p => p.framework.toLowerCase()));
|
|
724
|
+
const wanted = String(options.framework).split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
|
|
725
|
+
const unknown = wanted.filter(f => !validFrameworks.has(f));
|
|
726
|
+
if (unknown.length > 0) {
|
|
727
|
+
return {
|
|
728
|
+
error: 'invalid-framework',
|
|
729
|
+
message: `Unknown framework "${unknown.join('", "')}". Valid: ${[...validFrameworks].sort().join(', ')}.`,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
625
734
|
// Build callback entrypoint map (call-pattern detection)
|
|
626
735
|
const callbackMap = buildCallbackEntrypointMap(index);
|
|
627
736
|
|
|
@@ -711,6 +820,9 @@ function detectEntrypoints(index, options = {}) {
|
|
|
711
820
|
// Check name-based patterns (main, init, TestXxx, etc.)
|
|
712
821
|
for (const np of namePatterns) {
|
|
713
822
|
if (!np.languages.has(fileEntry.language)) continue;
|
|
823
|
+
// Per-pattern symbol predicate (fix #243) — e.g. main must be
|
|
824
|
+
// a free function (Rust/Go) or a static method (Java)
|
|
825
|
+
if (np.symbolFilter && !np.symbolFilter(symbol)) continue;
|
|
714
826
|
if (np.pattern.test(name)) {
|
|
715
827
|
const key = `${symbol.file}:${symbol.startLine}:${name}`;
|
|
716
828
|
if (seen.has(key)) continue;
|
|
@@ -743,6 +855,17 @@ function detectEntrypoints(index, options = {}) {
|
|
|
743
855
|
if (seen.has(key)) continue;
|
|
744
856
|
seen.add(key);
|
|
745
857
|
|
|
858
|
+
let evidence = `${info.method} route handler`;
|
|
859
|
+
let registeredAt;
|
|
860
|
+
if (info.registrationFile) {
|
|
861
|
+
const regEntry = index.files.get(info.registrationFile);
|
|
862
|
+
const regRel = regEntry?.relativePath || info.registrationFile;
|
|
863
|
+
registeredAt = { file: regRel, line: info.registrationLine };
|
|
864
|
+
if (info.registrationFile !== info.file || info.registrationLine !== info.line) {
|
|
865
|
+
evidence += ` — registered at ${regRel}:${info.registrationLine}`;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
746
869
|
results.push({
|
|
747
870
|
name,
|
|
748
871
|
file: relPath,
|
|
@@ -751,7 +874,8 @@ function detectEntrypoints(index, options = {}) {
|
|
|
751
874
|
type: info.type,
|
|
752
875
|
framework: info.framework,
|
|
753
876
|
patternId: info.patternId,
|
|
754
|
-
evidence: [
|
|
877
|
+
evidence: [evidence],
|
|
878
|
+
...(registeredAt && { registeredAt }),
|
|
755
879
|
confidence: 0.90,
|
|
756
880
|
});
|
|
757
881
|
}
|
|
@@ -860,6 +984,14 @@ function detectEntrypoints(index, options = {}) {
|
|
|
860
984
|
const fmatches = fileMatches.get(symbol.file);
|
|
861
985
|
if (!fmatches || fmatches.length === 0) continue;
|
|
862
986
|
|
|
987
|
+
// Entry points are executable: enum members, interface
|
|
988
|
+
// property signatures, type aliases, and fields in a matched
|
|
989
|
+
// file are declarations, not runtime entries (fix #247 — a
|
|
990
|
+
// spec file listed its enum members as test entry points).
|
|
991
|
+
// Callable members carry their own symbols, so reachability
|
|
992
|
+
// seeding loses nothing.
|
|
993
|
+
if (NON_CALLABLE_TYPES.has(symbol.type)) continue;
|
|
994
|
+
|
|
863
995
|
// Apply narrow filter: for shebang/cli-main files, only allow
|
|
864
996
|
// identified entry symbols.
|
|
865
997
|
if (narrowAllowedByFile.has(symbol.file)) {
|
|
@@ -916,7 +1048,7 @@ function detectEntrypoints(index, options = {}) {
|
|
|
916
1048
|
|
|
917
1049
|
// Sort by file, then line
|
|
918
1050
|
filtered.sort((a, b) => {
|
|
919
|
-
if (a.file !== b.file) return a.file
|
|
1051
|
+
if (a.file !== b.file) return codeUnitCompare(a.file, b.file);
|
|
920
1052
|
return a.line - b.line;
|
|
921
1053
|
});
|
|
922
1054
|
|