codesearch-mcp 0.1.0-alpha → 0.2.1-alpha

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 (64) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +146 -41
  3. package/dist/app.js +4 -19
  4. package/dist/src/commands/IndexCommand.js +68 -7
  5. package/dist/src/config-error.d.ts +7 -0
  6. package/dist/src/config-error.js +14 -0
  7. package/dist/src/config-store.d.ts +30 -12
  8. package/dist/src/config-store.js +81 -26
  9. package/dist/src/config.d.ts +5 -4
  10. package/dist/src/config.js +14 -16
  11. package/dist/src/constants.d.ts +8 -2
  12. package/dist/src/constants.js +8 -5
  13. package/dist/src/db/index.d.ts +1 -1
  14. package/dist/src/db/index.js +2 -1
  15. package/dist/src/db/schema.d.ts +75 -0
  16. package/dist/src/db/schema.js +10 -1
  17. package/dist/src/enriched-prompt.d.ts +8 -0
  18. package/dist/src/enriched-prompt.js +87 -0
  19. package/dist/src/help.js +1 -1
  20. package/dist/src/interfaces/IAstParser.d.ts +18 -2
  21. package/dist/src/interfaces/IDatabase.d.ts +31 -0
  22. package/dist/src/interfaces/ILogger.d.ts +7 -0
  23. package/dist/src/interfaces/ISearcher.d.ts +3 -0
  24. package/dist/src/services/AstChunker.d.ts +11 -0
  25. package/dist/src/services/AstChunker.js +44 -0
  26. package/dist/src/services/AstParserService.d.ts +39 -7
  27. package/dist/src/services/AstParserService.js +504 -104
  28. package/dist/src/services/DatabaseService.d.ts +17 -0
  29. package/dist/src/services/DatabaseService.js +130 -2
  30. package/dist/src/services/EmbedderFactory.d.ts +1 -1
  31. package/dist/src/services/EmbedderFactory.js +2 -5
  32. package/dist/src/services/IndexerService.d.ts +4 -14
  33. package/dist/src/services/IndexerService.js +54 -156
  34. package/dist/src/services/InfinityEmbedder.d.ts +14 -55
  35. package/dist/src/services/InfinityEmbedder.js +30 -141
  36. package/dist/src/services/LoggerService.d.ts +3 -1
  37. package/dist/src/services/LoggerService.js +17 -3
  38. package/dist/src/services/McpServer.js +3 -3
  39. package/dist/src/services/SearcherService.d.ts +39 -4
  40. package/dist/src/services/SearcherService.js +254 -81
  41. package/dist/src/services/WebServerService.d.ts +41 -3
  42. package/dist/src/services/WebServerService.js +90 -2
  43. package/dist/src/services/infinity-provisioning.d.ts +115 -0
  44. package/dist/src/services/infinity-provisioning.js +516 -0
  45. package/dist/src/services/legacy-ollama-index.d.ts +24 -0
  46. package/dist/src/services/legacy-ollama-index.js +61 -0
  47. package/dist/src/services/reranker.d.ts +3 -3
  48. package/dist/src/services/reranker.js +3 -3
  49. package/dist/src/setup.d.ts +37 -0
  50. package/dist/src/setup.js +111 -157
  51. package/dist/src/symbol-name.d.ts +44 -0
  52. package/dist/src/symbol-name.js +68 -0
  53. package/dist/src/symbol-rows.d.ts +12 -0
  54. package/dist/src/symbol-rows.js +38 -0
  55. package/dist/src/tools/FindSymbolTool.js +30 -7
  56. package/dist/src/tools/GetCallersTool.js +20 -5
  57. package/docker/docker-compose.yml +5 -15
  58. package/docs/architecture.svg +103 -0
  59. package/docs/web-dashboard.md +36 -17
  60. package/package.json +1 -6
  61. package/dist/src/commands/BackfillCommand.d.ts +0 -7
  62. package/dist/src/commands/BackfillCommand.js +0 -37
  63. package/dist/src/services/OllamaEmbedder.d.ts +0 -25
  64. package/dist/src/services/OllamaEmbedder.js +0 -111
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.AstParserService = void 0;
37
37
  const path = __importStar(require("path"));
38
38
  const fs = __importStar(require("fs"));
39
+ const typescript_1 = require("typescript");
39
40
  const ts_morph_1 = require("ts-morph");
40
41
  const LoggerService_1 = require("./LoggerService");
41
42
  const FALLBACK_OPTS = {
@@ -44,6 +45,7 @@ const FALLBACK_OPTS = {
44
45
  compilerOptions: { allowJs: true, noEmit: true },
45
46
  };
46
47
  const MORPH_CACHE_MAX = 10;
48
+ const VALID_SEGMENT = /^#?[\w$]+$/;
47
49
  class AstParserService {
48
50
  logger;
49
51
  morphProjects = new Map();
@@ -60,7 +62,7 @@ class AstParserService {
60
62
  const morphProject = this.createMorphProject(projectRoot, skipDependencyResolution, rootTsConfigPath);
61
63
  for (const filePath of filePaths) {
62
64
  if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
63
- analysisMap.set(filePath, { edges: [], symbols: [], calls: [] });
65
+ analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [] });
64
66
  continue;
65
67
  }
66
68
  try {
@@ -68,22 +70,12 @@ class AstParserService {
68
70
  }
69
71
  catch (err) {
70
72
  this.logger.warn('AstParser: failed to add source file', { filePath, reason: err.message });
71
- analysisMap.set(filePath, { edges: [], symbols: [], calls: [] });
73
+ analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [] });
72
74
  }
73
75
  }
74
76
  for (const sourceFile of morphProject.getSourceFiles()) {
75
77
  const filePath = sourceFile.getFilePath();
76
- try {
77
- const edges = this.extractEdges(sourceFile, filePath, serviceName);
78
- const symbols = this.extractSymbols(sourceFile, filePath, serviceName);
79
- const calls = this.extractCalls(sourceFile, filePath, serviceName);
80
- const ownerRanges = this.extractOwnerRanges(sourceFile);
81
- analysisMap.set(filePath, { edges, symbols, calls, ownerRanges });
82
- }
83
- catch (err) {
84
- this.logger.warn('AstParser: failed to analyze file', { filePath, reason: err.message });
85
- analysisMap.set(filePath, { edges: [], symbols: [], calls: [] });
86
- }
78
+ analysisMap.set(filePath, this.analyzeSourceFile(sourceFile, filePath, serviceName));
87
79
  }
88
80
  // Release source file memory before returning
89
81
  for (const sourceFile of morphProject.getSourceFiles()) {
@@ -110,7 +102,7 @@ class AstParserService {
110
102
  this.logger.debug('AstParser: analyzeFile start', { filePath, serviceName, projectRoot });
111
103
  if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
112
104
  this.logger.debug('AstParser: analyzeFile skip (non-TS)', { filePath, ms: Date.now() - t0 });
113
- return { edges: [], symbols: [], calls: [] };
105
+ return { edges: [], symbols: [], calls: [], members: [] };
114
106
  }
115
107
  const morphProject = this.getMorphProject(projectRoot);
116
108
  const existing = morphProject.getSourceFile(filePath);
@@ -122,21 +114,48 @@ class AstParserService {
122
114
  }
123
115
  catch (err) {
124
116
  this.logger.warn('AstParser: analyzeFile failed to add source file', { filePath, reason: err.message, ms: Date.now() - t0 });
125
- return { edges: [], symbols: [], calls: [] };
117
+ return { edges: [], symbols: [], calls: [], members: [] };
126
118
  }
127
- const edges = this.extractEdges(sourceFile, filePath, serviceName);
128
- const symbols = this.extractSymbols(sourceFile, filePath, serviceName);
129
- const calls = this.extractCalls(sourceFile, filePath, serviceName);
130
- const ownerRanges = this.extractOwnerRanges(sourceFile);
119
+ const result = this.analyzeSourceFile(sourceFile, filePath, serviceName);
131
120
  morphProject.removeSourceFile(sourceFile);
132
- this.logger.debug('AstParser: analyzeFile done', { filePath, edges: edges.length, symbols: symbols.length, calls: calls.length, ms: Date.now() - t0 });
133
- return { edges, symbols, calls, ownerRanges };
121
+ this.logger.debug('AstParser: analyzeFile done', {
122
+ filePath, edges: result.edges.length, symbols: result.symbols.length, members: result.members.length, calls: result.calls.length, ms: Date.now() - t0,
123
+ });
124
+ return result;
134
125
  }
135
126
  // Kept for test compatibility (extract-import-edges.test.ts).
136
127
  async extractImportEdges(filePath, serviceName, projectRoot) {
137
128
  const { edges } = await this.analyzeFile(filePath, serviceName, projectRoot);
138
129
  return edges;
139
130
  }
131
+ analyzeSourceFile(sourceFile, filePath, serviceName) {
132
+ try {
133
+ const edges = this.extractEdges(sourceFile, filePath, serviceName);
134
+ const symbolCandidates = this.extractSymbols(sourceFile, filePath, serviceName);
135
+ const symbols = symbolCandidates.map(c => c.entry);
136
+ // Member extraction (classes anywhere, class/interface/object-literal
137
+ // members, namespace-level declarations) is isolated in its own try: a
138
+ // bug there must never change a file's top-level symbols, chunks or
139
+ // embedding prompt (see the `symbol-index-members` retrieval-invariance
140
+ // requirement).
141
+ let memberCandidates = [];
142
+ try {
143
+ memberCandidates = this.extractMembers(sourceFile, filePath, serviceName);
144
+ }
145
+ catch (err) {
146
+ this.logger.warn('AstParser: member extraction failed', { filePath, reason: err.message });
147
+ memberCandidates = [];
148
+ }
149
+ const members = memberCandidates.map(c => c.entry);
150
+ const calls = this.extractCalls(sourceFile, filePath, serviceName);
151
+ const ownerRanges = this.buildOwnerRanges([...symbolCandidates, ...memberCandidates]);
152
+ return { edges, symbols, members, calls, ownerRanges };
153
+ }
154
+ catch (err) {
155
+ this.logger.warn('AstParser: failed to analyze file', { filePath, reason: err.message });
156
+ return { edges: [], symbols: [], calls: [], members: [] };
157
+ }
158
+ }
140
159
  // ── private: MorphProject cache (LRU, max MORPH_CACHE_MAX entries) ──────────
141
160
  createMorphProject(projectRoot, skipFileDependencyResolution = false, rootTsConfigPath) {
142
161
  const t0 = Date.now();
@@ -307,96 +326,492 @@ class AstParserService {
307
326
  }
308
327
  return map;
309
328
  }
310
- // ── private: symbol extraction ─────────────────────────────────────────────
329
+ // ── private: position / signature helpers ───────────────────────────────────
330
+ getPos(sourceFile, node) {
331
+ const { line, character } = sourceFile.compilerNode
332
+ .getLineAndCharacterOfPosition(node.getStart());
333
+ return { line: line + 1, col: character + 1 };
334
+ }
335
+ /** Trimmed text of the source line containing `node`'s start — the signature
336
+ * rule for every kind (name node for members, the position node for
337
+ * constructors/anonymous default classes). */
338
+ lineTextAt(sourceLines, sourceFile, node) {
339
+ const { line } = this.getPos(sourceFile, node);
340
+ return (sourceLines[line - 1] ?? node.getText().split('\n')[0]).trim();
341
+ }
342
+ // ── private: top-level symbol extraction (feeds the chunker + prompt) ──────
311
343
  extractSymbols(sourceFile, filePath, serviceName) {
312
- const symbols = [];
313
- const sig = (text, keyword) => {
314
- const found = text.split('\n').find(l => keyword.test(l));
315
- return found?.trim() ?? text.split('\n')[0].trim();
316
- };
317
- const getPos = (node) => {
318
- const { line, character } = sourceFile.compilerNode
319
- .getLineAndCharacterOfPosition(node.getStart());
320
- return { line: line + 1, col: character + 1 };
321
- };
344
+ const candidates = [];
345
+ const sourceLines = sourceFile.getFullText().split('\n');
346
+ const lineAt = (node) => this.lineTextAt(sourceLines, sourceFile, node);
322
347
  for (const cls of sourceFile.getClasses()) {
323
348
  const name = cls.getName();
324
349
  if (!name)
325
350
  continue;
326
- const nameNode = cls.getNameNode();
327
- const { line, col } = nameNode ? getPos(nameNode) : { line: cls.getStartLineNumber(), col: 1 };
328
- symbols.push({
329
- project: serviceName, filePath, name, kind: 'class',
330
- signature: sig(cls.getText(), /\bclass\s/),
331
- line, endLine: cls.getEndLineNumber(), col,
351
+ const nameNode = cls.getNameNode() ?? cls;
352
+ const { line, col } = this.getPos(sourceFile, nameNode);
353
+ candidates.push({
354
+ declNode: cls,
355
+ entry: {
356
+ project: serviceName, filePath, name, kind: 'class',
357
+ signature: lineAt(nameNode),
358
+ line, endLine: cls.getEndLineNumber(), col,
359
+ container: null, isStatic: false,
360
+ },
332
361
  });
333
362
  }
334
363
  for (const fn of sourceFile.getFunctions()) {
335
364
  const name = fn.getName();
336
365
  if (!name)
337
366
  continue;
338
- const nameNode = fn.getNameNode();
339
- const { line, col } = nameNode ? getPos(nameNode) : { line: fn.getStartLineNumber(), col: 1 };
340
- symbols.push({
341
- project: serviceName, filePath, name, kind: 'function',
342
- signature: sig(fn.getText(), /\bfunction\s/),
343
- line, endLine: fn.getEndLineNumber(), col,
367
+ const nameNode = fn.getNameNode() ?? fn;
368
+ const { line, col } = this.getPos(sourceFile, nameNode);
369
+ candidates.push({
370
+ declNode: fn,
371
+ entry: {
372
+ project: serviceName, filePath, name, kind: 'function',
373
+ signature: lineAt(nameNode),
374
+ line, endLine: fn.getEndLineNumber(), col,
375
+ container: null, isStatic: false, hasBody: fn.getBody() !== undefined,
376
+ },
344
377
  });
345
378
  }
346
379
  for (const iface of sourceFile.getInterfaces()) {
347
- const { line, col } = getPos(iface.getNameNode());
348
- symbols.push({
349
- project: serviceName, filePath, name: iface.getName(), kind: 'interface',
350
- signature: sig(iface.getText(), /\binterface\s/),
351
- line, endLine: iface.getEndLineNumber(), col,
380
+ const nameNode = iface.getNameNode();
381
+ const { line, col } = this.getPos(sourceFile, nameNode);
382
+ candidates.push({
383
+ declNode: iface,
384
+ entry: {
385
+ project: serviceName, filePath, name: iface.getName(), kind: 'interface',
386
+ signature: lineAt(nameNode),
387
+ line, endLine: iface.getEndLineNumber(), col,
388
+ container: null, isStatic: false,
389
+ },
352
390
  });
353
391
  }
354
392
  for (const ta of sourceFile.getTypeAliases()) {
355
- const { line, col } = getPos(ta.getNameNode());
356
- symbols.push({
357
- project: serviceName, filePath, name: ta.getName(), kind: 'type',
358
- signature: sig(ta.getText(), /\btype\s/),
359
- line, endLine: ta.getEndLineNumber(), col,
393
+ const nameNode = ta.getNameNode();
394
+ const { line, col } = this.getPos(sourceFile, nameNode);
395
+ candidates.push({
396
+ declNode: ta,
397
+ entry: {
398
+ project: serviceName, filePath, name: ta.getName(), kind: 'type',
399
+ signature: lineAt(nameNode),
400
+ line, endLine: ta.getEndLineNumber(), col,
401
+ container: null, isStatic: false,
402
+ },
360
403
  });
361
404
  }
362
405
  for (const stmt of sourceFile.getVariableStatements()) {
363
406
  for (const decl of stmt.getDeclarations()) {
364
- const { line, col } = getPos(decl.getNameNode());
365
- symbols.push({
366
- project: serviceName, filePath, name: decl.getName(), kind: 'variable',
367
- signature: sig(stmt.getText(), /\b(const|let|var)\s/),
368
- line, endLine: stmt.getEndLineNumber(), col,
407
+ const nameNode = decl.getNameNode();
408
+ const { line, col } = this.getPos(sourceFile, nameNode);
409
+ candidates.push({
410
+ // Owner ranges use the declarator's own span, not the statement's —
411
+ // each declarator of `const a = 1, b = () => {…}` owns only its lines.
412
+ declNode: decl,
413
+ entry: {
414
+ project: serviceName, filePath, name: decl.getName(), kind: 'variable',
415
+ signature: lineAt(nameNode),
416
+ line, endLine: stmt.getEndLineNumber(), col,
417
+ container: null, isStatic: false,
418
+ },
369
419
  });
370
420
  }
371
421
  }
372
- return symbols;
422
+ return candidates;
373
423
  }
374
- /**
375
- * Method / constructor / accessor line ranges with their enclosing class, for
376
- * owner-path resolution only. Deliberately NOT fed into `code_symbol` (methods
377
- * stay grep-based in find_symbol), so this is separate from extractSymbols.
378
- */
379
- extractOwnerRanges(sourceFile) {
380
- const ranges = [];
381
- for (const cls of sourceFile.getClasses()) {
382
- const container = cls.getName();
383
- if (!container)
424
+ // ── private: member extraction (classes anywhere, members, namespace body) ─
425
+ extractMembers(sourceFile, filePath, serviceName) {
426
+ const candidates = [];
427
+ const sourceLines = sourceFile.getFullText().split('\n');
428
+ const lineAt = (node) => this.lineTextAt(sourceLines, sourceFile, node);
429
+ const push = (declNode, posNode, name, kind, container, isStatic, hasBody) => {
430
+ const { line, col } = this.getPos(sourceFile, posNode);
431
+ candidates.push({
432
+ declNode,
433
+ entry: {
434
+ project: serviceName, filePath, name, kind,
435
+ signature: lineAt(posNode),
436
+ line, endLine: declNode.getEndLineNumber(), col,
437
+ container: container.length ? container.join('.') : null,
438
+ isStatic, hasBody,
439
+ },
440
+ });
441
+ };
442
+ // Classes anywhere in the file — nested, expression, anonymous default.
443
+ for (const cls of [
444
+ ...sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.ClassDeclaration),
445
+ ...sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.ClassExpression),
446
+ ]) {
447
+ // Named top-level classes are already handled by extractSymbols; an
448
+ // anonymous top-level class (`export default class {}`) is not — it
449
+ // must still be collected here.
450
+ if (ts_morph_1.Node.isClassDeclaration(cls) && cls.getName() && ts_morph_1.Node.isSourceFile(cls.getParent()))
384
451
  continue;
385
- const members = [
386
- ...cls.getMethods().map(n => ({ name: n.getName(), node: n })),
387
- ...cls.getGetAccessors().map(n => ({ name: n.getName(), node: n })),
388
- ...cls.getSetAccessors().map(n => ({ name: n.getName(), node: n })),
389
- ...cls.getConstructors().map(n => ({ name: 'constructor', node: n })),
390
- ];
391
- for (const { name, node } of members) {
392
- ranges.push({
393
- name, container,
394
- line: node.getStartLineNumber(),
395
- endLine: node.getEndLineNumber(),
396
- });
452
+ const described = this.describeIndexedNode(cls);
453
+ if (!described)
454
+ continue;
455
+ const posNode = (ts_morph_1.Node.isClassDeclaration(cls) ? cls.getNameNode() : undefined) ?? cls;
456
+ push(cls, posNode, described.name, 'class', described.container, false, true);
457
+ }
458
+ // Class / object-literal methods, accessors, constructors, function properties.
459
+ for (const m of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.MethodDeclaration)) {
460
+ const described = this.describeIndexedNode(m);
461
+ if (!described)
462
+ continue;
463
+ push(m, m.getNameNode(), described.name, 'method', described.container, m.isStatic(), m.getBody() !== undefined);
464
+ }
465
+ for (const a of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.GetAccessor)) {
466
+ const described = this.describeIndexedNode(a);
467
+ if (!described)
468
+ continue;
469
+ push(a, a.getNameNode(), described.name, 'get_accessor', described.container, a.isStatic(), true);
470
+ }
471
+ for (const a of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.SetAccessor)) {
472
+ const described = this.describeIndexedNode(a);
473
+ if (!described)
474
+ continue;
475
+ push(a, a.getNameNode(), described.name, 'set_accessor', described.container, a.isStatic(), true);
476
+ }
477
+ for (const c of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.Constructor)) {
478
+ const described = this.describeIndexedNode(c);
479
+ if (!described)
480
+ continue;
481
+ const kw = c.getFirstChildByKind(typescript_1.SyntaxKind.ConstructorKeyword) ?? c;
482
+ push(c, kw, described.name, 'constructor', described.container, false, c.getBody() !== undefined);
483
+ }
484
+ for (const p of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.PropertyDeclaration)) {
485
+ const described = this.describeIndexedNode(p);
486
+ if (!described)
487
+ continue;
488
+ push(p, p.getNameNode(), described.name, 'function_property', described.container, p.isStatic(), true);
489
+ }
490
+ for (const p of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.PropertyAssignment)) {
491
+ const described = this.describeIndexedNode(p);
492
+ if (!described)
493
+ continue;
494
+ push(p, p.getNameNode(), described.name, 'function_property', described.container, false, true);
495
+ }
496
+ // Interface / type-literal method and function-typed property signatures.
497
+ for (const s of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.MethodSignature)) {
498
+ const described = this.describeIndexedNode(s);
499
+ if (!described)
500
+ continue;
501
+ push(s, s.getNameNode(), described.name, 'method_signature', described.container, false, false);
502
+ }
503
+ for (const s of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.PropertySignature)) {
504
+ const described = this.describeIndexedNode(s);
505
+ if (!described)
506
+ continue;
507
+ push(s, s.getNameNode(), described.name, 'method_signature', described.container, false, false);
508
+ }
509
+ // Declarations directly in a namespace body.
510
+ for (const fn of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.FunctionDeclaration)) {
511
+ if (!ts_morph_1.Node.isModuleBlock(fn.getParent()))
512
+ continue;
513
+ const described = this.describeIndexedNode(fn);
514
+ if (!described)
515
+ continue;
516
+ const nameNode = fn.getNameNode();
517
+ if (!nameNode)
518
+ continue;
519
+ push(fn, nameNode, described.name, 'function', described.container, false, fn.getBody() !== undefined);
520
+ }
521
+ for (const iface of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.InterfaceDeclaration)) {
522
+ if (!ts_morph_1.Node.isModuleBlock(iface.getParent()))
523
+ continue;
524
+ const described = this.describeIndexedNode(iface);
525
+ if (!described)
526
+ continue;
527
+ push(iface, iface.getNameNode(), described.name, 'interface', described.container, false, true);
528
+ }
529
+ for (const ta of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.TypeAliasDeclaration)) {
530
+ if (!ts_morph_1.Node.isModuleBlock(ta.getParent()))
531
+ continue;
532
+ const described = this.describeIndexedNode(ta);
533
+ if (!described)
534
+ continue;
535
+ push(ta, ta.getNameNode(), described.name, 'type', described.container, false, true);
536
+ }
537
+ for (const stmt of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.VariableStatement)) {
538
+ if (!ts_morph_1.Node.isModuleBlock(stmt.getParent()))
539
+ continue;
540
+ for (const decl of stmt.getDeclarations()) {
541
+ const described = this.describeIndexedNode(decl);
542
+ if (!described)
543
+ continue;
544
+ push(decl, decl.getNameNode(), described.name, 'variable', described.container, false, true);
397
545
  }
398
546
  }
399
- return ranges;
547
+ return candidates;
548
+ }
549
+ /** Chunk-ownership ranges from every indexed declaration (top-level symbols +
550
+ * members) except `method_signature`. Each range spans the declaration node's
551
+ * own first-to-last line — decorator-inclusive, comment-exclusive, and (for a
552
+ * variable) the declarator alone, never the whole statement. */
553
+ buildOwnerRanges(candidates) {
554
+ return candidates
555
+ .filter(c => c.entry.kind !== 'method_signature')
556
+ .map(c => ({
557
+ name: c.entry.name,
558
+ container: c.entry.container,
559
+ line: c.declNode.getStartLineNumber(),
560
+ endLine: c.declNode.getEndLineNumber(),
561
+ }));
562
+ }
563
+ // ── private: shared container resolution ────────────────────────────────────
564
+ static isValidSegment(s) {
565
+ return VALID_SEGMENT.test(s);
566
+ }
567
+ static memberName(nameNode) {
568
+ if (!nameNode)
569
+ return null;
570
+ if (ts_morph_1.Node.isPrivateIdentifier(nameNode))
571
+ return nameNode.getText();
572
+ if (ts_morph_1.Node.isIdentifier(nameNode))
573
+ return nameNode.getText();
574
+ if (ts_morph_1.Node.isStringLiteral(nameNode))
575
+ return nameNode.getLiteralValue();
576
+ if (ts_morph_1.Node.isNumericLiteral(nameNode))
577
+ return nameNode.getText();
578
+ if (ts_morph_1.Node.isComputedPropertyName(nameNode))
579
+ return nameNode.getText().replace(/\s+/g, '');
580
+ return null;
581
+ }
582
+ static unwrapExpr(node) {
583
+ let current = node;
584
+ while (ts_morph_1.Node.isParenthesizedExpression(current) || ts_morph_1.Node.isAsExpression(current) || ts_morph_1.Node.isSatisfiesExpression(current)) {
585
+ current = current.getExpression();
586
+ }
587
+ return current;
588
+ }
589
+ static unwrapParent(node) {
590
+ let current = node.getParent();
591
+ while (current && (ts_morph_1.Node.isParenthesizedExpression(current) || ts_morph_1.Node.isAsExpression(current) || ts_morph_1.Node.isSatisfiesExpression(current))) {
592
+ current = current.getParent();
593
+ }
594
+ return current;
595
+ }
596
+ isModuleOrNamespaceLevelVariable(varDecl) {
597
+ // VariableDeclaration -> VariableDeclarationList -> VariableStatement -> (SourceFile | ModuleBlock)
598
+ const stmt = varDecl.getParent()?.getParent();
599
+ const stmtParent = stmt?.getParent();
600
+ return ts_morph_1.Node.isSourceFile(stmtParent) || ts_morph_1.Node.isModuleBlock(stmtParent);
601
+ }
602
+ classExpressionRoot(classExpr) {
603
+ const parent = AstParserService.unwrapParent(classExpr);
604
+ if (!parent)
605
+ return null;
606
+ if (ts_morph_1.Node.isVariableDeclaration(parent)) {
607
+ const name = parent.getName();
608
+ return name ? { name, namingParent: parent } : null;
609
+ }
610
+ if (ts_morph_1.Node.isExportAssignment(parent) && !parent.isExportEquals()) {
611
+ return { name: 'default', namingParent: parent };
612
+ }
613
+ return null;
614
+ }
615
+ objectLiteralRootValid(obj) {
616
+ const parent = AstParserService.unwrapParent(obj);
617
+ if (!parent)
618
+ return false;
619
+ if (ts_morph_1.Node.isPropertyAssignment(parent))
620
+ return true;
621
+ if (ts_morph_1.Node.isVariableDeclaration(parent))
622
+ return this.isModuleOrNamespaceLevelVariable(parent);
623
+ if (ts_morph_1.Node.isExportAssignment(parent) && !parent.isExportEquals())
624
+ return true;
625
+ return false;
626
+ }
627
+ isTopLevelOrNamespaceDirect(node) {
628
+ return ts_morph_1.Node.isSourceFile(node) || ts_morph_1.Node.isModuleBlock(node);
629
+ }
630
+ /** One step of the ancestor walk used by `containerSegments`: does `node`
631
+ * contribute a named segment, pass through transparently, or disqualify
632
+ * (exclude) everything nested inside it? */
633
+ walkStep(node) {
634
+ if (ts_morph_1.Node.isModuleDeclaration(node)) {
635
+ if (node.compilerNode.flags & typescript_1.NodeFlags.GlobalAugmentation)
636
+ return { kind: 'exclude' };
637
+ const nameNode = node.getNameNode();
638
+ if (ts_morph_1.Node.isStringLiteral(nameNode))
639
+ return { kind: 'exclude' };
640
+ return { kind: 'segment', text: nameNode.getText() };
641
+ }
642
+ if (ts_morph_1.Node.isClassDeclaration(node)) {
643
+ const name = node.getName();
644
+ if (name)
645
+ return { kind: 'segment', text: name };
646
+ if (node.isDefaultExport())
647
+ return { kind: 'segment', text: 'default' };
648
+ return { kind: 'exclude' };
649
+ }
650
+ if (ts_morph_1.Node.isClassExpression(node)) {
651
+ const root = this.classExpressionRoot(node);
652
+ if (!root)
653
+ return { kind: 'exclude' };
654
+ return { kind: 'segment', text: root.name, skipTo: root.namingParent };
655
+ }
656
+ if (ts_morph_1.Node.isFunctionDeclaration(node)) {
657
+ const name = node.getName();
658
+ return name ? { kind: 'segment', text: name } : { kind: 'transparent' };
659
+ }
660
+ if (ts_morph_1.Node.isMethodDeclaration(node) || ts_morph_1.Node.isGetAccessorDeclaration(node) || ts_morph_1.Node.isSetAccessorDeclaration(node)) {
661
+ const name = AstParserService.memberName(node.getNameNode());
662
+ return name !== null ? { kind: 'segment', text: name } : { kind: 'exclude' };
663
+ }
664
+ if (ts_morph_1.Node.isConstructorDeclaration(node)) {
665
+ return { kind: 'segment', text: 'constructor' };
666
+ }
667
+ if (ts_morph_1.Node.isPropertyDeclaration(node) || ts_morph_1.Node.isPropertyAssignment(node)) {
668
+ const name = AstParserService.memberName(node.getNameNode());
669
+ return name !== null ? { kind: 'segment', text: name } : { kind: 'exclude' };
670
+ }
671
+ if (ts_morph_1.Node.isInterfaceDeclaration(node)) {
672
+ return { kind: 'segment', text: node.getName() };
673
+ }
674
+ if (ts_morph_1.Node.isTypeAliasDeclaration(node)) {
675
+ return { kind: 'segment', text: node.getName() };
676
+ }
677
+ if (ts_morph_1.Node.isVariableDeclaration(node)) {
678
+ return { kind: 'segment', text: node.getName() };
679
+ }
680
+ if (ts_morph_1.Node.isObjectLiteralExpression(node)) {
681
+ return this.objectLiteralRootValid(node) ? { kind: 'transparent' } : { kind: 'exclude' };
682
+ }
683
+ return { kind: 'transparent' };
684
+ }
685
+ /** The dotted container path of `node`'s enclosing named scopes, outermost
686
+ * first, or null when any contributing ancestor is invalid (a computed /
687
+ * string-literal segment, a string-named module, `declare global`, or an
688
+ * unrooted class expression / object literal). */
689
+ containerSegments(node) {
690
+ const segments = [];
691
+ let current = node.getParent();
692
+ while (current) {
693
+ const step = this.walkStep(current);
694
+ if (step.kind === 'exclude')
695
+ return null;
696
+ if (step.kind === 'segment') {
697
+ if (!AstParserService.isValidSegment(step.text))
698
+ return null;
699
+ segments.unshift(step.text);
700
+ current = step.skipTo ? step.skipTo.getParent() : current.getParent();
701
+ continue;
702
+ }
703
+ current = current.getParent();
704
+ }
705
+ return segments;
706
+ }
707
+ /** If `node` is a declaration this service indexes (top-level symbol, member,
708
+ * or collected class), its own name + container. Otherwise null. Shared by
709
+ * member extraction, ownership ranges and the call graph so all three name a
710
+ * declaration identically. */
711
+ describeIndexedNode(node) {
712
+ if (ts_morph_1.Node.isClassDeclaration(node)) {
713
+ const name = node.getName() ?? (node.isDefaultExport() ? 'default' : null);
714
+ if (name === null)
715
+ return null;
716
+ const container = this.containerSegments(node);
717
+ return container === null ? null : { name, container };
718
+ }
719
+ if (ts_morph_1.Node.isClassExpression(node)) {
720
+ const root = this.classExpressionRoot(node);
721
+ if (!root)
722
+ return null;
723
+ const container = this.containerSegments(root.namingParent);
724
+ return container === null ? null : { name: root.name, container };
725
+ }
726
+ if (ts_morph_1.Node.isFunctionDeclaration(node)) {
727
+ const name = node.getName();
728
+ if (!name || !this.isTopLevelOrNamespaceDirect(node.getParent()))
729
+ return null;
730
+ const container = this.containerSegments(node);
731
+ return container === null ? null : { name, container };
732
+ }
733
+ if (ts_morph_1.Node.isMethodDeclaration(node) || ts_morph_1.Node.isGetAccessorDeclaration(node) || ts_morph_1.Node.isSetAccessorDeclaration(node)) {
734
+ const name = AstParserService.memberName(node.getNameNode());
735
+ if (name === null)
736
+ return null;
737
+ const container = this.containerSegments(node);
738
+ return container === null ? null : { name, container };
739
+ }
740
+ if (ts_morph_1.Node.isConstructorDeclaration(node)) {
741
+ const container = this.containerSegments(node);
742
+ return container === null ? null : { name: 'constructor', container };
743
+ }
744
+ if (ts_morph_1.Node.isPropertyDeclaration(node) || ts_morph_1.Node.isPropertyAssignment(node)) {
745
+ const init = node.getInitializer();
746
+ if (!init)
747
+ return null;
748
+ const unwrapped = AstParserService.unwrapExpr(init);
749
+ if (!(ts_morph_1.Node.isArrowFunction(unwrapped) || ts_morph_1.Node.isFunctionExpression(unwrapped)))
750
+ return null;
751
+ const name = AstParserService.memberName(node.getNameNode());
752
+ if (name === null)
753
+ return null;
754
+ const container = this.containerSegments(node);
755
+ return container === null ? null : { name, container };
756
+ }
757
+ if (ts_morph_1.Node.isInterfaceDeclaration(node)) {
758
+ if (!this.isTopLevelOrNamespaceDirect(node.getParent()))
759
+ return null;
760
+ const container = this.containerSegments(node);
761
+ return container === null ? null : { name: node.getName(), container };
762
+ }
763
+ if (ts_morph_1.Node.isTypeAliasDeclaration(node)) {
764
+ if (!this.isTopLevelOrNamespaceDirect(node.getParent()))
765
+ return null;
766
+ const container = this.containerSegments(node);
767
+ return container === null ? null : { name: node.getName(), container };
768
+ }
769
+ if (ts_morph_1.Node.isVariableDeclaration(node)) {
770
+ if (!this.isModuleOrNamespaceLevelVariable(node))
771
+ return null;
772
+ const container = this.containerSegments(node);
773
+ return container === null ? null : { name: node.getName(), container };
774
+ }
775
+ if (ts_morph_1.Node.isMethodSignature(node) || ts_morph_1.Node.isPropertySignature(node)) {
776
+ if (ts_morph_1.Node.isPropertySignature(node)) {
777
+ const typeNode = node.getTypeNode();
778
+ if (!typeNode || !ts_morph_1.Node.isFunctionTypeNode(typeNode))
779
+ return null;
780
+ }
781
+ const parent = node.getParent();
782
+ let validParent = false;
783
+ if (ts_morph_1.Node.isInterfaceDeclaration(parent)) {
784
+ validParent = true;
785
+ }
786
+ else if (ts_morph_1.Node.isTypeLiteral(parent)) {
787
+ const aliasParent = parent.getParent();
788
+ validParent = ts_morph_1.Node.isTypeAliasDeclaration(aliasParent) && aliasParent.getTypeNode() === parent;
789
+ }
790
+ if (!validParent)
791
+ return null;
792
+ const name = AstParserService.memberName(node.getNameNode());
793
+ if (name === null)
794
+ return null;
795
+ const container = this.containerSegments(node);
796
+ return container === null ? null : { name, container };
797
+ }
798
+ return null;
799
+ }
800
+ qualifiedName(described) {
801
+ return described.container.length ? `${described.container.join('.')}.${described.name}` : described.name;
802
+ }
803
+ /** The qualified name of the nearest ancestor of `node` that is itself an
804
+ * indexed declaration (method, accessor, constructor, function property,
805
+ * namespace/top-level function, or module-/namespace-level variable). */
806
+ findFromSymbol(node) {
807
+ let current = node.getParent();
808
+ while (current) {
809
+ const described = this.describeIndexedNode(current);
810
+ if (described)
811
+ return this.qualifiedName(described);
812
+ current = current.getParent();
813
+ }
814
+ return null;
400
815
  }
401
816
  // ── private: call graph extraction ────────────────────────────────────────
402
817
  extractCalls(sourceFile, filePath, serviceName) {
@@ -405,7 +820,7 @@ class AstParserService {
405
820
  const expr = callExpr.getExpression();
406
821
  let calleeName = null;
407
822
  let calleeFile = null;
408
- let calleeSymbol = null;
823
+ let calleeDecl;
409
824
  try {
410
825
  if (ts_morph_1.Node.isPropertyAccessExpression(expr)) {
411
826
  calleeName = expr.getName();
@@ -414,7 +829,7 @@ class AstParserService {
414
829
  const declPath = decl?.getSourceFile().getFilePath();
415
830
  if (declPath && !declPath.includes('/node_modules/')) {
416
831
  calleeFile = declPath;
417
- calleeSymbol = calleeName;
832
+ calleeDecl = decl;
418
833
  }
419
834
  }
420
835
  else if (ts_morph_1.Node.isIdentifier(expr)) {
@@ -430,7 +845,7 @@ class AstParserService {
430
845
  const declPath = decl?.getSourceFile().getFilePath();
431
846
  if (declPath && !declPath.includes('/node_modules/')) {
432
847
  calleeFile = declPath;
433
- calleeSymbol = calleeName;
848
+ calleeDecl = decl;
434
849
  }
435
850
  }
436
851
  }
@@ -440,7 +855,9 @@ class AstParserService {
440
855
  }
441
856
  if (!calleeName || !calleeFile)
442
857
  continue;
443
- const fromSymbol = this.getContainerName(callExpr);
858
+ const described = calleeDecl ? this.describeIndexedNode(calleeDecl) : null;
859
+ const calleeSymbol = described ? this.qualifiedName(described) : null;
860
+ const fromSymbol = this.findFromSymbol(callExpr);
444
861
  const pos = sourceFile.compilerNode
445
862
  .getLineAndCharacterOfPosition(callExpr.getStart());
446
863
  calls.push({
@@ -456,22 +873,5 @@ class AstParserService {
456
873
  }
457
874
  return calls;
458
875
  }
459
- getContainerName(node) {
460
- let current = node.getParent();
461
- while (current) {
462
- if (ts_morph_1.Node.isMethodDeclaration(current) || ts_morph_1.Node.isFunctionDeclaration(current)) {
463
- return current.getName() ?? null;
464
- }
465
- if (ts_morph_1.Node.isArrowFunction(current) || ts_morph_1.Node.isFunctionExpression(current)) {
466
- const parent = current.getParent();
467
- if (ts_morph_1.Node.isVariableDeclaration(parent))
468
- return parent.getName();
469
- if (ts_morph_1.Node.isPropertyDeclaration(parent))
470
- return parent.getName();
471
- }
472
- current = current.getParent();
473
- }
474
- return null;
475
- }
476
876
  }
477
877
  exports.AstParserService = AstParserService;