devsmind-mcp 2.0.4 → 2.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.
@@ -0,0 +1,1079 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.parseNodeId = parseNodeId;
37
+ exports.resolveConnectionsLocally = resolveConnectionsLocally;
38
+ exports.extractNodeFromFile = extractNodeFromFile;
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ const ts = __importStar(require("typescript"));
42
+ const config_1 = require("./config");
43
+ /**
44
+ * Parses a DevsMind node ID into constituent parts
45
+ */
46
+ function parseNodeId(id) {
47
+ // Matches e.g., "{harrir-backend-products-service}/src/controllers/SearchIndexController.ts#SearchIndexController.searchFiltersV2"
48
+ const match = id.match(/^\{([^}]+)\}\/([^#]+)#(.+)$/);
49
+ if (!match)
50
+ return null;
51
+ const [, repo, filePath, symbolName] = match;
52
+ const parts = symbolName.split('.');
53
+ if (parts.length === 2) {
54
+ return { repo, filePath, symbolName, className: parts[0], memberName: parts[1] };
55
+ }
56
+ return { repo, filePath, symbolName };
57
+ }
58
+ /**
59
+ * Resolves path aliases and relative paths to match target files
60
+ */
61
+ function matchPaths(resolvedImport, targetFile) {
62
+ const cleanImport = resolvedImport.replace(/\\/g, '/').toLowerCase();
63
+ const cleanTarget = targetFile.replace(/\\/g, '/').toLowerCase();
64
+ // Strip extensions and standard index file conventions
65
+ const importBase = cleanImport.replace(/\.(d\.)?[jt]sx?$/, '').replace(/\/index$/, '');
66
+ const targetBase = cleanTarget.replace(/\.(d\.)?[jt]sx?$/, '').replace(/\/index$/, '');
67
+ return importBase === targetBase || cleanImport === cleanTarget;
68
+ }
69
+ /**
70
+ * Extracts imports from a TypeScript AST SourceFile
71
+ */
72
+ function getFileImports(sourceFile) {
73
+ const imports = [];
74
+ function visit(node) {
75
+ if (ts.isImportDeclaration(node)) {
76
+ if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
77
+ const moduleSpecifier = node.moduleSpecifier.text;
78
+ if (node.importClause) {
79
+ // Default import: import X from 'y'
80
+ if (node.importClause.name) {
81
+ imports.push({
82
+ importedName: node.importClause.name.text,
83
+ moduleSpecifier,
84
+ isDefault: true
85
+ });
86
+ }
87
+ // Named imports: import { A, B as C } from 'y'
88
+ if (node.importClause.namedBindings) {
89
+ const bindings = node.importClause.namedBindings;
90
+ if (ts.isNamedImports(bindings)) {
91
+ for (const element of bindings.elements) {
92
+ imports.push({
93
+ importedName: element.name.text,
94
+ moduleSpecifier,
95
+ isDefault: false
96
+ });
97
+ }
98
+ }
99
+ else if (ts.isNamespaceImport(bindings)) {
100
+ // import * as X from 'y'
101
+ imports.push({
102
+ importedName: bindings.name.text,
103
+ moduleSpecifier,
104
+ isDefault: false,
105
+ isNamespace: true
106
+ });
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
112
+ ts.forEachChild(node, visit);
113
+ }
114
+ ts.forEachChild(sourceFile, visit);
115
+ return imports;
116
+ }
117
+ /**
118
+ * True when this identifier sits in a *definition* position (the name being
119
+ * declared) rather than a *usage* position (a reference to something else).
120
+ * Counting definitions as references is the main false-positive source: an
121
+ * object-literal key `{ validateEmailAddress: false }`, a parameter name, or a
122
+ * local declaration name would otherwise "match" an unrelated target node that
123
+ * happens to share that name. Requires parent pointers (createSourceFile(..., true)).
124
+ */
125
+ function isDefinitionName(node) {
126
+ const parent = node.parent;
127
+ if (!parent)
128
+ return false;
129
+ // The declared name of a declaration (function foo, class Foo, const foo, param foo, foo() {} …)
130
+ if ((ts.isFunctionDeclaration(parent) || ts.isFunctionExpression(parent) ||
131
+ ts.isClassDeclaration(parent) || ts.isClassExpression(parent) ||
132
+ ts.isInterfaceDeclaration(parent) || ts.isTypeAliasDeclaration(parent) ||
133
+ ts.isEnumDeclaration(parent) || ts.isEnumMember(parent) ||
134
+ ts.isModuleDeclaration(parent) ||
135
+ ts.isMethodDeclaration(parent) || ts.isMethodSignature(parent) ||
136
+ ts.isPropertyDeclaration(parent) || ts.isPropertySignature(parent) ||
137
+ ts.isGetAccessorDeclaration(parent) || ts.isSetAccessorDeclaration(parent) ||
138
+ ts.isParameter(parent) || ts.isVariableDeclaration(parent) ||
139
+ ts.isBindingElement(parent)) &&
140
+ parent.name === node) {
141
+ return true;
142
+ }
143
+ // Object-literal key: `{ foo: ... }` — a definition. (Shorthand `{ foo }` is a
144
+ // real read, so it is intentionally NOT excluded here.)
145
+ if (ts.isPropertyAssignment(parent) && parent.name === node)
146
+ return true;
147
+ // Import/export binding names (the local aliases, not usages of the target)
148
+ if (ts.isImportSpecifier(parent) || ts.isImportClause(parent) ||
149
+ ts.isNamespaceImport(parent) || ts.isExportSpecifier(parent)) {
150
+ return true;
151
+ }
152
+ return false;
153
+ }
154
+ /**
155
+ * Traverses a TypeScript AST node to collect names *referenced* (used) within it,
156
+ * excluding definition/declaration positions (see isDefinitionName).
157
+ */
158
+ function collectReferencedNames(root) {
159
+ const names = new Set();
160
+ function visit(node) {
161
+ if (ts.isIdentifier(node)) {
162
+ if (!isDefinitionName(node))
163
+ names.add(node.text);
164
+ }
165
+ else if (ts.isPropertyAccessExpression(node)) {
166
+ if (node.name && ts.isIdentifier(node.name)) {
167
+ names.add(node.name.text);
168
+ }
169
+ }
170
+ else if (ts.isJsxOpeningElement(node)) {
171
+ if (node.tagName && ts.isIdentifier(node.tagName)) {
172
+ names.add(node.tagName.text);
173
+ }
174
+ }
175
+ else if (ts.isJsxSelfClosingElement(node)) {
176
+ if (node.tagName && ts.isIdentifier(node.tagName)) {
177
+ names.add(node.tagName.text);
178
+ }
179
+ }
180
+ ts.forEachChild(node, visit);
181
+ }
182
+ ts.forEachChild(root, visit);
183
+ return names;
184
+ }
185
+ /** Nodes that introduce their own variable scope (for free-variable analysis). */
186
+ function isFunctionLikeScope(node) {
187
+ return (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) ||
188
+ ts.isArrowFunction(node) || ts.isMethodDeclaration(node) ||
189
+ ts.isConstructorDeclaration(node) || ts.isGetAccessorDeclaration(node) ||
190
+ ts.isSetAccessorDeclaration(node));
191
+ }
192
+ /**
193
+ * Names bound directly in `scopeNode`'s own scope: its parameters plus every
194
+ * declaration anywhere in its body EXCEPT those inside nested function scopes (which
195
+ * own their bindings). Collected up front so forward references (a function that calls
196
+ * a sibling declared later) resolve as bound, not free.
197
+ */
198
+ function collectScopeBindings(scopeNode) {
199
+ const bound = new Set();
200
+ const add = (name) => {
201
+ if (name && ts.isIdentifier(name))
202
+ bound.add(name.text);
203
+ };
204
+ function collect(n) {
205
+ if (ts.isVariableDeclaration(n) || ts.isFunctionDeclaration(n) || ts.isClassDeclaration(n))
206
+ add(n.name);
207
+ else if (ts.isParameter(n) && ts.isIdentifier(n.name))
208
+ add(n.name);
209
+ else if (ts.isBindingElement(n) && ts.isIdentifier(n.name))
210
+ add(n.name);
211
+ else if (ts.isCatchClause(n) && n.variableDeclaration)
212
+ add(n.variableDeclaration.name);
213
+ // Do not descend into nested function scopes — their params/locals belong to them.
214
+ if (n !== scopeNode && isFunctionLikeScope(n))
215
+ return;
216
+ ts.forEachChild(n, collect);
217
+ }
218
+ ts.forEachChild(scopeNode, collect);
219
+ return bound;
220
+ }
221
+ /**
222
+ * Scope-aware free-variable analysis. Returns the names a node USES that are NOT declared
223
+ * within its own scope (its genuine external dependencies) — plus the set of `this.<member>`
224
+ * accesses. Locally-declared names, parameters, and nested-closure bindings are excluded,
225
+ * which removes the name-collision noise that the flat collectReferencedNames produced (a
226
+ * local `const total` no longer matches an unrelated node named `total`). Property-access
227
+ * member names and JSX tags are kept (used for member/namespace matching downstream).
228
+ */
229
+ function collectFreeReferences(root) {
230
+ const free = new Set();
231
+ const thisMembers = new Set();
232
+ function walk(node, stack) {
233
+ const scope = isFunctionLikeScope(node) ? [...stack, collectScopeBindings(node)] : stack;
234
+ if (ts.isPropertyAccessExpression(node) &&
235
+ node.expression.kind === ts.SyntaxKind.ThisKeyword &&
236
+ ts.isIdentifier(node.name)) {
237
+ thisMembers.add(node.name.text);
238
+ }
239
+ if (ts.isIdentifier(node)) {
240
+ const parent = node.parent;
241
+ if (isDefinitionName(node)) {
242
+ // definition position — not a reference
243
+ }
244
+ else if (parent && ts.isPropertyAccessExpression(parent) && parent.name === node) {
245
+ free.add(node.text); // member name of `a.b` — kept for member/namespace matching
246
+ }
247
+ else if (parent && ts.isQualifiedName(parent) && parent.right === node) {
248
+ free.add(node.text); // qualified type name RHS
249
+ }
250
+ else {
251
+ // genuine value/type reference — free iff not bound in any enclosing scope
252
+ if (!scope.some(s => s.has(node.text)))
253
+ free.add(node.text);
254
+ }
255
+ }
256
+ ts.forEachChild(node, child => walk(child, scope));
257
+ }
258
+ walk(root, []);
259
+ return { free, thisMembers };
260
+ }
261
+ /** Best-effort structural node type from an AST declaration (no framework subtype). */
262
+ function astBaseType(node) {
263
+ if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node))
264
+ return 'function';
265
+ if (ts.isMethodDeclaration(node) || ts.isMethodSignature(node))
266
+ return 'method';
267
+ if (ts.isClassDeclaration(node) || ts.isClassExpression(node))
268
+ return 'class';
269
+ if (ts.isInterfaceDeclaration(node))
270
+ return 'interface';
271
+ if (ts.isTypeAliasDeclaration(node))
272
+ return 'type_alias';
273
+ if (ts.isEnumDeclaration(node))
274
+ return 'enum';
275
+ if (ts.isVariableDeclaration(node)) {
276
+ const init = node.initializer;
277
+ if (init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init)))
278
+ return 'function';
279
+ if (init && ts.isClassExpression(init))
280
+ return 'class';
281
+ return 'variable';
282
+ }
283
+ if (ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node))
284
+ return 'variable';
285
+ return 'variable';
286
+ }
287
+ /**
288
+ * Searches an arbitrary subtree for a member matching propName — at any nesting depth.
289
+ * Covers two shapes findNodeInAst's top-level scan can't reach on its own:
290
+ * - object-literal properties/methods, e.g.
291
+ * `const api = createApi({ endpoints: (builder) => ({ myEndpoint: builder.mutation(...) }) })`
292
+ * - nested function/const declarations inside another function's body, e.g. a React
293
+ * component's locally-defined handlers: `function CartSidebar() { function handleX() {} }`
294
+ * In both cases the target isn't a class member or a standalone top-level declaration —
295
+ * it's a member sitting somewhere inside another declaration's body/initializer.
296
+ */
297
+ function findPropertyInContainer(containerNode, propName) {
298
+ let found = null;
299
+ function visit(node) {
300
+ if (found)
301
+ return;
302
+ if ((ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node) || ts.isMethodDeclaration(node)) &&
303
+ node.name &&
304
+ (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)) &&
305
+ node.name.text === propName) {
306
+ found = node;
307
+ return;
308
+ }
309
+ if ((ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node)) &&
310
+ node.name &&
311
+ ts.isIdentifier(node.name) &&
312
+ node.name.text === propName) {
313
+ found = node;
314
+ return;
315
+ }
316
+ ts.forEachChild(node, visit);
317
+ }
318
+ ts.forEachChild(containerNode, visit);
319
+ return found;
320
+ }
321
+ /**
322
+ * Framework-route adapter. Node IDs like `router.get("/boxy/regions")` refer to a
323
+ * specific call `router.get("/boxy/regions", handler)`, not a declared symbol. Find that
324
+ * exact call by its method (`get`) + first string-literal argument (the route path), so
325
+ * we isolate just that registration instead of scanning the whole (multi-route) file.
326
+ */
327
+ function findRouteCall(sourceFile, method, arg) {
328
+ let found = null;
329
+ function visit(node) {
330
+ if (found)
331
+ return;
332
+ if (ts.isCallExpression(node) &&
333
+ ts.isPropertyAccessExpression(node.expression) &&
334
+ node.expression.name.text === method &&
335
+ node.arguments.length > 0 &&
336
+ ts.isStringLiteralLike(node.arguments[0]) &&
337
+ node.arguments[0].text === arg) {
338
+ found = node;
339
+ return;
340
+ }
341
+ ts.forEachChild(node, visit);
342
+ }
343
+ ts.forEachChild(sourceFile, visit);
344
+ return found;
345
+ }
346
+ /** Navigate a dotted path through nested object-literal properties. */
347
+ function navigateObjectPath(obj, segments) {
348
+ let current = obj;
349
+ for (let i = 0; i < segments.length; i++) {
350
+ if (!current)
351
+ return null;
352
+ const seg = segments[i];
353
+ const prop = current.properties.find(p => p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && p.name.text === seg);
354
+ if (!prop)
355
+ return null;
356
+ if (i === segments.length - 1)
357
+ return prop;
358
+ if (ts.isPropertyAssignment(prop) && ts.isObjectLiteralExpression(prop.initializer)) {
359
+ current = prop.initializer;
360
+ }
361
+ else {
362
+ current = null;
363
+ }
364
+ }
365
+ return null;
366
+ }
367
+ /**
368
+ * Framework-container adapter. Node IDs like `HomePageComponent.methods._getLang` come
369
+ * from `Component({ methods: { _getLang() {} } })` factories (WeChat/Alipay mini-programs,
370
+ * and similar object-config frameworks) where the container name isn't a real declaration.
371
+ * Navigate the dotted path (minus the synthetic container name) inside a top-level
372
+ * factory call's object-literal argument, so we isolate just that method.
373
+ */
374
+ function findInFrameworkContainer(sourceFile, segments) {
375
+ if (segments.length === 0)
376
+ return null;
377
+ const objArgs = [];
378
+ for (const stmt of sourceFile.statements) {
379
+ let expr;
380
+ if (ts.isExpressionStatement(stmt))
381
+ expr = stmt.expression;
382
+ else if (ts.isExportAssignment(stmt))
383
+ expr = stmt.expression;
384
+ if (expr && ts.isCallExpression(expr)) {
385
+ for (const a of expr.arguments) {
386
+ if (ts.isObjectLiteralExpression(a))
387
+ objArgs.push(a);
388
+ }
389
+ }
390
+ }
391
+ // Precise: navigate the full path (e.g. methods -> _getLang).
392
+ for (const obj of objArgs) {
393
+ const node = navigateObjectPath(obj, segments);
394
+ if (node)
395
+ return node;
396
+ }
397
+ // Fallback: the last segment anywhere inside a factory object (handles path shapes
398
+ // whose middle segments don't map cleanly to nested object literals).
399
+ const last = segments[segments.length - 1];
400
+ for (const obj of objArgs) {
401
+ const node = findPropertyInContainer(obj, last);
402
+ if (node)
403
+ return node;
404
+ }
405
+ return null;
406
+ }
407
+ /**
408
+ * Searches for a class method, function, or block inside the file AST matching our symbol name
409
+ */
410
+ function findNodeInAst(sourceFile, className, symbolName) {
411
+ // Framework-route adapter: `router.get("/path")` → the specific registration call.
412
+ const routeMatch = symbolName.match(/^\w+\.\w+\((['"])(.+)\1\)$/);
413
+ if (routeMatch) {
414
+ const method = symbolName.slice(symbolName.indexOf('.') + 1, symbolName.indexOf('('));
415
+ const routeCall = findRouteCall(sourceFile, method, routeMatch[2]);
416
+ if (routeCall)
417
+ return routeCall;
418
+ }
419
+ let foundNode = null;
420
+ let containerCandidate = null;
421
+ function visit(node) {
422
+ if (foundNode)
423
+ return;
424
+ if (className) {
425
+ if (ts.isClassDeclaration(node) && node.name && node.name.text === className) {
426
+ // Search methods / properties of the class
427
+ for (const member of node.members) {
428
+ if (member.name && ts.isIdentifier(member.name) && member.name.text === symbolName.split('.').pop()) {
429
+ foundNode = member;
430
+ return;
431
+ }
432
+ }
433
+ }
434
+ // Track any declaration named `className` that isn't a class — a const object
435
+ // (`const api = createApi({...})`) or a function/component (`function CartSidebar() {}`,
436
+ // `const CartSidebar = () => {}`) — in case the class-member lookup above never
437
+ // matches. Used as a fallback below to search inside it for the member.
438
+ //
439
+ // Also covers frameworks (e.g. WeChat/Alipay mini-programs) where the "class"
440
+ // isn't a local declaration at all — it's a bare call to a global framework
441
+ // function whose object-literal argument holds the members:
442
+ // Component({ data: {...}, methods: { onTap() {...} }, didMount() {...} })
443
+ // Without this, `className` ("Component") never resolves to anything declared
444
+ // in the file, findNodeInAst falls back to scanning the WHOLE file's identifiers
445
+ // for every single member, and every property/method in the file gets wrongly
446
+ // cross-linked to every other one.
447
+ if (!containerCandidate &&
448
+ ((ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name) && node.name.text === className) ||
449
+ (ts.isFunctionDeclaration(node) && node.name && node.name.text === className) ||
450
+ (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === className))) {
451
+ containerCandidate = node;
452
+ }
453
+ }
454
+ else {
455
+ if ((ts.isFunctionDeclaration(node) ||
456
+ ts.isClassDeclaration(node) ||
457
+ ts.isInterfaceDeclaration(node) ||
458
+ ts.isTypeAliasDeclaration(node) ||
459
+ ts.isEnumDeclaration(node)) &&
460
+ node.name && node.name.text === symbolName) {
461
+ foundNode = node;
462
+ return;
463
+ }
464
+ if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name) && node.name.text === symbolName) {
465
+ foundNode = node;
466
+ return;
467
+ }
468
+ }
469
+ ts.forEachChild(node, visit);
470
+ }
471
+ ts.forEachChild(sourceFile, visit);
472
+ // Fallback: className resolved to a non-class declaration (object literal, factory call,
473
+ // etc). Search inside it for a property/method matching the member name, at any depth.
474
+ if (!foundNode && className && containerCandidate) {
475
+ const memberName = symbolName.includes('.') ? symbolName.split('.').pop() : symbolName;
476
+ foundNode = findPropertyInContainer(containerCandidate, memberName);
477
+ }
478
+ // Framework-container adapter: dotted IDs like `HomePageComponent.methods._getLang`
479
+ // whose container name isn't a real declaration. Navigate the path (minus the leading
480
+ // synthetic container segment) inside a top-level factory call's object literal.
481
+ if (!foundNode && symbolName.includes('.')) {
482
+ const segments = symbolName.split('.');
483
+ foundNode = findInFrameworkContainer(sourceFile, segments.slice(1));
484
+ }
485
+ return foundNode;
486
+ }
487
+ /**
488
+ * Generically extracts identifiers from non-JS/TS code files using regex
489
+ */
490
+ function collectRegexNames(code) {
491
+ const names = new Set();
492
+ // Strip block/line comments and string literals to reduce noise
493
+ const cleanCode = code
494
+ .replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '') // C-style comments
495
+ .replace(/#.*$/gm, '') // Scripting-style comments
496
+ .replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, ''); // String literals
497
+ // Match words that look like identifiers/variables/method names (alphanumeric + underscores)
498
+ const matches = cleanCode.match(/\b[a-zA-Z_][a-zA-Z0-9_]*\b/g);
499
+ if (matches) {
500
+ for (const m of matches) {
501
+ names.add(m);
502
+ }
503
+ }
504
+ return names;
505
+ }
506
+ // ---------------------------------------------------------------------------
507
+ // Caches (keyed by path + mtime so a long-lived server picks up edited files).
508
+ // ---------------------------------------------------------------------------
509
+ const sourceFileCache = new Map();
510
+ const tsPathsCache = new Map();
511
+ const barrelCache = new Map();
512
+ // Paths we've already determined have no index/barrel file — avoids repeating 8
513
+ // fs.existsSync probes for the (very common) non-barrel import case.
514
+ const barrelMissCache = new Set();
515
+ function statMtime(p) {
516
+ try {
517
+ return fs.statSync(p).mtimeMs;
518
+ }
519
+ catch {
520
+ return null;
521
+ }
522
+ }
523
+ /** Parse (and cache) a TS/JS source file with parent pointers set. */
524
+ function getSourceFile(filePath, content) {
525
+ const mtimeMs = statMtime(filePath) ?? -1;
526
+ const cached = sourceFileCache.get(filePath);
527
+ if (cached && cached.mtimeMs === mtimeMs && content === undefined)
528
+ return cached.sf;
529
+ const text = content ?? fs.readFileSync(filePath, 'utf-8');
530
+ const sf = ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true);
531
+ sourceFileCache.set(filePath, { mtimeMs, sf });
532
+ return sf;
533
+ }
534
+ /** Load (and cache) tsconfig/jsconfig baseUrl + paths for a repo root. */
535
+ function loadTsPaths(repoRoot) {
536
+ if (!repoRoot)
537
+ return null;
538
+ if (tsPathsCache.has(repoRoot))
539
+ return tsPathsCache.get(repoRoot);
540
+ let result = null;
541
+ for (const name of ['tsconfig.json', 'jsconfig.json']) {
542
+ const cfgPath = path.join(repoRoot, name);
543
+ if (!fs.existsSync(cfgPath))
544
+ continue;
545
+ try {
546
+ const text = fs.readFileSync(cfgPath, 'utf-8');
547
+ const parsed = ts.parseConfigFileTextToJson(cfgPath, text);
548
+ const opts = parsed.config?.compilerOptions;
549
+ if (opts && (opts.paths || opts.baseUrl)) {
550
+ const baseUrl = path.resolve(repoRoot, opts.baseUrl ?? '.');
551
+ result = { baseUrl, paths: opts.paths ?? {} };
552
+ }
553
+ else {
554
+ result = { baseUrl: repoRoot, paths: {} };
555
+ }
556
+ }
557
+ catch {
558
+ /* ignore malformed config */
559
+ }
560
+ break;
561
+ }
562
+ tsPathsCache.set(repoRoot, result);
563
+ return result;
564
+ }
565
+ /** Expand a module specifier to every plausible absolute base path it could resolve to. */
566
+ function resolveImportToPaths(moduleSpecifier, sourceDir, repoRoot, tsPaths) {
567
+ const out = [];
568
+ if (moduleSpecifier.startsWith('.')) {
569
+ out.push(path.resolve(sourceDir, moduleSpecifier));
570
+ return out;
571
+ }
572
+ // tsconfig `paths` aliases (e.g. "@utils/*": ["src/utils/*"])
573
+ if (tsPaths) {
574
+ for (const [pattern, targets] of Object.entries(tsPaths.paths)) {
575
+ const starPattern = pattern.includes('*');
576
+ if (starPattern) {
577
+ const [prefix, suffix] = pattern.split('*');
578
+ if (moduleSpecifier.startsWith(prefix) && moduleSpecifier.endsWith(suffix)) {
579
+ const middle = moduleSpecifier.slice(prefix.length, moduleSpecifier.length - suffix.length);
580
+ for (const t of targets) {
581
+ out.push(path.resolve(tsPaths.baseUrl, t.replace('*', middle)));
582
+ }
583
+ }
584
+ }
585
+ else if (moduleSpecifier === pattern) {
586
+ for (const t of targets)
587
+ out.push(path.resolve(tsPaths.baseUrl, t));
588
+ }
589
+ }
590
+ // baseUrl-relative bare import (e.g. baseUrl "src", import "utils/math")
591
+ if (tsPaths.baseUrl)
592
+ out.push(path.resolve(tsPaths.baseUrl, moduleSpecifier));
593
+ }
594
+ // Legacy hardcoded aliases, kept for repos without tsconfig paths
595
+ if (moduleSpecifier.startsWith('@/') || moduleSpecifier.startsWith('~/')) {
596
+ const cleanSpec = moduleSpecifier.substring(2);
597
+ if (repoRoot) {
598
+ out.push(path.resolve(repoRoot, cleanSpec));
599
+ out.push(path.resolve(repoRoot, 'src', cleanSpec));
600
+ }
601
+ }
602
+ else if (repoRoot) {
603
+ out.push(path.resolve(repoRoot, moduleSpecifier));
604
+ }
605
+ return out;
606
+ }
607
+ // Resolve a set of extension-less base paths to the first actual source file on disk
608
+ // (tries .ts/.tsx/.js/.jsx and /index.*). Returns null for bare/node_modules specifiers
609
+ // that don't map to a repo file. Cached — file existence is stable within an indexing run.
610
+ const existingFileCache = new Map();
611
+ function resolveToExistingFile(basePaths) {
612
+ const key = basePaths.join('|');
613
+ const cached = existingFileCache.get(key);
614
+ if (cached !== undefined)
615
+ return cached;
616
+ let result = null;
617
+ outer: for (const base of basePaths) {
618
+ if (!base)
619
+ continue;
620
+ for (const ext of ['.ts', '.tsx', '.js', '.jsx']) {
621
+ if (fs.existsSync(base + ext)) {
622
+ result = base + ext;
623
+ break outer;
624
+ }
625
+ }
626
+ for (const idx of ['/index.ts', '/index.tsx', '/index.js', '/index.jsx']) {
627
+ if (fs.existsSync(base + idx)) {
628
+ result = base + idx;
629
+ break outer;
630
+ }
631
+ }
632
+ try {
633
+ if (fs.existsSync(base) && fs.statSync(base).isFile()) {
634
+ result = base;
635
+ break outer;
636
+ }
637
+ }
638
+ catch { /* ignore */ }
639
+ }
640
+ existingFileCache.set(key, result);
641
+ return result;
642
+ }
643
+ /** Parse (and cache) an index/barrel file's re-export declarations. */
644
+ function getBarrelReexports(resolvedImportPath) {
645
+ const indexCandidates = [
646
+ path.join(resolvedImportPath, 'index.ts'),
647
+ path.join(resolvedImportPath, 'index.tsx'),
648
+ path.join(resolvedImportPath, 'index.js'),
649
+ path.join(resolvedImportPath, 'index.jsx'),
650
+ resolvedImportPath + '.ts',
651
+ resolvedImportPath + '.tsx',
652
+ resolvedImportPath + '.js',
653
+ resolvedImportPath + '.jsx',
654
+ ];
655
+ if (barrelMissCache.has(resolvedImportPath))
656
+ return [];
657
+ const indexPath = indexCandidates.find(p => fs.existsSync(p));
658
+ if (!indexPath) {
659
+ barrelMissCache.add(resolvedImportPath);
660
+ return [];
661
+ }
662
+ const mtimeMs = statMtime(indexPath) ?? -1;
663
+ const cached = barrelCache.get(indexPath);
664
+ if (cached && cached.mtimeMs === mtimeMs)
665
+ return cached.reexports;
666
+ const reexports = [];
667
+ try {
668
+ const sf = getSourceFile(indexPath);
669
+ const dir = path.dirname(indexPath);
670
+ sf.forEachChild(n => {
671
+ if (ts.isExportDeclaration(n) && n.moduleSpecifier && ts.isStringLiteral(n.moduleSpecifier)) {
672
+ const spec = n.moduleSpecifier.text;
673
+ if (!spec.startsWith('.'))
674
+ return; // only follow local re-exports
675
+ const resolvedBase = path.resolve(dir, spec);
676
+ if (n.exportClause && ts.isNamedExports(n.exportClause)) {
677
+ for (const el of n.exportClause.elements) {
678
+ reexports.push({ name: el.name.text, resolvedBase });
679
+ }
680
+ }
681
+ else {
682
+ reexports.push({ name: null, resolvedBase }); // export * from '...'
683
+ }
684
+ }
685
+ });
686
+ }
687
+ catch {
688
+ /* ignore */
689
+ }
690
+ barrelCache.set(indexPath, { mtimeMs, reexports });
691
+ return reexports;
692
+ }
693
+ // Resolve a file's `export default` so a default import (`import X from './y'`) can be
694
+ // linked to the ONE node it binds to — not every symbol in the file. Returns:
695
+ // - the export's NAME for a named default (`export default OrderController`),
696
+ // - ANON_DEFAULT for an anonymous default (`export default Joi.object({...})`) — the
697
+ // file HAS a default export but it has no source-level name; callers bridge it to the
698
+ // import alias by (case-insensitive) node name,
699
+ // - null when there is no default export at all.
700
+ const ANON_DEFAULT = 'anon';
701
+ const defaultExportCache = new Map();
702
+ function getDefaultExportName(filePath) {
703
+ const ext = path.extname(filePath).toLowerCase();
704
+ if (!['.ts', '.tsx', '.js', '.jsx'].includes(ext))
705
+ return null;
706
+ const mtimeMs = statMtime(filePath) ?? -1;
707
+ const cached = defaultExportCache.get(filePath);
708
+ if (cached && cached.mtimeMs === mtimeMs)
709
+ return cached.name;
710
+ let name = null;
711
+ try {
712
+ const sf = getSourceFile(filePath);
713
+ for (const stmt of sf.statements) {
714
+ // `export default <expr>`
715
+ if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
716
+ const expr = stmt.expression;
717
+ if (ts.isIdentifier(expr))
718
+ name = expr.text;
719
+ else if ((ts.isClassExpression(expr) || ts.isFunctionExpression(expr)) && expr.name)
720
+ name = expr.name.text;
721
+ else
722
+ name = ANON_DEFAULT; // `export default Joi.object({...})`, `{...}`, `() => …`
723
+ break;
724
+ }
725
+ // `export default class X {}` / `export default function X() {}`
726
+ if ((ts.isClassDeclaration(stmt) || ts.isFunctionDeclaration(stmt)) &&
727
+ stmt.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) &&
728
+ stmt.modifiers?.some(m => m.kind === ts.SyntaxKind.DefaultKeyword)) {
729
+ name = stmt.name ? stmt.name.text : ANON_DEFAULT;
730
+ break;
731
+ }
732
+ // `export { X as default }`
733
+ if (ts.isExportDeclaration(stmt) && stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
734
+ for (const el of stmt.exportClause.elements) {
735
+ if (el.name.text === 'default') {
736
+ name = (el.propertyName ?? el.name).text;
737
+ break;
738
+ }
739
+ }
740
+ if (name)
741
+ break;
742
+ }
743
+ }
744
+ }
745
+ catch {
746
+ /* ignore */
747
+ }
748
+ defaultExportCache.set(filePath, { mtimeMs, name });
749
+ return name;
750
+ }
751
+ /**
752
+ * Locally analyzes the source file and resolves references to candidate nodes
753
+ */
754
+ function resolveConnectionsLocally(sourceNodeId, sourceFilePath, candidateNodes, devmindPath, onMissing) {
755
+ const parsedSource = parseNodeId(sourceNodeId);
756
+ if (!parsedSource)
757
+ return [];
758
+ const connections = new Set();
759
+ // Determine repository root path for non-relative imports
760
+ let repoRoot = '';
761
+ try {
762
+ const context = (0, config_1.loadProjectContext)(devmindPath);
763
+ repoRoot = (0, config_1.resolveRepoPath)(context, parsedSource.repo) || '';
764
+ }
765
+ catch (err) {
766
+ // Fallback if config loading fails
767
+ }
768
+ // Check if file exists
769
+ if (!fs.existsSync(sourceFilePath)) {
770
+ return [];
771
+ }
772
+ const fileContent = fs.readFileSync(sourceFilePath, 'utf-8');
773
+ const ext = path.extname(sourceFilePath).toLowerCase();
774
+ const isTsOrJs = ['.ts', '.tsx', '.js', '.jsx'].includes(ext);
775
+ const tsPaths = loadTsPaths(repoRoot);
776
+ let referencedNames = new Set();
777
+ let thisMembers = new Set();
778
+ let imports = [];
779
+ // True when we could NOT isolate this symbol's own AST subtree and fell back to
780
+ // scanning the whole file. In that mode every identifier in the file is in scope,
781
+ // so same-file links become meaningless — we suppress them to avoid cross-linking
782
+ // every symbol in the file to every other one (the worst false-positive source).
783
+ let isolationFailed = false;
784
+ if (isTsOrJs) {
785
+ try {
786
+ const sourceFile = getSourceFile(sourceFilePath);
787
+ imports = getFileImports(sourceFile);
788
+ // Locate the AST node for this class/method/function, then collect its FREE
789
+ // variables (names used but not declared in its own scope). Scope-awareness means
790
+ // locals/params are excluded, so they can no longer collide with unrelated nodes.
791
+ const astNode = findNodeInAst(sourceFile, parsedSource.className, parsedSource.symbolName);
792
+ if (astNode) {
793
+ const fr = collectFreeReferences(astNode);
794
+ referencedNames = fr.free;
795
+ thisMembers = fr.thisMembers;
796
+ }
797
+ else {
798
+ // Fallback: scan the whole file, but mark isolation as failed so downstream
799
+ // matching stays conservative (cross-file, import-gated links only).
800
+ referencedNames = collectReferencedNames(sourceFile);
801
+ isolationFailed = true;
802
+ }
803
+ }
804
+ catch (err) {
805
+ // TS AST fallback to regex in case of parsing failures
806
+ referencedNames = collectRegexNames(fileContent);
807
+ isolationFailed = true;
808
+ }
809
+ }
810
+ else {
811
+ // Non-JS/TS code uses regex identifier matching
812
+ referencedNames = collectRegexNames(fileContent);
813
+ isolationFailed = true;
814
+ }
815
+ const sourceDir = path.dirname(sourceFilePath);
816
+ // Resolve every import's candidate paths + barrel re-exports ONCE per source node.
817
+ // This is candidate-independent, so doing it inside the candidate loop (7000+ nodes)
818
+ // was re-running filesystem probes thousands of times per source node.
819
+ const resolvedImports = [];
820
+ if (isTsOrJs) {
821
+ for (const imp of imports) {
822
+ const paths = resolveImportToPaths(imp.moduleSpecifier, sourceDir, repoRoot, tsPaths);
823
+ const barrels = [];
824
+ for (const p of paths) {
825
+ if (!p)
826
+ continue;
827
+ const rx = getBarrelReexports(p);
828
+ if (rx.length)
829
+ barrels.push(...rx);
830
+ }
831
+ resolvedImports.push({
832
+ importedName: imp.importedName,
833
+ isDefault: !!imp.isDefault,
834
+ isNamespace: !!imp.isNamespace,
835
+ paths,
836
+ barrels
837
+ });
838
+ }
839
+ }
840
+ // How many nodes each file contributes — used to safely link default imports of tiny,
841
+ // single-purpose files (Joi schemas, configs) whose anonymous default export the
842
+ // extractor named inconsistently (`default`, `Foo.schema`), so name matching fails.
843
+ const nodesPerFile = new Map();
844
+ const normFile = (fp) => path.resolve(fp).replace(/\\/g, '/').toLowerCase();
845
+ // Names present per file — used for missing-node detection (does an imported symbol
846
+ // actually have a node in its file?). Includes each node's name plus its id's symbol parts.
847
+ const nodeNamesByFile = onMissing ? new Map() : null;
848
+ for (const n of candidateNodes) {
849
+ for (const p of String(n.file_path).split(',').map(s => s.trim()).filter(Boolean)) {
850
+ const k = normFile(p);
851
+ nodesPerFile.set(k, (nodesPerFile.get(k) ?? 0) + 1);
852
+ if (nodeNamesByFile) {
853
+ let set = nodeNamesByFile.get(k);
854
+ if (!set) {
855
+ set = new Set();
856
+ nodeNamesByFile.set(k, set);
857
+ }
858
+ set.add(n.name);
859
+ const parsed = parseNodeId(n.id);
860
+ if (parsed) {
861
+ set.add(parsed.symbolName);
862
+ if (parsed.className)
863
+ set.add(parsed.className);
864
+ if (parsed.memberName)
865
+ set.add(parsed.memberName);
866
+ }
867
+ }
868
+ }
869
+ }
870
+ for (const targetNode of candidateNodes) {
871
+ if (targetNode.id === sourceNodeId)
872
+ continue;
873
+ const parsedTarget = parseNodeId(targetNode.id);
874
+ if (!parsedTarget)
875
+ continue;
876
+ const isSameFile = path.resolve(targetNode.file_path).replace(/\\/g, '/').toLowerCase() ===
877
+ path.resolve(sourceFilePath).replace(/\\/g, '/').toLowerCase();
878
+ const symbolName = parsedTarget.symbolName;
879
+ const memberName = parsedTarget.memberName;
880
+ const className = parsedTarget.className;
881
+ if (isSameFile) {
882
+ // Local dependency within same file. Skip when isolation failed — in
883
+ // whole-file-scan mode every symbol would link to every other one.
884
+ if (isolationFailed)
885
+ continue;
886
+ // For a sibling method in the SAME class, prefer a `this.<member>` access (precise)
887
+ // but still accept a bare free reference to the member name.
888
+ if (memberName && className && className === parsedSource.className && thisMembers.has(memberName)) {
889
+ connections.add(targetNode.id);
890
+ continue;
891
+ }
892
+ const nameToCheck = memberName || symbolName;
893
+ if (referencedNames.has(nameToCheck)) {
894
+ connections.add(targetNode.id);
895
+ }
896
+ continue;
897
+ }
898
+ // Different files: cross-file reference validation
899
+ let isImported = false;
900
+ let importedAsNames = [];
901
+ // Default imports can be renamed to anything by the importer (e.g.
902
+ // `import addToCartSchema from "./schema"` where the target's own declared name is
903
+ // "AddOrUpdateCartItemSchema"). Track these separately — matching them requires
904
+ // checking the LOCAL ALIAS was referenced, not the target's original name, since the
905
+ // original name may never appear anywhere in the importing file at all.
906
+ let importedAsDefaultNames = [];
907
+ // Whether the target's file was pulled in via `import * as ns from '...'`. With a
908
+ // namespace import the whole module is in scope, so a `ns.symbol` property access
909
+ // is the real usage signal (the target's own name, not a local alias).
910
+ let importedViaNamespace = false;
911
+ // Match the precomputed imports against this candidate's file (cheap string ops only).
912
+ if (isTsOrJs && resolvedImports.length > 0) {
913
+ for (const ri of resolvedImports) {
914
+ let matched = ri.paths.some(p => p && matchPaths(p, targetNode.file_path));
915
+ // Barrel hit: an index re-exports the target's file. Only accept when the
916
+ // re-exported name matches the import binding (or it's an `export *`).
917
+ if (!matched && ri.barrels.length > 0) {
918
+ matched = ri.barrels.some(rx => (rx.name === null || rx.name === ri.importedName) &&
919
+ matchPaths(rx.resolvedBase, targetNode.file_path));
920
+ }
921
+ if (matched) {
922
+ isImported = true;
923
+ importedAsNames.push(ri.importedName);
924
+ if (ri.isDefault)
925
+ importedAsDefaultNames.push(ri.importedName);
926
+ if (ri.isNamespace)
927
+ importedViaNamespace = true;
928
+ }
929
+ }
930
+ }
931
+ if (isImported) {
932
+ if (memberName) {
933
+ // Reference-based member matching relies on the subtree being isolated. When
934
+ // isolation failed we're scanning the whole file, so EVERY method of an imported
935
+ // class would match — the explosive false-positive case. Suppress those here and
936
+ // keep only the reliable explicit-import match below.
937
+ if (!isolationFailed) {
938
+ // e.g. Class.method: the class must be imported AND actually referenced in
939
+ // this subtree (e.g. `new UserService()`), and the method name referenced.
940
+ // Requiring the class be referenced — not just imported at file level —
941
+ // prevents `res.status(...)` from matching an imported `OrderService.status`.
942
+ if (className &&
943
+ importedAsNames.includes(className) &&
944
+ referencedNames.has(className) &&
945
+ referencedNames.has(memberName)) {
946
+ connections.add(targetNode.id);
947
+ continue;
948
+ }
949
+ // Class was a renamed default export — match on the (possibly aliased) import
950
+ // binding instead of the original class name, which may not appear in the file.
951
+ // Gate on the class actually BEING the target file's default export, so a
952
+ // default import doesn't link to every symbol in a large file.
953
+ if (className && referencedNames.has(memberName)) {
954
+ const defName = getDefaultExportName(targetNode.file_path);
955
+ if (defName !== null &&
956
+ importedAsDefaultNames.some(alias => referencedNames.has(alias) &&
957
+ (className === defName ||
958
+ (defName === ANON_DEFAULT && alias.toLowerCase() === className.toLowerCase())))) {
959
+ connections.add(targetNode.id);
960
+ continue;
961
+ }
962
+ }
963
+ // Namespace import: `ns.Class.method` / `ns.member` — the class/member is
964
+ // reached through the namespace object, so its name appears as a reference.
965
+ if (importedViaNamespace && referencedNames.has(memberName)) {
966
+ connections.add(targetNode.id);
967
+ continue;
968
+ }
969
+ }
970
+ // NOTE: we intentionally do NOT match `Class.method` on `importedAsNames.includes(memberName)`.
971
+ // A class method is never importable by name, so that only ever fires on a name
972
+ // collision with a same-named FREE function import (e.g. `import { formatDate }`
973
+ // matching a `Utils.formatDate` method) — a pure false positive.
974
+ }
975
+ else {
976
+ // Top-level function/variable imported & referenced
977
+ if (importedAsNames.includes(symbolName) && referencedNames.has(symbolName)) {
978
+ connections.add(targetNode.id);
979
+ continue;
980
+ }
981
+ // Renamed default export: the target's own declared name may never appear in
982
+ // this file at all — only the local alias the importer chose. Only link to the
983
+ // node that IS the file's default export, not every symbol in that file. For an
984
+ // anonymous default (`export default Joi.object({...})`) there's no source name,
985
+ // so bridge the alias to the extractor's node name case-insensitively
986
+ // (`createOrderSchema` → `CreateOrderSchema`).
987
+ const defName = getDefaultExportName(targetNode.file_path);
988
+ if (defName !== null &&
989
+ importedAsDefaultNames.some(alias => referencedNames.has(alias) &&
990
+ (symbolName === defName ||
991
+ (defName === ANON_DEFAULT && alias.toLowerCase() === symbolName.toLowerCase())))) {
992
+ connections.add(targetNode.id);
993
+ continue;
994
+ }
995
+ // Namespace import: `ns.symbol(...)` — the target's own name appears as a
996
+ // property access on the namespace object. Requires isolation (whole-file scan
997
+ // would match every member of the namespaced module).
998
+ if (!isolationFailed && importedViaNamespace && referencedNames.has(symbolName)) {
999
+ connections.add(targetNode.id);
1000
+ continue;
1001
+ }
1002
+ }
1003
+ }
1004
+ // Tiny single-purpose file with an ANONYMOUS default export (Joi schemas, config
1005
+ // objects): the extractor names these nodes inconsistently (`default`, `Foo.schema`,
1006
+ // dotted/3-part), so every name-based branch above misses them. If the file was
1007
+ // default-imported and its alias is referenced here, and the file is small enough that
1008
+ // its default export is unambiguous, link it. The node-count gate keeps this from
1009
+ // re-exploding on large files (whose default export is virtually always named anyway).
1010
+ if (isImported &&
1011
+ importedAsDefaultNames.some(alias => referencedNames.has(alias)) &&
1012
+ getDefaultExportName(targetNode.file_path) === ANON_DEFAULT &&
1013
+ (nodesPerFile.get(normFile(targetNode.file_path)) ?? 99) <= 3) {
1014
+ connections.add(targetNode.id);
1015
+ continue;
1016
+ }
1017
+ // Fallback: Only allow for top-level, non-class symbols (no className)
1018
+ // within the same repository, and require it to be very specific/long (length >= 16).
1019
+ // Skip when isolation failed — this is the only branch that links with NO import
1020
+ // relationship, so under a whole-file/regex scan it would connect any long token
1021
+ // (even in non-JS files) to a same-named node across files.
1022
+ if (!isolationFailed && !className && parsedSource.repo === parsedTarget.repo) {
1023
+ const nameToCheck = symbolName;
1024
+ // Free-variable analysis already excludes locals, so the old `commonNames` denylist
1025
+ // (which existed to blunt local-var noise) is no longer needed.
1026
+ if (nameToCheck.length >= 16 && referencedNames.has(nameToCheck)) {
1027
+ connections.add(targetNode.id);
1028
+ }
1029
+ }
1030
+ }
1031
+ // Missing-node detection: a name imported from a real repo file and actually used here,
1032
+ // but with no node in that file, is a Phase-1 extraction gap. (Namespace imports are
1033
+ // skipped — the specific missing symbol can't be attributed. node_modules/bare specifiers
1034
+ // resolve to no repo file and are ignored.)
1035
+ if (onMissing && nodeNamesByFile && isTsOrJs && !isolationFailed) {
1036
+ for (const ri of resolvedImports) {
1037
+ if (ri.isNamespace || !referencedNames.has(ri.importedName))
1038
+ continue;
1039
+ const file = resolveToExistingFile(ri.paths);
1040
+ if (!file)
1041
+ continue;
1042
+ const names = nodeNamesByFile.get(normFile(file));
1043
+ const satisfied = !!names && (names.has(ri.importedName) || (ri.isDefault && names.size > 0));
1044
+ if (!satisfied) {
1045
+ onMissing({ sourceNodeId, name: ri.importedName, targetFile: file });
1046
+ }
1047
+ }
1048
+ }
1049
+ return Array.from(connections);
1050
+ }
1051
+ /**
1052
+ * Derive a node's identity/type/code directly from its declaration in a file — deterministic,
1053
+ * no LLM. Used by `--fill-missing` to create nodes that Phase-1 extraction skipped. Returns
1054
+ * null when the file isn't TS/JS or the symbol can't be located.
1055
+ */
1056
+ function extractNodeFromFile(filePath, symbolName) {
1057
+ const ext = path.extname(filePath).toLowerCase();
1058
+ if (!['.ts', '.tsx', '.js', '.jsx'].includes(ext))
1059
+ return null;
1060
+ try {
1061
+ const sf = getSourceFile(filePath);
1062
+ const parts = symbolName.split('.');
1063
+ const className = parts.length === 2 ? parts[0] : undefined;
1064
+ const node = findNodeInAst(sf, className, symbolName);
1065
+ if (!node)
1066
+ return null;
1067
+ const code = node.getText(sf);
1068
+ return {
1069
+ name: parts[parts.length - 1] || symbolName,
1070
+ type: astBaseType(node),
1071
+ signature: code.split('\n')[0].slice(0, 200),
1072
+ codeSnapshot: code
1073
+ };
1074
+ }
1075
+ catch {
1076
+ return null;
1077
+ }
1078
+ }
1079
+ //# sourceMappingURL=ast.js.map