codesearch-mcp 0.3.0-alpha → 0.4.0-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 (224) hide show
  1. package/CHANGELOG.md +59 -6
  2. package/LICENSE +661 -21
  3. package/README.md +33 -17
  4. package/dist/app.js +13 -20
  5. package/dist/src/ast/CallExtractor.d.ts +8 -0
  6. package/dist/src/ast/CallExtractor.js +68 -0
  7. package/dist/src/ast/GraphqlNameExtractor.d.ts +22 -0
  8. package/dist/src/ast/GraphqlNameExtractor.js +240 -0
  9. package/dist/src/ast/ImportEdgeExtractor.d.ts +11 -0
  10. package/dist/src/ast/ImportEdgeExtractor.js +129 -0
  11. package/dist/src/ast/IndexedNodeResolver.d.ts +14 -0
  12. package/dist/src/ast/IndexedNodeResolver.js +217 -0
  13. package/dist/src/ast/MemberExtractor.d.ts +8 -0
  14. package/dist/src/ast/MemberExtractor.js +129 -0
  15. package/dist/src/ast/TopLevelSymbolExtractor.d.ts +5 -0
  16. package/dist/src/ast/TopLevelSymbolExtractor.js +86 -0
  17. package/dist/src/ast/graphql-grammar.d.ts +24 -0
  18. package/dist/src/ast/graphql-grammar.js +93 -0
  19. package/dist/src/ast/node-helpers.d.ts +11 -0
  20. package/dist/src/ast/node-helpers.js +55 -0
  21. package/dist/src/commands/EvalCommand.d.ts +7 -3
  22. package/dist/src/commands/EvalCommand.js +36 -158
  23. package/dist/src/commands/IndexCommand.d.ts +10 -4
  24. package/dist/src/commands/IndexCommand.js +52 -124
  25. package/dist/src/commands/McpCommand.d.ts +6 -3
  26. package/dist/src/commands/McpCommand.js +19 -14
  27. package/dist/src/commands/WebCommand.d.ts +9 -3
  28. package/dist/src/commands/WebCommand.js +17 -8
  29. package/dist/src/commands/eval/baseline-comparison.d.ts +6 -0
  30. package/dist/src/commands/eval/baseline-comparison.js +54 -0
  31. package/dist/src/commands/eval/eval-args.d.ts +6 -0
  32. package/dist/src/commands/eval/eval-args.js +71 -0
  33. package/dist/src/commands/eval/eval-corpora.d.ts +6 -0
  34. package/dist/src/commands/eval/eval-corpora.js +76 -0
  35. package/dist/src/commands/eval/eval-metadata.d.ts +17 -0
  36. package/dist/src/commands/eval/eval-metadata.js +75 -0
  37. package/dist/src/commands/eval/index-inspector.d.ts +3 -0
  38. package/dist/src/commands/eval/index-inspector.js +21 -0
  39. package/dist/src/commands/eval/staged-search.d.ts +3 -0
  40. package/dist/src/commands/eval/staged-search.js +20 -0
  41. package/dist/src/commands/eval/suite-loader.d.ts +8 -0
  42. package/dist/src/commands/eval/suite-loader.js +52 -0
  43. package/dist/src/commands/index/backend-probe.d.ts +2 -0
  44. package/dist/src/commands/index/backend-probe.js +15 -0
  45. package/dist/src/commands/index/embedding-setup.d.ts +3 -0
  46. package/dist/src/commands/index/embedding-setup.js +16 -0
  47. package/dist/src/commands/index/index-summary.d.ts +2 -0
  48. package/dist/src/commands/index/index-summary.js +22 -0
  49. package/dist/src/commands/index/legacy-ollama-guard.d.ts +3 -0
  50. package/dist/src/commands/index/legacy-ollama-guard.js +23 -0
  51. package/dist/src/commands/index/prune-step.d.ts +3 -0
  52. package/dist/src/commands/index/prune-step.js +22 -0
  53. package/dist/src/commands/index/scan-targets.d.ts +8 -0
  54. package/dist/src/commands/index/scan-targets.js +107 -0
  55. package/dist/src/config-store.d.ts +0 -1
  56. package/dist/src/config-store.js +3 -4
  57. package/dist/src/config.d.ts +1 -1
  58. package/dist/src/config.js +1 -1
  59. package/dist/src/constants.d.ts +15 -0
  60. package/dist/src/constants.js +34 -6
  61. package/dist/src/container.d.ts +4 -2
  62. package/dist/src/container.js +7 -5
  63. package/dist/src/db/drizzle-types.d.ts +7 -0
  64. package/dist/src/db/index.d.ts +1 -0
  65. package/dist/src/db/schema-steps.d.ts +8 -0
  66. package/dist/src/db/schema-steps.js +105 -0
  67. package/dist/src/declarations/DeclarationTree.d.ts +1 -1
  68. package/dist/src/eval-results.js +16 -2
  69. package/dist/src/eval-vector-index.d.ts +5 -0
  70. package/dist/src/eval-vector-index.js +60 -0
  71. package/dist/src/help.js +9 -1
  72. package/dist/src/index-args.d.ts +13 -0
  73. package/dist/src/index-args.js +48 -0
  74. package/dist/src/indexing/AnalysisBatcher.d.ts +12 -0
  75. package/dist/src/indexing/AnalysisBatcher.js +28 -0
  76. package/dist/src/indexing/ChunkEmbedder.d.ts +24 -0
  77. package/dist/src/indexing/ChunkEmbedder.js +37 -0
  78. package/dist/src/indexing/ConsoleScanReporter.d.ts +10 -0
  79. package/dist/src/indexing/ConsoleScanReporter.js +73 -0
  80. package/dist/src/indexing/EmbeddingSchemaPreparer.d.ts +14 -0
  81. package/dist/src/indexing/EmbeddingSchemaPreparer.js +66 -0
  82. package/dist/src/indexing/FileIngestor.d.ts +22 -0
  83. package/dist/src/indexing/FileIngestor.js +116 -0
  84. package/dist/src/indexing/IndexStore.d.ts +24 -0
  85. package/dist/src/indexing/IndexStore.js +69 -0
  86. package/dist/src/indexing/StaleFilePruner.d.ts +8 -0
  87. package/dist/src/indexing/StaleFilePruner.js +55 -0
  88. package/dist/src/indexing/analysis-rows.d.ts +4 -0
  89. package/dist/src/indexing/analysis-rows.js +65 -0
  90. package/dist/src/indexing/project-notices.d.ts +2 -0
  91. package/dist/src/indexing/project-notices.js +63 -0
  92. package/dist/src/interfaces/IAstParser.d.ts +2 -1
  93. package/dist/src/interfaces/ICallGraph.d.ts +28 -0
  94. package/dist/src/interfaces/ICallGraph.js +2 -0
  95. package/dist/src/interfaces/IChunkSearch.d.ts +42 -0
  96. package/dist/src/interfaces/IChunkSearch.js +2 -0
  97. package/dist/src/interfaces/IDatabase.d.ts +6 -2
  98. package/dist/src/interfaces/IDependencyGraph.d.ts +24 -0
  99. package/dist/src/interfaces/IDependencyGraph.js +2 -0
  100. package/dist/src/interfaces/IIndexIdentityGuard.d.ts +3 -0
  101. package/dist/src/interfaces/IIndexIdentityGuard.js +2 -0
  102. package/dist/src/interfaces/IIndexedNodeResolver.d.ts +10 -0
  103. package/dist/src/interfaces/IIndexedNodeResolver.js +2 -0
  104. package/dist/src/interfaces/IMcpReadiness.d.ts +12 -0
  105. package/dist/src/interfaces/IMcpReadiness.js +2 -0
  106. package/dist/src/interfaces/IProjectCatalog.d.ts +13 -0
  107. package/dist/src/interfaces/IProjectCatalog.js +2 -0
  108. package/dist/src/interfaces/IProjectRoots.d.ts +3 -0
  109. package/dist/src/interfaces/IProjectRoots.js +2 -0
  110. package/dist/src/interfaces/IRetrievalEvaluator.d.ts +21 -2
  111. package/dist/src/interfaces/IScanReporter.d.ts +13 -0
  112. package/dist/src/interfaces/IScanReporter.js +2 -0
  113. package/dist/src/interfaces/ISymbolLookup.d.ts +49 -0
  114. package/dist/src/interfaces/ISymbolLookup.js +2 -0
  115. package/dist/src/interfaces/IToolPaths.d.ts +62 -0
  116. package/dist/src/interfaces/IToolPaths.js +2 -0
  117. package/dist/src/interfaces/IWorkspaceFiles.d.ts +28 -0
  118. package/dist/src/interfaces/IWorkspaceFiles.js +2 -0
  119. package/dist/src/interfaces/SymbolKind.d.ts +1 -0
  120. package/dist/src/interfaces/SymbolKind.js +2 -0
  121. package/dist/src/interfaces/index.d.ts +12 -1
  122. package/dist/src/project-scope.d.ts +1 -0
  123. package/dist/src/project-scope.js +5 -0
  124. package/dist/src/retrieval-eval.d.ts +2 -1
  125. package/dist/src/retrieval-eval.js +45 -2
  126. package/dist/src/search/CallGraph.d.ts +9 -0
  127. package/dist/src/search/CallGraph.js +84 -0
  128. package/dist/src/search/ChunkSearch.d.ts +23 -0
  129. package/dist/src/search/ChunkSearch.js +115 -0
  130. package/dist/src/search/DependencyGraph.d.ts +19 -0
  131. package/dist/src/search/DependencyGraph.js +180 -0
  132. package/dist/src/search/IndexIdentityGuard.d.ts +11 -0
  133. package/dist/src/search/IndexIdentityGuard.js +37 -0
  134. package/dist/src/search/ProjectCatalog.d.ts +11 -0
  135. package/dist/src/search/ProjectCatalog.js +38 -0
  136. package/dist/src/search/SymbolLookup.d.ts +24 -0
  137. package/dist/src/search/SymbolLookup.js +97 -0
  138. package/dist/src/search/chunks/CandidateFetcher.d.ts +19 -0
  139. package/dist/src/search/chunks/CandidateFetcher.js +40 -0
  140. package/dist/src/search/chunks/IterativeScanGate.d.ts +10 -0
  141. package/dist/src/search/chunks/IterativeScanGate.js +42 -0
  142. package/dist/src/search/chunks/RerankStage.d.ts +15 -0
  143. package/dist/src/search/chunks/RerankStage.js +42 -0
  144. package/dist/src/search/chunks/search-metrics.d.ts +18 -0
  145. package/dist/src/search/chunks/search-metrics.js +32 -0
  146. package/dist/src/search/chunks/search-plan.d.ts +18 -0
  147. package/dist/src/search/chunks/search-plan.js +19 -0
  148. package/dist/src/search/create-search-services.d.ts +26 -0
  149. package/dist/src/search/create-search-services.js +29 -0
  150. package/dist/src/search/index-paths.d.ts +1 -0
  151. package/dist/src/search/index-paths.js +5 -0
  152. package/dist/src/search/symbol-sql.d.ts +13 -0
  153. package/dist/src/search/symbol-sql.js +66 -0
  154. package/dist/src/search/symbols/SymbolGrepFallback.d.ts +19 -0
  155. package/dist/src/search/symbols/SymbolGrepFallback.js +106 -0
  156. package/dist/src/search/symbols/SymbolIndexQuery.d.ts +14 -0
  157. package/dist/src/search/symbols/SymbolIndexQuery.js +52 -0
  158. package/dist/src/search/symbols/SymbolTextSearch.d.ts +13 -0
  159. package/dist/src/search/symbols/SymbolTextSearch.js +84 -0
  160. package/dist/src/search/symbols/TargetColumnGate.d.ts +7 -0
  161. package/dist/src/search/symbols/TargetColumnGate.js +23 -0
  162. package/dist/src/search/symbols/find-symbol-constants.d.ts +4 -0
  163. package/dist/src/search/symbols/find-symbol-constants.js +8 -0
  164. package/dist/src/search/symbols/grep-args.d.ts +3 -0
  165. package/dist/src/search/symbols/grep-args.js +7 -0
  166. package/dist/src/search/symbols/grep-patterns.d.ts +4 -0
  167. package/dist/src/search/symbols/grep-patterns.js +60 -0
  168. package/dist/src/services/AstParserService.d.ts +8 -44
  169. package/dist/src/services/AstParserService.js +19 -938
  170. package/dist/src/services/DatabaseService.d.ts +8 -10
  171. package/dist/src/services/DatabaseService.js +29 -86
  172. package/dist/src/services/IndexerService.d.ts +7 -10
  173. package/dist/src/services/IndexerService.js +36 -255
  174. package/dist/src/services/McpReadiness.d.ts +18 -0
  175. package/dist/src/services/McpReadiness.js +73 -0
  176. package/dist/src/services/McpServer.d.ts +32 -9
  177. package/dist/src/services/McpServer.js +78 -41
  178. package/dist/src/services/WebServerService.d.ts +4 -3
  179. package/dist/src/services/WebServerService.js +16 -8
  180. package/dist/src/setup.d.ts +16 -4
  181. package/dist/src/setup.js +115 -40
  182. package/dist/src/symbol-name.d.ts +1 -0
  183. package/dist/src/symbol-name.js +16 -12
  184. package/dist/src/tools/FindSymbolTool.d.ts +17 -7
  185. package/dist/src/tools/FindSymbolTool.js +54 -35
  186. package/dist/src/tools/GetCallersTool.d.ts +16 -8
  187. package/dist/src/tools/GetCallersTool.js +65 -35
  188. package/dist/src/tools/GetDependenciesTool.d.ts +16 -8
  189. package/dist/src/tools/GetDependenciesTool.js +35 -28
  190. package/dist/src/tools/GetDependentsTool.d.ts +20 -8
  191. package/dist/src/tools/GetDependentsTool.js +47 -33
  192. package/dist/src/tools/ListFilesTool.d.ts +16 -8
  193. package/dist/src/tools/ListFilesTool.js +20 -26
  194. package/dist/src/tools/ListProjectsTool.d.ts +11 -7
  195. package/dist/src/tools/ListProjectsTool.js +13 -11
  196. package/dist/src/tools/McpTool.d.ts +17 -0
  197. package/dist/src/tools/McpTool.js +28 -0
  198. package/dist/src/tools/ReadFileRangeTool.d.ts +16 -8
  199. package/dist/src/tools/ReadFileRangeTool.js +23 -25
  200. package/dist/src/tools/ReadFileTool.d.ts +15 -8
  201. package/dist/src/tools/ReadFileTool.js +19 -24
  202. package/dist/src/tools/SearchCodebaseTool.d.ts +22 -10
  203. package/dist/src/tools/SearchCodebaseTool.js +69 -51
  204. package/dist/src/tools/format-helpers.d.ts +9 -3
  205. package/dist/src/tools/format-helpers.js +59 -14
  206. package/dist/src/tools/tool-args.d.ts +38 -0
  207. package/dist/src/tools/tool-args.js +95 -0
  208. package/dist/src/workspace/GrepRunner.d.ts +15 -0
  209. package/dist/src/workspace/GrepRunner.js +104 -0
  210. package/dist/src/workspace/ProjectRoots.d.ts +22 -0
  211. package/dist/src/workspace/ProjectRoots.js +102 -0
  212. package/dist/src/workspace/ToolPaths.d.ts +31 -0
  213. package/dist/src/workspace/ToolPaths.js +312 -0
  214. package/dist/src/workspace/WorkspaceFiles.d.ts +19 -0
  215. package/dist/src/workspace/WorkspaceFiles.js +285 -0
  216. package/dist/src/workspace/fs-access.d.ts +9 -0
  217. package/dist/src/workspace/fs-access.js +47 -0
  218. package/dist/src/workspace/path-helpers.d.ts +3 -0
  219. package/dist/src/workspace/path-helpers.js +43 -0
  220. package/package.json +4 -3
  221. package/dist/src/interfaces/ISearcher.d.ts +0 -46
  222. package/dist/src/services/SearcherService.d.ts +0 -235
  223. package/dist/src/services/SearcherService.js +0 -1118
  224. /package/dist/src/{interfaces/ISearcher.js → db/drizzle-types.js} +0 -0
package/README.md CHANGED
@@ -30,7 +30,7 @@ See `CHANGELOG.md` for version history.
30
30
 
31
31
  ## Requirements
32
32
 
33
- - Node.js 18+
33
+ - Node.js `^22.13.0` or `>=23.5.0`
34
34
  - PostgreSQL 15+ with pgvector (the symbol index's unique index relies on `NULLS NOT DISTINCT`, added in PostgreSQL 15); pgvector ≥ 0.8 recommended (filtered search on large projects; older versions work with reduced recall)
35
35
  - Docker (for local DB + Infinity) — or bring your own PostgreSQL with pgvector + an embedding backend (see [Embedding providers](#embedding-providers))
36
36
 
@@ -48,7 +48,8 @@ rag-setup
48
48
 
49
49
  Walks you through:
50
50
  - **Database** — enter connection details; optionally starts Docker containers automatically
51
- - **Embedding backend** — pick a provider (`infinity` / `openai-compatible` / `voyage`), its model, and an API key if the provider needs one (see [Embedding providers](#embedding-providers))
51
+ - **Embedding backend** — pick a provider (`infinity` / `openai-compatible` / `voyage`), its model, and an API key if the provider needs one (see [Embedding providers](#embedding-providers)), then a reranker
52
+ - **`voyage`** is a short path: the API key comes first and is required (stored once in `.env` as `VOYAGE_API_KEY`); no base URL is asked. The embedding model is picked from a numbered list (`voyage-code-4` default, `voyage-4-large`, `voyage-4`, `voyage-4-lite`) — enter a number, press Enter for the default, or type any model name. Then `Enable Voyage reranker? (y/n) (y)`: `y` picks `rerank-3` (default) or `rerank-3-lite` the same way and reuses the same key (`reranker.apiKey: env:VOYAGE_API_KEY`); `n` turns reranking off. Other rerankers for a Voyage embedder (an `openai-compatible` `/rerank` server, a separate Voyage key) are set with `rag-config` — see [Reranker with a cloud embedder](#reranker-with-a-cloud-or-external-embedder)
52
53
  - **Logging** — choose log level and directory (defaults: `error` level, `~/.config/codesearch-mcp/logs/`)
53
54
 
54
55
  Config is saved to `~/.config/codesearch-mcp/config.json`.
@@ -76,15 +77,28 @@ rag-index --select --reindex
76
77
  rag-index --project billing-service # scope this run to one project, by exact name
77
78
  rag-index --reindex --project billing-service # force a full reindex of just that project
78
79
  rag-index --project # no name — pick one from an interactive prompt
80
+ rag-index --project billing-service --require-cwd # refuse the run unless the current directory is that project's directory
79
81
  ```
80
82
 
81
83
  `--project` leaves every other indexed project untouched: it skips the `.ragprojects`/`.ragskip` selector entirely and never prunes projects outside the one(s) it resolves to. A name that doesn't match any discovered project or monorepo unit exits with an error before touching the database — matching exactly, no partial or fuzzy match.
82
84
 
85
+ `--require-cwd` is for scripts that must not read another checkout's index by accident: run from a linked worktree, a second clone, or a subdirectory of the project, `rag-index` prints both directories and exits 1 before any database or embedding work. It needs `--project <name>`; without a name it is a usage error, never a prompt. Directories are compared by real path, against the unit's directory when a unit name matched, else the leaf project's, and a relative `workspaceRoot` is refused. In a root-level Nx workspace with no discovered project at its root, no `--project` name matches that root, so `--require-cwd` refuses every run from there.
86
+
87
+ ### Exit codes
88
+
89
+ | Code | Meaning |
90
+ |------|---------|
91
+ | `0` | No file failed and nothing else failed. This includes an interrupted run, which may have left files unindexed. |
92
+ | `2` | The run finished with failed files and the rest of the index is usable. `Indexing finished with <n> of <total> file(s) failed.` is on stderr, and the next run retries those files. |
93
+ | `1` | Any other failure, including failed files in a run that was interrupted, skipped no file and stored no chunks, failed to prune, or ended with the database or the embedding backend unavailable (`Indexing backend unavailable: <cause>`). |
94
+
95
+ A failed file does not stop the run: the summary is still printed, and unless the run is interrupted the remaining files and projects are still indexed and the project graph is still produced.
96
+
83
97
  ## Monorepo support
84
98
 
85
99
  If `workspaceRoot` is an [Nx](https://nx.dev) workspace (detected by `nx.json`), `rag-index` discovers each project under `apps/`, `libs/`, and `packages/` as a **separate indexed project** rather than scanning the workspace as one blob. Cross-project imports like `@scope/shared` resolve through the workspace `tsconfig.base.json` path aliases, so `get_dependencies` / `get_dependents` report edges **across** projects.
86
100
 
87
- **Nested Nx repos.** `workspaceRoot` may also be a plain directory that *contains* an Nx repo in one of its immediate subdirectories (alongside ordinary projects). That nested repo is detected and expanded into its own projects, named `<repo>/<project>` (e.g. `micros-learning/account`), each resolved with the repo's own `tsconfig.base.json`. In the project selector the whole Nx repo appears as **one** selectable entry (so `.ragprojects` stores just `micros-learning`); selecting it indexes all its sub-projects. Ordinary sibling directories are unaffected.
101
+ **Nested Nx repos.** `workspaceRoot` may also be a plain directory that *contains* an Nx repo in one of its immediate subdirectories (alongside ordinary projects). That nested repo is detected and expanded into its own projects, named `<repo>/<project>` (e.g. `mu-monorepo/account`), each resolved with the repo's own `tsconfig.base.json`. In the project selector the whole Nx repo appears as **one** selectable entry (so `.ragprojects` stores just `mu-monorepo`); selecting it indexes all its sub-projects. Ordinary sibling directories are unaffected.
88
102
 
89
103
  No configuration needed — detection is automatic. Plain (non-Nx) workspaces keep the original behavior: each immediate subdirectory with a `package.json` is one project.
90
104
 
@@ -126,7 +140,7 @@ Add `.ragmeta.json` to a project root to enrich `list_projects` and generate a v
126
140
  "stack": "NestJS, GraphQL, PostgreSQL",
127
141
  "notes": "Uses optimistic locking — see order-service/docs/concurrency.md",
128
142
  "relations": {
129
- "consumed_by": ["crm-frontend"],
143
+ "consumed_by": ["beta-frontend"],
130
144
  "consumes": ["billing-service", "auth-service"]
131
145
  }
132
146
  }
@@ -143,7 +157,7 @@ Pass `include_graph_context: true` to `search_codebase` to see direct imports an
143
157
  **Symbol:** InvoiceService.create
144
158
  **Line:** 45
145
159
  ...code...
146
- **Relevance:** 0.32
160
+ **Distance:** 0.32
147
161
  **Imports:** src/repositories/InvoiceRepo.ts · src/dto/CreateInvoiceDto.ts
148
162
  **Used by:** src/controllers/InvoiceController.ts
149
163
  ```
@@ -210,13 +224,13 @@ rag-config edit # open config in $EDITOR; validates on save
210
224
  | `embedding.model` | `jinaai/jina-embeddings-v2-base-code` | Embedding model |
211
225
  | `embedding.dimensions` | (derived) | Vector width override; required for models whose native width exceeds 2000 (Matryoshka). Otherwise derived from model → registry → runtime probe |
212
226
  | `embedding.batchSize` | `32` | Texts per batch embedding request |
213
- | `search.similarityThreshold` | `0.6` | Max cosine distance — raise if search returns nothing |
227
+ | `search.similarityThreshold` | per model | Max cosine distance, lower = stricter. Auto per model unless set: the recommended value of `embedding.model` (`0.95` for `voyage-code-4`, `voyage-4`, `voyage-4-large` and `voyage-4-lite`, `0.6` for `jinaai/jina-embeddings-v2-base-code`), `1.0` for a model with none. An explicit value more than `0.1` from the recommendation produces a warning in the MCP server instructions and the log, and on the eval console |
214
228
  | `reranker.enabled` | context | Enable cross-encoder reranking. When absent: on with `infinity` embedding or when the `reranker` block names a server (`provider`/`baseURL`/`url`), off otherwise |
215
229
  | `reranker.provider` | `openai-compatible` | Rerank backend — `openai-compatible` `/rerank` (Infinity, vLLM) or `voyage` (Voyage AI, needs `reranker.apiKey`) |
216
230
  | `reranker.baseURL` | per-provider | Rerank server base URL — `embedding.baseURL` for `openai-compatible` (Infinity serves embed + rerank from one server), `https://api.voyageai.com/v1` for `voyage` |
217
231
  | `reranker.model` | per-provider | Reranker model name — `mixedbread-ai/mxbai-rerank-xsmall-v1` for `openai-compatible`, `rerank-3` for `voyage` |
218
- | `reranker.overfetch` | `5` | Candidate multiplier before reranking (`limit × N`) |
219
- | `reranker.candidateFloor` | `80` | Minimum candidate pool fed to the reranker regardless of `limit` — `fetchLimit = min(max(limit × overfetch, candidateFloor), 200)`. Raise on GPU/fast hosts for better recall; lower on weak CPU to cut latency |
232
+ | `reranker.overfetch` | `5` | Candidate multiplier before reranking (`limit × N`); also sizes the pool of a search without reranking whose `per_file` is below `limit` |
233
+ | `reranker.candidateFloor` | `80` | Minimum candidate pool fed to the reranker regardless of `limit` — `fetchLimit = min(max(limit × overfetch, candidateFloor), 200)`. The same pool serves a search without reranking whose `per_file` is below `limit`. Raise on GPU/fast hosts for better recall; lower on weak CPU to cut latency |
220
234
  | `indexing.chunkSize` | `1500` | Max characters per chunk |
221
235
  | `indexing.chunkOverlap` | `200` | Overlap between chunks |
222
236
  | `indexing.embedBatchSize` | `5` | Parallel embedding requests per batch |
@@ -296,11 +310,11 @@ The same "already running?" probe applies everywhere: a reachable backend defaul
296
310
 
297
311
  ### Reranker with a cloud or external embedder
298
312
 
299
- With a cloud embedder (OpenAI, Voyage, Azure) and no `reranker` block, reranking is **off** — those endpoints don't serve `/rerank`. `rag-setup` asks for a reranker after the embedding step for every provider (`voyage`, `openai-compatible`, `none`, plus `infinity` for an Infinity embedder) and always writes an explicit `reranker` block. To configure one by hand:
313
+ With a cloud embedder (OpenAI, Voyage, Azure) and no `reranker` block, reranking is **off** — those endpoints don't serve `/rerank`. `rag-setup` configures a reranker after the embedding step for every provider and always writes an explicit `reranker` block: a Voyage embedder gets one question, `Enable Voyage reranker?`, and shares its `VOYAGE_API_KEY`; other embedders choose `voyage` (own key, `VOYAGE_RERANK_API_KEY`), `openai-compatible` or `none`, plus `infinity` for an Infinity embedder. To configure one by hand:
300
314
 
301
- These recipes edit whatever `reranker` block is already stored, so each one sets `reranker.enabled true` (an explicit `false` from `rag-setup`'s `none` choice would otherwise win) and clears the keys that would point the requests at the old server.
315
+ These recipes edit whatever `reranker` block is already stored, so each one sets `reranker.enabled true` (an explicit `false` from `rag-setup`'s `none` choice, or the `n` answer to a Voyage embedder's `Enable Voyage reranker?`, would otherwise win) and clears the keys that would point the requests at the old server.
302
316
 
303
- **Voyage reranker** (works with any embedder; its key is separate from the embedding key):
317
+ **Voyage reranker** (works with any embedder; with a Voyage embedder `reranker.apiKey` can point at the same `env:VOYAGE_API_KEY` instead of a separate key):
304
318
 
305
319
  ```bash
306
320
  echo 'VOYAGE_RERANK_API_KEY=pa-…' >> ~/.config/codesearch-mcp/.env
@@ -338,7 +352,7 @@ Switching the embedding model, provider, or dimension is applied by **re-indexin
338
352
  When reranking is enabled, `search_codebase` fetches a **candidate pool** by cosine distance, reranks it with the cross-encoder, and returns the top `limit`. Three knobs and one relationship govern how good the results are and how long they take:
339
353
 
340
354
  - **Pool size** — `fetchLimit = min(max(limit × overfetch, candidateFloor), 200)`. `candidateFloor` (default `80`) guarantees the reranker a large enough menu even at the default `limit = 5`; without it a small `limit` starves the reranker (only `5 × 5 = 25` candidates) and its best matches never enter the window.
341
- - **Threshold ↔ window** — raising `search.similarityThreshold` lets *more* chunks under the bar, so the pool must be wide enough to reach them; a strict threshold with a small pool silently truncates recall. Widen `candidateFloor`/`overfetch` when you raise the threshold.
355
+ - **Threshold ↔ window** — distance scales differ per embedding model, so an unset `search.similarityThreshold` follows the model. Raising the threshold lets *more* chunks under the bar, so the pool must be wide enough to reach them; when more chunks are under the threshold than the window takes, recall is silently truncated. Widen `candidateFloor`/`overfetch` when you raise the threshold.
342
356
  - **Latency** — the reranker cost grows with pool size (on CPU, tens of seconds; a rough guide is a large fixed cost plus a per-candidate increment). Bigger pool = better recall, slower query. GPU/fast hosts can raise `candidateFloor`; ultra-constrained hosts lower it.
343
357
 
344
358
  The pool is reachable on large projects too. The HNSW index scan stops after `hnsw.ef_search` rows (PostgreSQL default 40) and only then applies the `project` filter and the threshold, so a scoped search on a project of a few thousand chunks or more (measured from ~5.8 k; the crossover depends on table statistics) — or an unscoped one — used to get at most 40 candidates, often far fewer. Each search now sets `hnsw.ef_search` to the requested row count (at least 40, at most 1000) and, on pgvector ≥ 0.8, `hnsw.iterative_scan = relaxed_order` for its own query, so `candidateFloor` / `overfetch` size the pool the reranker actually receives, unless the iterative scan stops at `hnsw.max_scan_tuples` (20 000) first. On an older pgvector only `ef_search` is raised, and one `warn` line per process names the version — or `unknown` if the first version read failed, with no later line naming it. Like the Voyage warning above, it is visible only at `logging.level` `warn` or lower.
@@ -351,7 +365,7 @@ rag-config set logging.level info
351
365
 
352
366
  Each search then writes one `search metrics` line with, among others: `fetchLimit`, `candidatesReturned`, `poolSaturated`, `maxCosineRankSurvived`, `distinctFilesInResult`, `topFileShare`, `efSearch`/`iterativeScan` (the HNSW settings sent with the query), and `embedMs`/`rerankMs`/`dbMs`/`totalMs`. Read them as:
353
367
 
354
- - **`poolSaturated: true`** — more chunks were under the threshold than the window took → recall is being truncated; raise `candidateFloor`/`overfetch`.
368
+ - **`poolSaturated: true`** — more chunks were under the threshold than the window took → recall is being truncated; raise `candidateFloor`/`overfetch`. Under the `1.0` fallback (a model with no recommended value) nearly every chunk is under the threshold, so in a scope holding more chunks than the window this stays true at any pool size, and every search that reranks sends the full pool to the reranker; set `search.similarityThreshold` instead of widening the pool.
355
369
  - **`maxCosineRankSurvived`** — the deepest cosine rank that survived reranking. If it stays well below `fetchLimit`, the pool is bigger than needed → shrink it to save latency; if it approaches `fetchLimit`, widen.
356
370
 
357
371
  ## Index consistency
@@ -369,13 +383,13 @@ A failure pruning one project does not block the others or stop indexing of the
369
383
 
370
384
  ## Architecture
371
385
 
372
- **Stack:** TypeScript on Node.js 18+ · PostgreSQL 17 + pgvector (HNSW, cosine distance) via Drizzle ORM · ts-morph for AST/symbol/call-graph extraction · `@langchain/textsplitters` for chunk fallback · MCP SDK over stdio · Zod v4 for config validation and JSON-Schema generation · pino + pino-roll for logging · plain `node:http` dashboard with vis-network · Vitest · Docker Compose for Postgres + Infinity.
386
+ **Stack:** TypeScript on Node.js 22.13+ · PostgreSQL 17 + pgvector (HNSW, cosine distance) via Drizzle ORM · ts-morph for AST/symbol/call-graph extraction · `@langchain/textsplitters` for chunk fallback · MCP SDK over stdio · Zod v4 for config validation and JSON-Schema generation · pino + pino-roll for logging · plain `node:http` dashboard with vis-network · Vitest · Docker Compose for Postgres + Infinity.
373
387
 
374
388
  **Design:**
375
389
  - **Composition root + manual DI** — `src/container.ts` builds every service once and wires it through constructors; no module-level singletons, no DI framework.
376
- - **Dependency inversion** — services depend on interfaces (`IEmbedder`, `IDatabase`, `IAstParser`, `IChunker`, `ISearcher`, `IProjectDiscovery`, `IMonorepoResolver`, `IMcpToolHandler`), so providers (OpenAI-compatible/Voyage/Infinity — Ollama via `openai-compatible`), chunking strategy (`ast`/`character`), and monorepo resolvers are swappable.
390
+ - **Dependency inversion** — services depend on interfaces (`IEmbedder`, `IDatabase`, `IAstParser`, `IChunker`, the role interfaces of the search and path parts (`IChunkSearch`, `IToolPaths`, `ISymbolLookup`, …), `IProjectDiscovery`, `IMonorepoResolver`, `IMcpToolHandler`), so providers (OpenAI-compatible/Voyage/Infinity — Ollama via `openai-compatible`), chunking strategy (`ast`/`character`), and monorepo resolvers are swappable.
377
391
  - **Command pattern** — each CLI mode (`rag-index`, `rag-mcp`, `rag-config`, `rag-web`) is its own `ICommand`; `app.ts` only dispatches.
378
- - **Registry / Open-Closed** — MCP tools are `IMcpToolHandler` implementations registered in a `Map`; adding a tool never touches `McpServer`.
392
+ - **Registry / Open-Closed** — MCP tools extend `McpTool`, which implements `IMcpToolHandler`, and are registered in a `Map` by `McpServer.createTools()`; adding a tool means extending `McpTool` and registering it there, and the dispatch logic never changes.
379
393
  - **Fallback chains** — vector dimension: config override → model registry → runtime probe; `find_symbol`/`get_dependents`: indexed table → grep; failed rerank → vector order.
380
394
  - **Incremental indexing** — mtime fast path, SHA-256 fallback for clones/transfers, AST-aligned chunk boundaries so declarations aren't split arbitrarily.
381
395
  - **Index/model consistency guard** — `index_meta` tracks the embedding model that built the index; search refuses with a clear re-index message on mismatch instead of returning noise.
@@ -384,4 +398,6 @@ Full internals (per-file responsibilities, every pattern with file:line referenc
384
398
 
385
399
  ## License
386
400
 
387
- MIT
401
+ Copyright (C) 2026 Oleksandr Antoniuk.
402
+
403
+ Licensed under the [GNU Affero General Public License v3.0 only](LICENSE) (`AGPL-3.0-only`). Versions up to and including `0.3.0-alpha` were released under the MIT License and remain available under it.
package/dist/app.js CHANGED
@@ -37,6 +37,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const help_1 = require("./src/help");
40
+ const index_args_1 = require("./src/index-args");
40
41
  const container_1 = require("./src/container");
41
42
  const config_1 = require("./src/config");
42
43
  const config_store_1 = require("./src/config-store");
@@ -110,25 +111,17 @@ async function main() {
110
111
  }
111
112
  }
112
113
  else if (mode === 'index') {
113
- const boolFlags = new Set(['--reindex', '--select']);
114
- const unknown = [];
115
- for (let i = 0; i < effectiveArgs.length; i++) {
116
- const a = effectiveArgs[i];
117
- if (boolFlags.has(a))
118
- continue;
119
- if (a === '--project') {
120
- const next = effectiveArgs[i + 1];
121
- if (next !== undefined && !next.startsWith('-'))
122
- i++;
123
- continue;
124
- }
125
- unknown.push(a);
126
- }
127
- if (unknown.length > 0) {
128
- console.error(`Unexpected argument: ${unknown[0]}\n`);
114
+ const unexpected = (0, index_args_1.findUnexpectedIndexArg)(effectiveArgs);
115
+ if (unexpected !== undefined) {
116
+ console.error(`Unexpected argument: ${unexpected}\n`);
129
117
  (0, help_1.printHelp)(isBinary ? argv1 : 'rag-index');
130
118
  process.exit(1);
131
119
  }
120
+ const cwdCause = (0, index_args_1.unreadableCwdCause)(effectiveArgs);
121
+ if (cwdCause !== undefined) {
122
+ console.error(`\n${cwdCause}`);
123
+ process.exit(1);
124
+ }
132
125
  }
133
126
  else if (mode === 'mcp' || mode === 'setup') {
134
127
  if (effectiveArgs.length > 0) {
@@ -183,10 +176,10 @@ async function main() {
183
176
  process.exit(1);
184
177
  }
185
178
  const factories = {
186
- index: () => new IndexCommand_1.IndexCommand(container, argv1),
187
- eval: () => new EvalCommand_1.EvalCommand(container, argv1),
188
- mcp: () => new McpCommand_1.McpCommand(container),
189
- web: () => new WebCommand_1.WebCommand(container, effectiveArgs),
179
+ index: () => new IndexCommand_1.IndexCommand(container.db, container.embedder, container.indexer, container.search.dependencies, container.discovery, container.config, argv1),
180
+ eval: () => new EvalCommand_1.EvalCommand(container.db, container.search.chunks, container.search.catalog, container.config, argv1),
181
+ mcp: () => new McpCommand_1.McpCommand(container.readiness, container.mcp, container.logger),
182
+ web: () => new WebCommand_1.WebCommand(container.db, container.embedder, container.search.catalog, container.search.dependencies, container.config, container.logger, effectiveArgs),
190
183
  };
191
184
  const factory = factories[mode];
192
185
  if (!factory) {
@@ -0,0 +1,8 @@
1
+ import { type SourceFile } from 'ts-morph';
2
+ import type { CallEdge } from '../interfaces/IAstParser';
3
+ import type { IIndexedNodeResolver } from '../interfaces';
4
+ export declare class CallExtractor {
5
+ private readonly resolver;
6
+ constructor(resolver: IIndexedNodeResolver);
7
+ extractCalls(sourceFile: SourceFile, filePath: string, serviceName: string): CallEdge[];
8
+ }
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CallExtractor = void 0;
4
+ const ts_morph_1 = require("ts-morph");
5
+ class CallExtractor {
6
+ resolver;
7
+ constructor(resolver) {
8
+ this.resolver = resolver;
9
+ }
10
+ extractCalls(sourceFile, filePath, serviceName) {
11
+ const calls = [];
12
+ for (const callExpr of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)) {
13
+ const expr = callExpr.getExpression();
14
+ let calleeName = null;
15
+ let calleeFile = null;
16
+ let calleeDecl;
17
+ try {
18
+ if (ts_morph_1.Node.isPropertyAccessExpression(expr)) {
19
+ calleeName = expr.getName();
20
+ const sym = expr.getNameNode().getSymbol();
21
+ const decl = sym?.getDeclarations()?.[0];
22
+ const declPath = decl?.getSourceFile().getFilePath();
23
+ if (declPath && !declPath.includes('/node_modules/')) {
24
+ calleeFile = declPath;
25
+ calleeDecl = decl;
26
+ }
27
+ }
28
+ else if (ts_morph_1.Node.isIdentifier(expr)) {
29
+ calleeName = expr.getText();
30
+ const sym = expr.getSymbol();
31
+ if (sym) {
32
+ const firstDecl = sym.getDeclarations()?.[0];
33
+ const resolved = firstDecl && ts_morph_1.Node.isImportSpecifier(firstDecl)
34
+ ? (sym.getAliasedSymbol() ?? sym)
35
+ : sym;
36
+ const decl = resolved.getDeclarations()?.[0];
37
+ const declPath = decl?.getSourceFile().getFilePath();
38
+ if (declPath && !declPath.includes('/node_modules/')) {
39
+ calleeFile = declPath;
40
+ calleeDecl = decl;
41
+ }
42
+ }
43
+ }
44
+ }
45
+ catch {
46
+ }
47
+ if (!calleeName || !calleeFile)
48
+ continue;
49
+ const described = calleeDecl ? this.resolver.describeIndexedNode(calleeDecl) : null;
50
+ const calleeSymbol = described ? this.resolver.qualifiedName(described) : null;
51
+ const fromSymbol = this.resolver.findFromSymbol(callExpr);
52
+ const pos = sourceFile.compilerNode
53
+ .getLineAndCharacterOfPosition(callExpr.getStart());
54
+ calls.push({
55
+ project: serviceName,
56
+ fromFile: filePath,
57
+ fromSymbol,
58
+ calleeName,
59
+ calleeFile,
60
+ calleeSymbol,
61
+ line: pos.line + 1,
62
+ col: pos.character + 1,
63
+ });
64
+ }
65
+ return calls;
66
+ }
67
+ }
68
+ exports.CallExtractor = CallExtractor;
@@ -0,0 +1,22 @@
1
+ import { type SourceFile } from 'ts-morph';
2
+ import type { SymbolEntry } from '../interfaces/IAstParser';
3
+ import type { ILogger, IIndexedNodeResolver } from '../interfaces';
4
+ import type { DeclarationCandidate } from '../declarations/DeclarationTreeBuilder';
5
+ import { type ResolveDeclaration } from './node-helpers';
6
+ export declare class GraphqlNameExtractor {
7
+ private readonly resolver;
8
+ private readonly logger;
9
+ private readonly resolveDeclaration;
10
+ constructor(resolver: IIndexedNodeResolver, logger: ILogger, resolveDeclaration?: ResolveDeclaration);
11
+ private buildGraphqlImportMap;
12
+ private recognizedDecorator;
13
+ private deriveGraphqlName;
14
+ private logGraphqlSkip;
15
+ private declarationNameFor;
16
+ private classGraphqlTypeName;
17
+ private resolveResolverParentType;
18
+ private extractClassTypeRows;
19
+ private extractResolveFieldRow;
20
+ private extractMethodOperationRows;
21
+ extractGraphqlNames(sourceFile: SourceFile, filePath: string, serviceName: string, ownRows: DeclarationCandidate[]): SymbolEntry[];
22
+ }
@@ -0,0 +1,240 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GraphqlNameExtractor = void 0;
4
+ const ts_morph_1 = require("ts-morph");
5
+ const node_helpers_1 = require("./node-helpers");
6
+ const graphql_grammar_1 = require("./graphql-grammar");
7
+ class GraphqlNameExtractor {
8
+ resolver;
9
+ logger;
10
+ resolveDeclaration;
11
+ constructor(resolver, logger, resolveDeclaration = node_helpers_1.defaultResolveDeclaration) {
12
+ this.resolver = resolver;
13
+ this.logger = logger;
14
+ this.resolveDeclaration = resolveDeclaration;
15
+ }
16
+ buildGraphqlImportMap(sourceFile) {
17
+ const map = new Map();
18
+ for (const decl of sourceFile.getImportDeclarations()) {
19
+ if (decl.getModuleSpecifierValue() !== graphql_grammar_1.GRAPHQL_MODULE)
20
+ continue;
21
+ for (const named of decl.getNamedImports()) {
22
+ const exportedName = named.getName();
23
+ const localName = named.getAliasNode()?.getText() ?? exportedName;
24
+ map.set(localName, exportedName);
25
+ }
26
+ }
27
+ return map;
28
+ }
29
+ recognizedDecorator(dec, importMap) {
30
+ const idNode = (0, graphql_grammar_1.decoratorIdentifier)(dec);
31
+ if (!idNode)
32
+ return null;
33
+ const exported = importMap.get(idNode.getText());
34
+ if (!exported)
35
+ return null;
36
+ const expr = dec.getExpression();
37
+ const args = ts_morph_1.Node.isCallExpression(expr) ? expr.getArguments() : [];
38
+ return { name: exported, args };
39
+ }
40
+ deriveGraphqlName(args, allowOptionsName, fallbackName) {
41
+ if (args.length > 0) {
42
+ const first = args[0];
43
+ const { inner, wrapped } = (0, graphql_grammar_1.peekThroughWrapper)(first);
44
+ const literal = (0, graphql_grammar_1.literalStringValue)(inner);
45
+ if (literal !== null) {
46
+ if (wrapped)
47
+ return { ok: false, reason: 'non-literal-name' };
48
+ return (0, graphql_grammar_1.checkGraphqlGrammar)(literal);
49
+ }
50
+ const isTypeFnOrOptions = ts_morph_1.Node.isArrowFunction(inner) || ts_morph_1.Node.isFunctionExpression(inner) || ts_morph_1.Node.isObjectLiteralExpression(inner);
51
+ if (!isTypeFnOrOptions)
52
+ return { ok: false, reason: 'non-literal-name' };
53
+ }
54
+ if (allowOptionsName) {
55
+ for (const arg of args) {
56
+ const { inner, wrapped } = (0, graphql_grammar_1.peekThroughWrapper)(arg);
57
+ if (!ts_morph_1.Node.isObjectLiteralExpression(inner))
58
+ continue;
59
+ if (wrapped)
60
+ return { ok: false, reason: 'non-literal-name' };
61
+ const nameProp = (0, graphql_grammar_1.findNameProperty)(inner);
62
+ if (!nameProp)
63
+ continue;
64
+ if (ts_morph_1.Node.isShorthandPropertyAssignment(nameProp))
65
+ return { ok: false, reason: 'non-literal-name' };
66
+ const init = nameProp.getInitializer();
67
+ const value = init ? (0, graphql_grammar_1.literalStringValue)(init) : null;
68
+ if (value !== null)
69
+ return (0, graphql_grammar_1.checkGraphqlGrammar)(value);
70
+ return { ok: false, reason: 'non-literal-name' };
71
+ }
72
+ }
73
+ return (0, graphql_grammar_1.checkGraphqlGrammar)(fallbackName);
74
+ }
75
+ logGraphqlSkip(filePath, declaration, decorator, reason, value) {
76
+ const fields = { filePath, declaration, decorator, reason };
77
+ if (value !== undefined)
78
+ fields.value = value;
79
+ this.logger.debug('AstParser: graphql row skipped', fields);
80
+ }
81
+ declarationNameFor(node, fallback) {
82
+ const described = this.resolver.describeIndexedNode(node);
83
+ return described ? this.resolver.qualifiedName(described) : fallback;
84
+ }
85
+ classGraphqlTypeName(declClass) {
86
+ const declImportMap = this.buildGraphqlImportMap(declClass.getSourceFile());
87
+ const decorators = declClass.getDecorators();
88
+ const hasAnyTypeSpelledDecorator = decorators.some((d) => {
89
+ const idNode = (0, graphql_grammar_1.decoratorIdentifier)(d);
90
+ const text = idNode?.getText();
91
+ return text === 'ObjectType' || text === 'InterfaceType';
92
+ });
93
+ const recognized = decorators
94
+ .map(d => this.recognizedDecorator(d, declImportMap))
95
+ .find(r => r?.name === 'ObjectType')
96
+ ?? decorators.map(d => this.recognizedDecorator(d, declImportMap)).find(r => r?.name === 'InterfaceType');
97
+ if (recognized) {
98
+ const fallbackName = declClass.getName() ?? (declClass.isDefaultExport() ? 'default' : '');
99
+ const nameResult = this.deriveGraphqlName(recognized.args, false, fallbackName);
100
+ return nameResult.ok ? { ok: true, name: nameResult.name } : { ok: false };
101
+ }
102
+ if (hasAnyTypeSpelledDecorator)
103
+ return { ok: false };
104
+ const name = declClass.getName();
105
+ if (!name)
106
+ return { ok: false };
107
+ return graphql_grammar_1.GRAPHQL_NAME_RE.test(name) ? { ok: true, name } : { ok: false, value: name };
108
+ }
109
+ resolveResolverParentType(cls, importMap) {
110
+ const resolverDec = cls.getDecorators()
111
+ .map(d => this.recognizedDecorator(d, importMap))
112
+ .find(r => r?.name === 'Resolver');
113
+ if (!resolverDec || resolverDec.args.length === 0)
114
+ return { ok: false };
115
+ const argNode = resolverDec.args[0];
116
+ const literal = (0, graphql_grammar_1.literalStringValue)(argNode);
117
+ if (literal !== null) {
118
+ return graphql_grammar_1.GRAPHQL_NAME_RE.test(literal) ? { ok: true, name: literal } : { ok: false, value: literal };
119
+ }
120
+ const identifier = (0, graphql_grammar_1.extractResolverIdentifier)(argNode);
121
+ if (!identifier)
122
+ return { ok: false };
123
+ const resolve = this.resolveDeclaration;
124
+ const declaration = resolve(identifier);
125
+ if (!declaration || !ts_morph_1.Node.isClassDeclaration(declaration))
126
+ return { ok: false };
127
+ return this.classGraphqlTypeName(declaration);
128
+ }
129
+ extractClassTypeRows(cls, importMap, ownByDeclNode, filePath, serviceName, entries) {
130
+ const own = ownByDeclNode.get(cls);
131
+ const declaration = this.declarationNameFor(cls, own?.entry.name ?? cls.getName() ?? 'default');
132
+ const seenNames = new Set();
133
+ for (const dec of cls.getDecorators()) {
134
+ const rec = this.recognizedDecorator(dec, importMap);
135
+ if (!rec || !graphql_grammar_1.GRAPHQL_TYPE_DECORATORS.has(rec.name))
136
+ continue;
137
+ if ((0, graphql_grammar_1.isAbstractGraphqlType)(rec.args)) {
138
+ this.logGraphqlSkip(filePath, declaration, rec.name, 'abstract-type');
139
+ continue;
140
+ }
141
+ if (!own) {
142
+ this.logGraphqlSkip(filePath, declaration, rec.name, 'no-symbol-row');
143
+ continue;
144
+ }
145
+ const nameResult = this.deriveGraphqlName(rec.args, false, own.entry.name);
146
+ if (!nameResult.ok) {
147
+ this.logGraphqlSkip(filePath, declaration, rec.name, nameResult.reason, nameResult.reason === 'invalid-name' ? nameResult.value : undefined);
148
+ continue;
149
+ }
150
+ if (seenNames.has(nameResult.name)) {
151
+ this.logGraphqlSkip(filePath, declaration, rec.name, 'duplicate', own.entry.name);
152
+ continue;
153
+ }
154
+ seenNames.add(nameResult.name);
155
+ entries.push({
156
+ project: serviceName, filePath, name: nameResult.name, kind: 'graphql_type',
157
+ signature: own.entry.signature, line: own.entry.line, endLine: own.entry.endLine, col: own.entry.col,
158
+ container: null, isStatic: false, target: declaration,
159
+ });
160
+ }
161
+ }
162
+ extractResolveFieldRow(method, rec, own, declaration, importMap, filePath, serviceName, entries) {
163
+ if (!own) {
164
+ this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'no-symbol-row');
165
+ return;
166
+ }
167
+ const cls = method.getParent();
168
+ if (!cls || !ts_morph_1.Node.isClassDeclaration(cls)) {
169
+ this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'unresolvable-parent');
170
+ return;
171
+ }
172
+ const parent = this.resolveResolverParentType(cls, importMap);
173
+ if (!parent.ok) {
174
+ this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'unresolvable-parent', parent.value);
175
+ return;
176
+ }
177
+ const nameResult = this.deriveGraphqlName(rec.args, true, own.entry.name);
178
+ if (!nameResult.ok) {
179
+ this.logGraphqlSkip(filePath, declaration, 'ResolveField', nameResult.reason, nameResult.reason === 'invalid-name' ? nameResult.value : undefined);
180
+ return;
181
+ }
182
+ entries.push({
183
+ project: serviceName, filePath, name: nameResult.name, kind: 'graphql_field',
184
+ signature: own.entry.signature, line: own.entry.line, endLine: own.entry.endLine, col: own.entry.col,
185
+ container: parent.name, isStatic: own.entry.isStatic, target: declaration,
186
+ });
187
+ }
188
+ extractMethodOperationRows(method, importMap, ownByDeclNode, filePath, serviceName, entries) {
189
+ const own = ownByDeclNode.get(method);
190
+ const declaration = this.declarationNameFor(method, own?.entry.name ?? (0, node_helpers_1.memberName)(method.getNameNode()) ?? '<anonymous>');
191
+ for (const dec of method.getDecorators()) {
192
+ const rec = this.recognizedDecorator(dec, importMap);
193
+ if (!rec)
194
+ continue;
195
+ if (rec.name === 'ResolveField') {
196
+ try {
197
+ this.extractResolveFieldRow(method, rec, own, declaration, importMap, filePath, serviceName, entries);
198
+ }
199
+ catch (err) {
200
+ this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'unresolvable-parent');
201
+ }
202
+ continue;
203
+ }
204
+ const kind = graphql_grammar_1.GRAPHQL_OPERATION_KIND[rec.name];
205
+ if (!kind)
206
+ continue;
207
+ if (!own) {
208
+ this.logGraphqlSkip(filePath, declaration, rec.name, 'no-symbol-row');
209
+ continue;
210
+ }
211
+ const nameResult = this.deriveGraphqlName(rec.args, true, own.entry.name);
212
+ if (!nameResult.ok) {
213
+ this.logGraphqlSkip(filePath, declaration, rec.name, nameResult.reason, nameResult.reason === 'invalid-name' ? nameResult.value : undefined);
214
+ continue;
215
+ }
216
+ entries.push({
217
+ project: serviceName, filePath, name: nameResult.name, kind,
218
+ signature: own.entry.signature, line: own.entry.line, endLine: own.entry.endLine, col: own.entry.col,
219
+ container: rec.name, isStatic: own.entry.isStatic, target: declaration,
220
+ });
221
+ }
222
+ }
223
+ extractGraphqlNames(sourceFile, filePath, serviceName, ownRows) {
224
+ const importMap = this.buildGraphqlImportMap(sourceFile);
225
+ if (importMap.size === 0)
226
+ return [];
227
+ const ownByDeclNode = new Map();
228
+ for (const c of ownRows)
229
+ ownByDeclNode.set(c.declNode, c);
230
+ const entries = [];
231
+ for (const cls of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.ClassDeclaration)) {
232
+ this.extractClassTypeRows(cls, importMap, ownByDeclNode, filePath, serviceName, entries);
233
+ }
234
+ for (const method of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.MethodDeclaration)) {
235
+ this.extractMethodOperationRows(method, importMap, ownByDeclNode, filePath, serviceName, entries);
236
+ }
237
+ return entries;
238
+ }
239
+ }
240
+ exports.GraphqlNameExtractor = GraphqlNameExtractor;
@@ -0,0 +1,11 @@
1
+ import { type SourceFile } from 'ts-morph';
2
+ import type { DepEdge } from '../interfaces/IAstParser';
3
+ import type { ILogger } from '../interfaces';
4
+ export declare class ImportEdgeExtractor {
5
+ private readonly logger;
6
+ constructor(logger: ILogger);
7
+ extractEdges(sourceFile: SourceFile, filePath: string, serviceName: string): DepEdge[];
8
+ private resolveSideEffectModuleSpecifier;
9
+ private resolveFromSymbol;
10
+ private buildUsageMap;
11
+ }