codesearch-mcp 0.2.2-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 +34 -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 +430 -25
  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
@@ -38,6 +38,12 @@ const path = __importStar(require("path"));
38
38
  const fs = __importStar(require("fs"));
39
39
  const ts_morph_1 = require("ts-morph");
40
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]*$/;
41
47
  const FALLBACK_OPTS = {
42
48
  skipAddingFilesFromTsConfig: true,
43
49
  skipFileDependencyResolution: false,
@@ -47,9 +53,11 @@ const MORPH_CACHE_MAX = 10;
47
53
  const VALID_SEGMENT = /^#?[\w$]+$/;
48
54
  class AstParserService {
49
55
  logger;
56
+ deps;
50
57
  morphProjects = new Map();
51
- constructor(logger = LoggerService_1.LoggerService.silent()) {
58
+ constructor(logger = LoggerService_1.LoggerService.silent(), deps = {}) {
52
59
  this.logger = logger;
60
+ this.deps = deps;
53
61
  }
54
62
  // ── public interface ────────────────────────────────────────────────────────
55
63
  async analyzeProject(filePaths, serviceName, projectRoot = process.cwd(), opts = {}) {
@@ -61,7 +69,7 @@ class AstParserService {
61
69
  const morphProject = this.createMorphProject(projectRoot, skipDependencyResolution, rootTsConfigPath);
62
70
  for (const filePath of filePaths) {
63
71
  if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
64
- analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [] });
72
+ analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] });
65
73
  continue;
66
74
  }
67
75
  try {
@@ -69,7 +77,7 @@ class AstParserService {
69
77
  }
70
78
  catch (err) {
71
79
  this.logger.warn('AstParser: failed to add source file', { filePath, reason: err.message });
72
- analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [] });
80
+ analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] });
73
81
  }
74
82
  }
75
83
  for (const sourceFile of morphProject.getSourceFiles()) {
@@ -101,7 +109,7 @@ class AstParserService {
101
109
  this.logger.debug('AstParser: analyzeFile start', { filePath, serviceName, projectRoot });
102
110
  if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
103
111
  this.logger.debug('AstParser: analyzeFile skip (non-TS)', { filePath, ms: Date.now() - t0 });
104
- return { edges: [], symbols: [], calls: [], members: [] };
112
+ return { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] };
105
113
  }
106
114
  const morphProject = this.getMorphProject(projectRoot);
107
115
  const existing = morphProject.getSourceFile(filePath);
@@ -113,7 +121,7 @@ class AstParserService {
113
121
  }
114
122
  catch (err) {
115
123
  this.logger.warn('AstParser: analyzeFile failed to add source file', { filePath, reason: err.message, ms: Date.now() - t0 });
116
- return { edges: [], symbols: [], calls: [], members: [] };
124
+ return { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] };
117
125
  }
118
126
  const result = this.analyzeSourceFile(sourceFile, filePath, serviceName);
119
127
  morphProject.removeSourceFile(sourceFile);
@@ -148,11 +156,24 @@ class AstParserService {
148
156
  const members = memberCandidates.map(c => c.entry);
149
157
  const calls = this.extractCalls(sourceFile, filePath, serviceName);
150
158
  const ownerRanges = this.buildOwnerRanges([...symbolCandidates, ...memberCandidates]);
151
- 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 };
152
173
  }
153
174
  catch (err) {
154
175
  this.logger.warn('AstParser: failed to analyze file', { filePath, reason: err.message });
155
- return { edges: [], symbols: [], calls: [], members: [] };
176
+ return { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] };
156
177
  }
157
178
  }
158
179
  // ── private: MorphProject cache (LRU, max MORPH_CACHE_MAX entries) ──────────
@@ -230,8 +251,19 @@ class AstParserService {
230
251
  const edges = [];
231
252
  const usageMap = this.buildUsageMap(sourceFile);
232
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);
233
258
  const resolved = decl.getModuleSpecifierSourceFile();
234
- 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) {
235
267
  const specifier = decl.getModuleSpecifierValue();
236
268
  const isCodeImport = !/\.(json|css|scss|less|svg|png|jpg|gif|ico|woff2?|ttf|eot)(\?.*)?$/.test(specifier);
237
269
  if (specifier.startsWith('.') && isCodeImport) {
@@ -239,34 +271,37 @@ class AstParserService {
239
271
  }
240
272
  continue;
241
273
  }
242
- const toFile = resolved.getFilePath();
243
274
  if (toFile.includes('/node_modules/'))
244
275
  continue;
245
- const namedImports = decl.getNamedImports();
246
- const defaultImport = decl.getDefaultImport();
247
- const namespaceImport = decl.getNamespaceImport();
248
- if (namedImports.length > 0) {
249
- for (const named of namedImports) {
250
- const localName = named.getAliasNode()?.getText() ?? named.getName();
251
- const fromSymbol = this.resolveFromSymbol(localName, usageMap);
252
- edges.push({
253
- project: serviceName, fromFile: filePath, fromSymbol,
254
- toFile, toSymbol: named.getName(), depType: 'import',
255
- });
256
- }
257
- }
258
- else if (defaultImport) {
276
+ if (!hasClause) {
259
277
  edges.push({
260
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,
261
287
  toFile, toSymbol: null, depType: 'import-default',
262
288
  });
263
289
  }
264
- else if (namespaceImport) {
290
+ if (namespaceImport) {
291
+ const fromSymbol = this.resolveFromSymbol(namespaceImport.getText(), usageMap);
265
292
  edges.push({
266
- project: serviceName, fromFile: filePath, fromSymbol: null,
293
+ project: serviceName, fromFile: filePath, fromSymbol,
267
294
  toFile, toSymbol: null, depType: 'import-namespace',
268
295
  });
269
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
+ }
270
305
  }
271
306
  for (const decl of sourceFile.getExportDeclarations()) {
272
307
  const resolved = decl.getModuleSpecifierSourceFile();
@@ -293,6 +328,18 @@ class AstParserService {
293
328
  }
294
329
  return edges;
295
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
+ }
296
343
  resolveFromSymbol(localName, usageMap) {
297
344
  const users = usageMap.get(localName);
298
345
  return users?.length === 1 ? users[0] : null;
@@ -559,6 +606,364 @@ class AstParserService {
559
606
  endLine: c.declNode.getEndLineNumber(),
560
607
  }));
561
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
+ }
562
967
  // ── private: shared container resolution ────────────────────────────────────
563
968
  static isValidSegment(s) {
564
969
  return VALID_SEGMENT.test(s);
@@ -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;
@@ -33,14 +33,13 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.DatabaseService = exports.CURRENT_SYMBOL_INDEX_VERSION = void 0;
36
+ exports.DatabaseService = void 0;
37
37
  const node_postgres_1 = require("drizzle-orm/node-postgres");
38
38
  const pg_1 = require("pg");
39
39
  const drizzle_orm_1 = require("drizzle-orm");
40
40
  const schema = __importStar(require("../db/schema"));
41
41
  const db_1 = require("../db");
42
42
  const MIN_PG_VERSION_NUM = 150000;
43
- exports.CURRENT_SYMBOL_INDEX_VERSION = 2;
44
43
  class DatabaseService {
45
44
  config;
46
45
  pool;
@@ -140,6 +139,7 @@ class DatabaseService {
140
139
  await this.query(`CREATE INDEX IF NOT EXISTS idx_symbol_file ON code_symbol (project, "filePath")`);
141
140
  await this.query(`ALTER TABLE code_symbol ADD COLUMN IF NOT EXISTS container TEXT`);
142
141
  await this.query(`ALTER TABLE code_symbol ADD COLUMN IF NOT EXISTS "isStatic" BOOLEAN NOT NULL DEFAULT false`);
142
+ await this.query(`ALTER TABLE code_symbol ADD COLUMN IF NOT EXISTS "target" TEXT`);
143
143
  // DDL is transactional in PostgreSQL — either both statements apply or
144
144
  // neither does, so a failure here never leaves code_symbol without a
145
145
  // unique index (the old idx_symbol_unique is dropped only once the new
@@ -149,12 +149,6 @@ class DatabaseService {
149
149
  await tx.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_symbol_unique_v2 ' +
150
150
  'ON code_symbol (project, "filePath", container, name, kind, line) NULLS NOT DISTINCT');
151
151
  });
152
- await this.query(`
153
- CREATE TABLE IF NOT EXISTS symbol_index_version (
154
- project TEXT PRIMARY KEY,
155
- version INT NOT NULL
156
- )
157
- `);
158
152
  // code_call table
159
153
  await this.query(`
160
154
  CREATE TABLE IF NOT EXISTS code_call (
@@ -268,50 +262,6 @@ class DatabaseService {
268
262
  await tx.delete(db_1.codeDependency).where((0, drizzle_orm_1.eq)(db_1.codeDependency.project, name));
269
263
  });
270
264
  }
271
- async listProjectsWithIndexRows() {
272
- const rows = await this.query(`SELECT DISTINCT name FROM (
273
- SELECT "serviceName" AS name FROM code_chunk
274
- UNION
275
- SELECT project AS name FROM code_symbol
276
- ) t`);
277
- return rows.map(r => r.name);
278
- }
279
- /**
280
- * Among `projectNames` (or every project with rows when null), those with at
281
- * least one row in `code_chunk`/`code_symbol` and a `symbol_index_version`
282
- * below `CURRENT_SYMBOL_INDEX_VERSION` (a missing row counts as version 1).
283
- * A missing `symbol_index_version` table (un-migrated DB, `rag-mcp`/`rag-web`
284
- * bootstrapped with `initDb`) counts as "no marker rows" — every project with
285
- * rows is then outdated. Any other database error propagates.
286
- */
287
- async listOutdatedProjects(projectNames) {
288
- const rowsUnion = `
289
- SELECT "serviceName" AS name FROM code_chunk
290
- UNION
291
- SELECT project AS name FROM code_symbol
292
- `;
293
- const filterSql = projectNames ? 'AND t.name = ANY($1)' : '';
294
- const params = projectNames ? [projectNames] : [];
295
- try {
296
- const rows = await this.query(`SELECT DISTINCT t.name FROM (${rowsUnion}) t
297
- WHERE NOT EXISTS (
298
- SELECT 1 FROM symbol_index_version v
299
- WHERE v.project = t.name AND v.version >= ${exports.CURRENT_SYMBOL_INDEX_VERSION}
300
- )
301
- ${filterSql}`, params);
302
- return rows.map(r => r.name);
303
- }
304
- catch (err) {
305
- if (!/relation "symbol_index_version" does not exist/i.test(err.message))
306
- throw err;
307
- const rows = await this.query(`SELECT DISTINCT t.name FROM (${rowsUnion}) t ${projectNames ? 'WHERE t.name = ANY($1)' : ''}`, params);
308
- return rows.map(r => r.name);
309
- }
310
- }
311
- async upsertSymbolIndexVersion(project) {
312
- await this.query(`INSERT INTO symbol_index_version (project, version) VALUES ($1, $2)
313
- ON CONFLICT (project) DO UPDATE SET version = EXCLUDED.version`, [project, exports.CURRENT_SYMBOL_INDEX_VERSION]);
314
- }
315
265
  /** Guard against SQL injection — the dimension is interpolated into DDL. */
316
266
  assertDim(dimensions) {
317
267
  if (!Number.isInteger(dimensions) || dimensions <= 0 || dimensions > 2000) {