codesearch-mcp 0.2.1-alpha → 0.2.3-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 (62) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +15 -8
  3. package/dist/app.js +45 -7
  4. package/dist/src/commands/EvalCommand.d.ts +10 -0
  5. package/dist/src/commands/EvalCommand.js +231 -0
  6. package/dist/src/commands/IndexCommand.js +79 -53
  7. package/dist/src/db/index.d.ts +1 -1
  8. package/dist/src/db/index.js +1 -2
  9. package/dist/src/db/schema.d.ts +5 -29
  10. package/dist/src/db/schema.js +2 -8
  11. package/dist/src/dependency-traversal.d.ts +16 -0
  12. package/dist/src/dependency-traversal.js +45 -0
  13. package/dist/src/enriched-prompt.d.ts +3 -2
  14. package/dist/src/enriched-prompt.js +3 -2
  15. package/dist/src/eval-results.d.ts +12 -0
  16. package/dist/src/eval-results.js +114 -0
  17. package/dist/src/file-utils.d.ts +15 -0
  18. package/dist/src/file-utils.js +24 -1
  19. package/dist/src/help.js +4 -1
  20. package/dist/src/interfaces/IAstParser.d.ts +13 -1
  21. package/dist/src/interfaces/IDatabase.d.ts +0 -14
  22. package/dist/src/interfaces/IRetrievalEvaluator.d.ts +94 -0
  23. package/dist/src/interfaces/IRetrievalEvaluator.js +2 -0
  24. package/dist/src/interfaces/ISearcher.d.ts +11 -16
  25. package/dist/src/interfaces/index.d.ts +1 -0
  26. package/dist/src/project-scope.d.ts +19 -0
  27. package/dist/src/project-scope.js +49 -0
  28. package/dist/src/project-selector.d.ts +6 -0
  29. package/dist/src/project-selector.js +18 -0
  30. package/dist/src/retrieval-eval.d.ts +16 -0
  31. package/dist/src/retrieval-eval.js +214 -0
  32. package/dist/src/services/AstParserService.d.ts +66 -1
  33. package/dist/src/services/AstParserService.js +432 -28
  34. package/dist/src/services/DatabaseService.d.ts +0 -12
  35. package/dist/src/services/DatabaseService.js +2 -52
  36. package/dist/src/services/IndexerService.d.ts +8 -0
  37. package/dist/src/services/IndexerService.js +24 -11
  38. package/dist/src/services/McpServer.d.ts +7 -0
  39. package/dist/src/services/McpServer.js +42 -38
  40. package/dist/src/services/OpenAICompatibleEmbedder.js +2 -1
  41. package/dist/src/services/SearcherService.d.ts +189 -25
  42. package/dist/src/services/SearcherService.js +505 -203
  43. package/dist/src/symbol-rows.d.ts +14 -9
  44. package/dist/src/symbol-rows.js +17 -11
  45. package/dist/src/tools/FindSymbolTool.js +14 -5
  46. package/dist/src/tools/GetCallersTool.d.ts +3 -1
  47. package/dist/src/tools/GetCallersTool.js +16 -6
  48. package/dist/src/tools/GetDependenciesTool.d.ts +3 -1
  49. package/dist/src/tools/GetDependenciesTool.js +15 -8
  50. package/dist/src/tools/GetDependentsTool.d.ts +3 -1
  51. package/dist/src/tools/GetDependentsTool.js +31 -16
  52. package/dist/src/tools/ListFilesTool.d.ts +3 -1
  53. package/dist/src/tools/ListFilesTool.js +14 -12
  54. package/dist/src/tools/ListProjectsTool.js +5 -1
  55. package/dist/src/tools/ReadFileRangeTool.d.ts +3 -1
  56. package/dist/src/tools/ReadFileRangeTool.js +12 -11
  57. package/dist/src/tools/ReadFileTool.d.ts +3 -1
  58. package/dist/src/tools/ReadFileTool.js +12 -10
  59. package/dist/src/tools/SearchCodebaseTool.js +3 -1
  60. package/dist/src/tools/format-helpers.d.ts +16 -1
  61. package/dist/src/tools/format-helpers.js +64 -0
  62. package/package.json +9 -8
@@ -36,9 +36,14 @@ 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");
40
39
  const ts_morph_1 = require("ts-morph");
41
40
  const LoggerService_1 = require("./LoggerService");
41
+ const GRAPHQL_MODULE = '@nestjs/graphql';
42
+ const GRAPHQL_TYPE_DECORATORS = new Set(['ObjectType', 'InputType', 'InterfaceType']);
43
+ const GRAPHQL_OPERATION_KIND = {
44
+ Query: 'graphql_query', Mutation: 'graphql_mutation', Subscription: 'graphql_subscription',
45
+ };
46
+ const GRAPHQL_NAME_RE = /^[_A-Za-z][_0-9A-Za-z]*$/;
42
47
  const FALLBACK_OPTS = {
43
48
  skipAddingFilesFromTsConfig: true,
44
49
  skipFileDependencyResolution: false,
@@ -48,9 +53,11 @@ const MORPH_CACHE_MAX = 10;
48
53
  const VALID_SEGMENT = /^#?[\w$]+$/;
49
54
  class AstParserService {
50
55
  logger;
56
+ deps;
51
57
  morphProjects = new Map();
52
- constructor(logger = LoggerService_1.LoggerService.silent()) {
58
+ constructor(logger = LoggerService_1.LoggerService.silent(), deps = {}) {
53
59
  this.logger = logger;
60
+ this.deps = deps;
54
61
  }
55
62
  // ── public interface ────────────────────────────────────────────────────────
56
63
  async analyzeProject(filePaths, serviceName, projectRoot = process.cwd(), opts = {}) {
@@ -62,7 +69,7 @@ class AstParserService {
62
69
  const morphProject = this.createMorphProject(projectRoot, skipDependencyResolution, rootTsConfigPath);
63
70
  for (const filePath of filePaths) {
64
71
  if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
65
- analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [] });
72
+ analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] });
66
73
  continue;
67
74
  }
68
75
  try {
@@ -70,7 +77,7 @@ class AstParserService {
70
77
  }
71
78
  catch (err) {
72
79
  this.logger.warn('AstParser: failed to add source file', { filePath, reason: err.message });
73
- analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [] });
80
+ analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] });
74
81
  }
75
82
  }
76
83
  for (const sourceFile of morphProject.getSourceFiles()) {
@@ -102,7 +109,7 @@ class AstParserService {
102
109
  this.logger.debug('AstParser: analyzeFile start', { filePath, serviceName, projectRoot });
103
110
  if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
104
111
  this.logger.debug('AstParser: analyzeFile skip (non-TS)', { filePath, ms: Date.now() - t0 });
105
- return { edges: [], symbols: [], calls: [], members: [] };
112
+ return { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] };
106
113
  }
107
114
  const morphProject = this.getMorphProject(projectRoot);
108
115
  const existing = morphProject.getSourceFile(filePath);
@@ -114,7 +121,7 @@ class AstParserService {
114
121
  }
115
122
  catch (err) {
116
123
  this.logger.warn('AstParser: analyzeFile failed to add source file', { filePath, reason: err.message, ms: Date.now() - t0 });
117
- return { edges: [], symbols: [], calls: [], members: [] };
124
+ return { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] };
118
125
  }
119
126
  const result = this.analyzeSourceFile(sourceFile, filePath, serviceName);
120
127
  morphProject.removeSourceFile(sourceFile);
@@ -149,11 +156,24 @@ class AstParserService {
149
156
  const members = memberCandidates.map(c => c.entry);
150
157
  const calls = this.extractCalls(sourceFile, filePath, serviceName);
151
158
  const ownerRanges = this.buildOwnerRanges([...symbolCandidates, ...memberCandidates]);
152
- return { edges, symbols, members, calls, ownerRanges };
159
+ // GraphQL-named rows are isolated like members: a bug here must never
160
+ // change a file's symbols, members, chunks, ownerPath or call graph.
161
+ // Runs after member extraction (decision 6 of `graphql-symbols`) — a
162
+ // GraphQL row is written only when the decorated declaration's own row
163
+ // was written to code_symbol.
164
+ let graphqlNames = [];
165
+ try {
166
+ graphqlNames = this.extractGraphqlNames(sourceFile, filePath, serviceName, [...symbolCandidates, ...memberCandidates]);
167
+ }
168
+ catch (err) {
169
+ this.logger.warn('AstParser: GraphQL extraction failed', { filePath, reason: err.message });
170
+ graphqlNames = [];
171
+ }
172
+ return { edges, symbols, members, calls, ownerRanges, graphqlNames };
153
173
  }
154
174
  catch (err) {
155
175
  this.logger.warn('AstParser: failed to analyze file', { filePath, reason: err.message });
156
- return { edges: [], symbols: [], calls: [], members: [] };
176
+ return { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] };
157
177
  }
158
178
  }
159
179
  // ── private: MorphProject cache (LRU, max MORPH_CACHE_MAX entries) ──────────
@@ -231,8 +251,19 @@ class AstParserService {
231
251
  const edges = [];
232
252
  const usageMap = this.buildUsageMap(sourceFile);
233
253
  for (const decl of sourceFile.getImportDeclarations()) {
254
+ const namedImports = decl.getNamedImports();
255
+ const defaultImport = decl.getDefaultImport();
256
+ const namespaceImport = decl.getNamespaceImport();
257
+ const hasClause = Boolean(defaultImport || namespaceImport || namedImports.length > 0);
234
258
  const resolved = decl.getModuleSpecifierSourceFile();
235
- if (!resolved) {
259
+ let toFile = resolved ? resolved.getFilePath() : null;
260
+ // A bindingless specifier never resolves through ts-morph's bound symbol
261
+ // (nothing is bound), even for a real project file — fall back to
262
+ // TypeScript's own resolver and map the result back onto the project.
263
+ if (!toFile && !hasClause) {
264
+ toFile = this.resolveSideEffectModuleSpecifier(decl, sourceFile, filePath);
265
+ }
266
+ if (!toFile) {
236
267
  const specifier = decl.getModuleSpecifierValue();
237
268
  const isCodeImport = !/\.(json|css|scss|less|svg|png|jpg|gif|ico|woff2?|ttf|eot)(\?.*)?$/.test(specifier);
238
269
  if (specifier.startsWith('.') && isCodeImport) {
@@ -240,34 +271,37 @@ class AstParserService {
240
271
  }
241
272
  continue;
242
273
  }
243
- const toFile = resolved.getFilePath();
244
274
  if (toFile.includes('/node_modules/'))
245
275
  continue;
246
- const namedImports = decl.getNamedImports();
247
- const defaultImport = decl.getDefaultImport();
248
- const namespaceImport = decl.getNamespaceImport();
249
- if (namedImports.length > 0) {
250
- for (const named of namedImports) {
251
- const localName = named.getAliasNode()?.getText() ?? named.getName();
252
- const fromSymbol = this.resolveFromSymbol(localName, usageMap);
253
- edges.push({
254
- project: serviceName, fromFile: filePath, fromSymbol,
255
- toFile, toSymbol: named.getName(), depType: 'import',
256
- });
257
- }
258
- }
259
- else if (defaultImport) {
276
+ if (!hasClause) {
260
277
  edges.push({
261
278
  project: serviceName, fromFile: filePath, fromSymbol: null,
279
+ toFile, toSymbol: null, depType: 'import-side-effect',
280
+ });
281
+ continue;
282
+ }
283
+ if (defaultImport) {
284
+ const fromSymbol = this.resolveFromSymbol(defaultImport.getText(), usageMap);
285
+ edges.push({
286
+ project: serviceName, fromFile: filePath, fromSymbol,
262
287
  toFile, toSymbol: null, depType: 'import-default',
263
288
  });
264
289
  }
265
- else if (namespaceImport) {
290
+ if (namespaceImport) {
291
+ const fromSymbol = this.resolveFromSymbol(namespaceImport.getText(), usageMap);
266
292
  edges.push({
267
- project: serviceName, fromFile: filePath, fromSymbol: null,
293
+ project: serviceName, fromFile: filePath, fromSymbol,
268
294
  toFile, toSymbol: null, depType: 'import-namespace',
269
295
  });
270
296
  }
297
+ for (const named of namedImports) {
298
+ const localName = named.getAliasNode()?.getText() ?? named.getName();
299
+ const fromSymbol = this.resolveFromSymbol(localName, usageMap);
300
+ edges.push({
301
+ project: serviceName, fromFile: filePath, fromSymbol,
302
+ toFile, toSymbol: named.getName(), depType: 'import',
303
+ });
304
+ }
271
305
  }
272
306
  for (const decl of sourceFile.getExportDeclarations()) {
273
307
  const resolved = decl.getModuleSpecifierSourceFile();
@@ -294,6 +328,18 @@ class AstParserService {
294
328
  }
295
329
  return edges;
296
330
  }
331
+ /** Manual fallback for a bindingless import specifier (design.md decision 2) —
332
+ * ts-morph's `getModuleSpecifierSourceFile()` is `null` for these even when
333
+ * the target is a real project file, since nothing is bound to resolve through. */
334
+ resolveSideEffectModuleSpecifier(decl, sourceFile, filePath) {
335
+ const specifier = decl.getModuleSpecifierValue();
336
+ const compilerOptions = sourceFile.getProject().getCompilerOptions();
337
+ const result = ts_morph_1.ts.resolveModuleName(specifier, filePath, compilerOptions, ts_morph_1.ts.sys);
338
+ const resolvedFileName = result.resolvedModule?.resolvedFileName;
339
+ if (!resolvedFileName)
340
+ return null;
341
+ return sourceFile.getProject().getSourceFile(resolvedFileName)?.getFilePath() ?? null;
342
+ }
297
343
  resolveFromSymbol(localName, usageMap) {
298
344
  const users = usageMap.get(localName);
299
345
  return users?.length === 1 ? users[0] : null;
@@ -478,7 +524,7 @@ class AstParserService {
478
524
  const described = this.describeIndexedNode(c);
479
525
  if (!described)
480
526
  continue;
481
- const kw = c.getFirstChildByKind(typescript_1.SyntaxKind.ConstructorKeyword) ?? c;
527
+ const kw = c.getFirstChildByKind(ts_morph_1.SyntaxKind.ConstructorKeyword) ?? c;
482
528
  push(c, kw, described.name, 'constructor', described.container, false, c.getBody() !== undefined);
483
529
  }
484
530
  for (const p of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.PropertyDeclaration)) {
@@ -560,6 +606,364 @@ class AstParserService {
560
606
  endLine: c.declNode.getEndLineNumber(),
561
607
  }));
562
608
  }
609
+ // ── private: GraphQL-named rows (graphql-symbols) ───────────────────────────
610
+ /** Local name → the name exported by `@nestjs/graphql`, for every named import
611
+ * of that module specifier (aliases followed). Recognition is import-based,
612
+ * not type-checker-based, so `skipFileDependencyResolution` batches agree. */
613
+ buildGraphqlImportMap(sourceFile) {
614
+ const map = new Map();
615
+ for (const decl of sourceFile.getImportDeclarations()) {
616
+ if (decl.getModuleSpecifierValue() !== GRAPHQL_MODULE)
617
+ continue;
618
+ for (const named of decl.getNamedImports()) {
619
+ const exportedName = named.getName();
620
+ const localName = named.getAliasNode()?.getText() ?? exportedName;
621
+ map.set(localName, exportedName);
622
+ }
623
+ }
624
+ return map;
625
+ }
626
+ /** The identifier naming a decorator's expression (`@Foo` or the callee of `@Foo(...)`). */
627
+ static decoratorIdentifier(dec) {
628
+ const expr = dec.getExpression();
629
+ if (ts_morph_1.Node.isCallExpression(expr)) {
630
+ const callee = expr.getExpression();
631
+ return ts_morph_1.Node.isIdentifier(callee) ? callee : undefined;
632
+ }
633
+ return ts_morph_1.Node.isIdentifier(expr) ? expr : undefined;
634
+ }
635
+ /** `dec` recognised against `importMap` — its exported `@nestjs/graphql` name and its call arguments (`[]` for a bare `@Foo`). */
636
+ recognizedDecorator(dec, importMap) {
637
+ const idNode = AstParserService.decoratorIdentifier(dec);
638
+ if (!idNode)
639
+ return null;
640
+ const exported = importMap.get(idNode.getText());
641
+ if (!exported)
642
+ return null;
643
+ const expr = dec.getExpression();
644
+ const args = ts_morph_1.Node.isCallExpression(expr) ? expr.getArguments() : [];
645
+ return { name: exported, args };
646
+ }
647
+ static literalStringValue(node) {
648
+ if (ts_morph_1.Node.isStringLiteral(node) || ts_morph_1.Node.isNoSubstitutionTemplateLiteral(node))
649
+ return node.getLiteralValue();
650
+ return null;
651
+ }
652
+ static checkGraphqlGrammar(name) {
653
+ return GRAPHQL_NAME_RE.test(name) ? { ok: true, name } : { ok: false, reason: 'invalid-name', value: name };
654
+ }
655
+ /** `true` when any decorator argument is an (unwrapped) options object
656
+ * literal with `isAbstract: true` — an abstract base type (`PaginatedType`
657
+ * pattern) that NestJS never registers in the actual GraphQL schema. */
658
+ static isAbstractGraphqlType(args) {
659
+ return args.some((arg) => {
660
+ if (!ts_morph_1.Node.isObjectLiteralExpression(arg))
661
+ return false;
662
+ const prop = arg.getProperties().find((p) => {
663
+ if (!ts_morph_1.Node.isPropertyAssignment(p))
664
+ return false;
665
+ const nameNode = p.getNameNode();
666
+ if (ts_morph_1.Node.isIdentifier(nameNode))
667
+ return nameNode.getText() === 'isAbstract';
668
+ if (ts_morph_1.Node.isStringLiteral(nameNode))
669
+ return nameNode.getLiteralValue() === 'isAbstract';
670
+ return false;
671
+ });
672
+ const init = prop?.getInitializer();
673
+ return init !== undefined && ts_morph_1.Node.isTrueLiteral(init);
674
+ });
675
+ }
676
+ /** `obj`'s own `name` property (`PropertyAssignment` or `ShorthandPropertyAssignment`), if any. */
677
+ static findNameProperty(obj) {
678
+ return obj.getProperties().find((p) => {
679
+ if (!ts_morph_1.Node.isPropertyAssignment(p) && !ts_morph_1.Node.isShorthandPropertyAssignment(p))
680
+ return false;
681
+ const nameNode = p.getNameNode();
682
+ if (ts_morph_1.Node.isIdentifier(nameNode))
683
+ return nameNode.getText() === 'name';
684
+ if (ts_morph_1.Node.isStringLiteral(nameNode))
685
+ return nameNode.getLiteralValue() === 'name';
686
+ return false;
687
+ });
688
+ }
689
+ /** Strips parens/`as`/`satisfies`/non-null wrappers (any nesting) to find the
690
+ * underlying node — used only to classify an argument's *shape* (type
691
+ * function vs. options object vs. literal vs. other), never to read a name
692
+ * value through the wrapper (decision 4 of `graphql-symbols` forbids that). */
693
+ static peekThroughWrapper(node) {
694
+ let current = node;
695
+ let wrapped = false;
696
+ while (true) {
697
+ if (ts_morph_1.Node.isParenthesizedExpression(current) || ts_morph_1.Node.isAsExpression(current) || ts_morph_1.Node.isSatisfiesExpression(current) || ts_morph_1.Node.isNonNullExpression(current)) {
698
+ current = current.getExpression();
699
+ wrapped = true;
700
+ continue;
701
+ }
702
+ break;
703
+ }
704
+ return { inner: current, wrapped };
705
+ }
706
+ /**
707
+ * GraphQL name from a decorator's arguments (design decision 4 of
708
+ * `graphql-symbols`) — syntactic, no wrapper looked through at any step.
709
+ * `allowOptionsName` gates step 2 (the `name` option — `Query`/`Mutation`/
710
+ * `Subscription`/`ResolveField` only); `fallbackName` is the TS name used at
711
+ * step 3.
712
+ */
713
+ deriveGraphqlName(args, allowOptionsName, fallbackName) {
714
+ if (args.length > 0) {
715
+ const first = args[0];
716
+ const { inner, wrapped } = AstParserService.peekThroughWrapper(first);
717
+ const literal = AstParserService.literalStringValue(inner);
718
+ if (literal !== null) {
719
+ // A wrapped literal is still "a wrapped ... argument", an anything-else
720
+ // node (decision 4) — reading its value through the wrapper is exactly
721
+ // what "no wrapper is looked through" forbids.
722
+ if (wrapped)
723
+ return { ok: false, reason: 'non-literal-name' };
724
+ return AstParserService.checkGraphqlGrammar(literal);
725
+ }
726
+ const isTypeFnOrOptions = ts_morph_1.Node.isArrowFunction(inner) || ts_morph_1.Node.isFunctionExpression(inner) || ts_morph_1.Node.isObjectLiteralExpression(inner);
727
+ if (!isTypeFnOrOptions)
728
+ return { ok: false, reason: 'non-literal-name' };
729
+ // Type function / options object, wrapped or not — this is a routing
730
+ // decision only (go to step 2/3); nothing is read from it here, so its
731
+ // wrapper doesn't matter yet.
732
+ }
733
+ if (allowOptionsName) {
734
+ for (const arg of args) {
735
+ const { inner, wrapped } = AstParserService.peekThroughWrapper(arg);
736
+ if (!ts_morph_1.Node.isObjectLiteralExpression(inner))
737
+ continue;
738
+ // A wrapped options object cannot be trusted without looking through
739
+ // the wrapper, which decision 4 forbids — treat it as non-literal
740
+ // rather than silently skipping to the next argument or to step 3.
741
+ if (wrapped)
742
+ return { ok: false, reason: 'non-literal-name' };
743
+ const nameProp = AstParserService.findNameProperty(inner);
744
+ if (!nameProp)
745
+ continue;
746
+ if (ts_morph_1.Node.isShorthandPropertyAssignment(nameProp))
747
+ return { ok: false, reason: 'non-literal-name' };
748
+ const init = nameProp.getInitializer();
749
+ const value = init ? AstParserService.literalStringValue(init) : null;
750
+ if (value !== null)
751
+ return AstParserService.checkGraphqlGrammar(value);
752
+ return { ok: false, reason: 'non-literal-name' };
753
+ }
754
+ }
755
+ return AstParserService.checkGraphqlGrammar(fallbackName);
756
+ }
757
+ logGraphqlSkip(filePath, declaration, decorator, reason, value) {
758
+ const fields = { filePath, declaration, decorator, reason };
759
+ if (value !== undefined)
760
+ fields.value = value;
761
+ this.logger.debug('AstParser: graphql row skipped', fields);
762
+ }
763
+ /** `declNode`'s qualified TypeScript name for diagnostics, independent of
764
+ * whether its own `code_symbol` row was actually written. */
765
+ declarationNameFor(node, fallback) {
766
+ const described = this.describeIndexedNode(node);
767
+ return described ? this.qualifiedName(described) : fallback;
768
+ }
769
+ /** The identifier named by a `@Resolver` argument: directly, or as an arrow
770
+ * function's expression body; or the sole identifier of a single-element
771
+ * array literal in either position (list-returning resolver). */
772
+ static extractResolverIdentifier(argNode) {
773
+ let target = argNode;
774
+ if (ts_morph_1.Node.isArrowFunction(target)) {
775
+ const body = target.getBody();
776
+ if (ts_morph_1.Node.isBlock(body))
777
+ return null;
778
+ target = body;
779
+ }
780
+ if (ts_morph_1.Node.isIdentifier(target))
781
+ return target;
782
+ if (ts_morph_1.Node.isArrayLiteralExpression(target)) {
783
+ const elements = target.getElements();
784
+ if (elements.length === 1 && ts_morph_1.Node.isIdentifier(elements[0]))
785
+ return elements[0];
786
+ }
787
+ return null;
788
+ }
789
+ defaultResolveDeclaration(identifier) {
790
+ const sym = identifier.getSymbol();
791
+ if (!sym)
792
+ return undefined;
793
+ const firstDecl = sym.getDeclarations()?.[0];
794
+ const resolved = firstDecl && ts_morph_1.Node.isImportSpecifier(firstDecl) ? (sym.getAliasedSymbol() ?? sym) : sym;
795
+ return resolved.getDeclarations()?.[0];
796
+ }
797
+ /** The GraphQL type name of a `@ResolveField`'s resolved parent class
798
+ * (design decision 5, second half): its recognised `@ObjectType` (else
799
+ * `@InterfaceType`) decorator, or its class name when it carries no
800
+ * decorator spelled `ObjectType`/`InterfaceType` at all, recognised or not. */
801
+ classGraphqlTypeName(declClass) {
802
+ const declImportMap = this.buildGraphqlImportMap(declClass.getSourceFile());
803
+ const decorators = declClass.getDecorators();
804
+ const hasAnyTypeSpelledDecorator = decorators.some((d) => {
805
+ const idNode = AstParserService.decoratorIdentifier(d);
806
+ const text = idNode?.getText();
807
+ return text === 'ObjectType' || text === 'InterfaceType';
808
+ });
809
+ const recognized = decorators
810
+ .map(d => this.recognizedDecorator(d, declImportMap))
811
+ .find(r => r?.name === 'ObjectType')
812
+ ?? decorators.map(d => this.recognizedDecorator(d, declImportMap)).find(r => r?.name === 'InterfaceType');
813
+ if (recognized) {
814
+ const fallbackName = declClass.getName() ?? (declClass.isDefaultExport() ? 'default' : '');
815
+ const nameResult = this.deriveGraphqlName(recognized.args, false, fallbackName);
816
+ return nameResult.ok ? { ok: true, name: nameResult.name } : { ok: false };
817
+ }
818
+ // Carries a type-spelled decorator that isn't recognised (e.g. imported
819
+ // through a barrel) — its GraphQL name is unreadable; the class-name
820
+ // fallback never applies to it (it may differ from the unreadable name).
821
+ if (hasAnyTypeSpelledDecorator)
822
+ return { ok: false };
823
+ const name = declClass.getName();
824
+ if (!name)
825
+ return { ok: false };
826
+ return GRAPHQL_NAME_RE.test(name) ? { ok: true, name } : { ok: false, value: name };
827
+ }
828
+ /** The parent type of a `@ResolveField` — the enclosing class's recognised
829
+ * `@Resolver` argument (design decision 5, first half). */
830
+ resolveResolverParentType(cls, importMap) {
831
+ const resolverDec = cls.getDecorators()
832
+ .map(d => this.recognizedDecorator(d, importMap))
833
+ .find(r => r?.name === 'Resolver');
834
+ if (!resolverDec || resolverDec.args.length === 0)
835
+ return { ok: false };
836
+ const argNode = resolverDec.args[0];
837
+ const literal = AstParserService.literalStringValue(argNode);
838
+ if (literal !== null) {
839
+ return GRAPHQL_NAME_RE.test(literal) ? { ok: true, name: literal } : { ok: false, value: literal };
840
+ }
841
+ const identifier = AstParserService.extractResolverIdentifier(argNode);
842
+ if (!identifier)
843
+ return { ok: false };
844
+ const resolve = this.deps.resolveDeclaration ?? this.defaultResolveDeclaration.bind(this);
845
+ const declaration = resolve(identifier);
846
+ if (!declaration || !ts_morph_1.Node.isClassDeclaration(declaration))
847
+ return { ok: false };
848
+ return this.classGraphqlTypeName(declaration);
849
+ }
850
+ extractClassTypeRows(cls, importMap, ownByDeclNode, filePath, serviceName, entries) {
851
+ const own = ownByDeclNode.get(cls);
852
+ const declaration = this.declarationNameFor(cls, own?.entry.name ?? cls.getName() ?? 'default');
853
+ const seenNames = new Set();
854
+ for (const dec of cls.getDecorators()) {
855
+ const rec = this.recognizedDecorator(dec, importMap);
856
+ if (!rec || !GRAPHQL_TYPE_DECORATORS.has(rec.name))
857
+ continue;
858
+ if (AstParserService.isAbstractGraphqlType(rec.args)) {
859
+ this.logGraphqlSkip(filePath, declaration, rec.name, 'abstract-type');
860
+ continue;
861
+ }
862
+ if (!own) {
863
+ this.logGraphqlSkip(filePath, declaration, rec.name, 'no-symbol-row');
864
+ continue;
865
+ }
866
+ const nameResult = this.deriveGraphqlName(rec.args, false, own.entry.name);
867
+ if (!nameResult.ok) {
868
+ this.logGraphqlSkip(filePath, declaration, rec.name, nameResult.reason, nameResult.reason === 'invalid-name' ? nameResult.value : undefined);
869
+ continue;
870
+ }
871
+ if (seenNames.has(nameResult.name)) {
872
+ this.logGraphqlSkip(filePath, declaration, rec.name, 'duplicate', own.entry.name);
873
+ continue;
874
+ }
875
+ seenNames.add(nameResult.name);
876
+ entries.push({
877
+ project: serviceName, filePath, name: nameResult.name, kind: 'graphql_type',
878
+ signature: own.entry.signature, line: own.entry.line, endLine: own.entry.endLine, col: own.entry.col,
879
+ container: null, isStatic: false, target: declaration,
880
+ });
881
+ }
882
+ }
883
+ extractResolveFieldRow(method, rec, own, declaration, importMap, filePath, serviceName, entries) {
884
+ if (!own) {
885
+ this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'no-symbol-row');
886
+ return;
887
+ }
888
+ const cls = method.getParent();
889
+ if (!cls || !ts_morph_1.Node.isClassDeclaration(cls)) {
890
+ this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'unresolvable-parent');
891
+ return;
892
+ }
893
+ const parent = this.resolveResolverParentType(cls, importMap);
894
+ if (!parent.ok) {
895
+ this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'unresolvable-parent', parent.value);
896
+ return;
897
+ }
898
+ const nameResult = this.deriveGraphqlName(rec.args, true, own.entry.name);
899
+ if (!nameResult.ok) {
900
+ this.logGraphqlSkip(filePath, declaration, 'ResolveField', nameResult.reason, nameResult.reason === 'invalid-name' ? nameResult.value : undefined);
901
+ return;
902
+ }
903
+ entries.push({
904
+ project: serviceName, filePath, name: nameResult.name, kind: 'graphql_field',
905
+ signature: own.entry.signature, line: own.entry.line, endLine: own.entry.endLine, col: own.entry.col,
906
+ container: parent.name, isStatic: own.entry.isStatic, target: declaration,
907
+ });
908
+ }
909
+ extractMethodOperationRows(method, importMap, ownByDeclNode, filePath, serviceName, entries) {
910
+ const own = ownByDeclNode.get(method);
911
+ const declaration = this.declarationNameFor(method, own?.entry.name ?? AstParserService.memberName(method.getNameNode()) ?? '<anonymous>');
912
+ for (const dec of method.getDecorators()) {
913
+ const rec = this.recognizedDecorator(dec, importMap);
914
+ if (!rec)
915
+ continue;
916
+ if (rec.name === 'ResolveField') {
917
+ // Isolated per field: a throw resolving this field's parent (e.g. the
918
+ // injected test seam) degrades to "no row for that field" and never
919
+ // trips the file-level catch in `analyzeSourceFile`.
920
+ try {
921
+ this.extractResolveFieldRow(method, rec, own, declaration, importMap, filePath, serviceName, entries);
922
+ }
923
+ catch (err) {
924
+ this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'unresolvable-parent');
925
+ }
926
+ continue;
927
+ }
928
+ const kind = GRAPHQL_OPERATION_KIND[rec.name];
929
+ if (!kind)
930
+ continue; // recognised but not an operation decorator (wrong declaration kind) — ignored, no log
931
+ if (!own) {
932
+ this.logGraphqlSkip(filePath, declaration, rec.name, 'no-symbol-row');
933
+ continue;
934
+ }
935
+ const nameResult = this.deriveGraphqlName(rec.args, true, own.entry.name);
936
+ if (!nameResult.ok) {
937
+ this.logGraphqlSkip(filePath, declaration, rec.name, nameResult.reason, nameResult.reason === 'invalid-name' ? nameResult.value : undefined);
938
+ continue;
939
+ }
940
+ entries.push({
941
+ project: serviceName, filePath, name: nameResult.name, kind,
942
+ signature: own.entry.signature, line: own.entry.line, endLine: own.entry.endLine, col: own.entry.col,
943
+ container: rec.name, isStatic: own.entry.isStatic, target: declaration,
944
+ });
945
+ }
946
+ }
947
+ /** GraphQL-named rows for every recognised `@nestjs/graphql` decorator in
948
+ * the file (`graphql-symbols`). `ownRows` is `symbols ∪ members` — the
949
+ * declarations' own `code_symbol` candidates, checked so a GraphQL row is
950
+ * never written without its decorated declaration's own row. */
951
+ extractGraphqlNames(sourceFile, filePath, serviceName, ownRows) {
952
+ const importMap = this.buildGraphqlImportMap(sourceFile);
953
+ if (importMap.size === 0)
954
+ return [];
955
+ const ownByDeclNode = new Map();
956
+ for (const c of ownRows)
957
+ ownByDeclNode.set(c.declNode, c);
958
+ const entries = [];
959
+ for (const cls of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.ClassDeclaration)) {
960
+ this.extractClassTypeRows(cls, importMap, ownByDeclNode, filePath, serviceName, entries);
961
+ }
962
+ for (const method of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.MethodDeclaration)) {
963
+ this.extractMethodOperationRows(method, importMap, ownByDeclNode, filePath, serviceName, entries);
964
+ }
965
+ return entries;
966
+ }
563
967
  // ── private: shared container resolution ────────────────────────────────────
564
968
  static isValidSegment(s) {
565
969
  return VALID_SEGMENT.test(s);
@@ -632,7 +1036,7 @@ class AstParserService {
632
1036
  * (exclude) everything nested inside it? */
633
1037
  walkStep(node) {
634
1038
  if (ts_morph_1.Node.isModuleDeclaration(node)) {
635
- if (node.compilerNode.flags & typescript_1.NodeFlags.GlobalAugmentation)
1039
+ if (node.compilerNode.flags & ts_morph_1.NodeFlags.GlobalAugmentation)
636
1040
  return { kind: 'exclude' };
637
1041
  const nameNode = node.getNameNode();
638
1042
  if (ts_morph_1.Node.isStringLiteral(nameNode))
@@ -6,7 +6,6 @@ import * as schema from '../db/schema';
6
6
  type Schema = typeof schema;
7
7
  export type DrizzleDb = NodePgDatabase<Schema>;
8
8
  export type DrizzleTx = NodePgTransaction<Schema, ExtractTablesWithRelations<Schema>>;
9
- export declare const CURRENT_SYMBOL_INDEX_VERSION = 2;
10
9
  export declare class DatabaseService implements IDatabase {
11
10
  private config;
12
11
  private pool;
@@ -32,17 +31,6 @@ export declare class DatabaseService implements IDatabase {
32
31
  deleteFiles(project: string, filePaths: string[]): Promise<void>;
33
32
  listIndexedProjects(): Promise<string[]>;
34
33
  deleteProject(name: string): Promise<void>;
35
- listProjectsWithIndexRows(): Promise<string[]>;
36
- /**
37
- * Among `projectNames` (or every project with rows when null), those with at
38
- * least one row in `code_chunk`/`code_symbol` and a `symbol_index_version`
39
- * below `CURRENT_SYMBOL_INDEX_VERSION` (a missing row counts as version 1).
40
- * A missing `symbol_index_version` table (un-migrated DB, `rag-mcp`/`rag-web`
41
- * bootstrapped with `initDb`) counts as "no marker rows" — every project with
42
- * rows is then outdated. Any other database error propagates.
43
- */
44
- listOutdatedProjects(projectNames: string[] | null): Promise<string[]>;
45
- upsertSymbolIndexVersion(project: string): Promise<void>;
46
34
  /** Guard against SQL injection — the dimension is interpolated into DDL. */
47
35
  private assertDim;
48
36
  private assertMinPostgresVersion;