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.
- package/CHANGELOG.md +59 -6
- package/LICENSE +661 -21
- package/README.md +33 -17
- package/dist/app.js +13 -20
- package/dist/src/ast/CallExtractor.d.ts +8 -0
- package/dist/src/ast/CallExtractor.js +68 -0
- package/dist/src/ast/GraphqlNameExtractor.d.ts +22 -0
- package/dist/src/ast/GraphqlNameExtractor.js +240 -0
- package/dist/src/ast/ImportEdgeExtractor.d.ts +11 -0
- package/dist/src/ast/ImportEdgeExtractor.js +129 -0
- package/dist/src/ast/IndexedNodeResolver.d.ts +14 -0
- package/dist/src/ast/IndexedNodeResolver.js +217 -0
- package/dist/src/ast/MemberExtractor.d.ts +8 -0
- package/dist/src/ast/MemberExtractor.js +129 -0
- package/dist/src/ast/TopLevelSymbolExtractor.d.ts +5 -0
- package/dist/src/ast/TopLevelSymbolExtractor.js +86 -0
- package/dist/src/ast/graphql-grammar.d.ts +24 -0
- package/dist/src/ast/graphql-grammar.js +93 -0
- package/dist/src/ast/node-helpers.d.ts +11 -0
- package/dist/src/ast/node-helpers.js +55 -0
- package/dist/src/commands/EvalCommand.d.ts +7 -3
- package/dist/src/commands/EvalCommand.js +36 -158
- package/dist/src/commands/IndexCommand.d.ts +10 -4
- package/dist/src/commands/IndexCommand.js +52 -124
- package/dist/src/commands/McpCommand.d.ts +6 -3
- package/dist/src/commands/McpCommand.js +19 -14
- package/dist/src/commands/WebCommand.d.ts +9 -3
- package/dist/src/commands/WebCommand.js +17 -8
- package/dist/src/commands/eval/baseline-comparison.d.ts +6 -0
- package/dist/src/commands/eval/baseline-comparison.js +54 -0
- package/dist/src/commands/eval/eval-args.d.ts +6 -0
- package/dist/src/commands/eval/eval-args.js +71 -0
- package/dist/src/commands/eval/eval-corpora.d.ts +6 -0
- package/dist/src/commands/eval/eval-corpora.js +76 -0
- package/dist/src/commands/eval/eval-metadata.d.ts +17 -0
- package/dist/src/commands/eval/eval-metadata.js +75 -0
- package/dist/src/commands/eval/index-inspector.d.ts +3 -0
- package/dist/src/commands/eval/index-inspector.js +21 -0
- package/dist/src/commands/eval/staged-search.d.ts +3 -0
- package/dist/src/commands/eval/staged-search.js +20 -0
- package/dist/src/commands/eval/suite-loader.d.ts +8 -0
- package/dist/src/commands/eval/suite-loader.js +52 -0
- package/dist/src/commands/index/backend-probe.d.ts +2 -0
- package/dist/src/commands/index/backend-probe.js +15 -0
- package/dist/src/commands/index/embedding-setup.d.ts +3 -0
- package/dist/src/commands/index/embedding-setup.js +16 -0
- package/dist/src/commands/index/index-summary.d.ts +2 -0
- package/dist/src/commands/index/index-summary.js +22 -0
- package/dist/src/commands/index/legacy-ollama-guard.d.ts +3 -0
- package/dist/src/commands/index/legacy-ollama-guard.js +23 -0
- package/dist/src/commands/index/prune-step.d.ts +3 -0
- package/dist/src/commands/index/prune-step.js +22 -0
- package/dist/src/commands/index/scan-targets.d.ts +8 -0
- package/dist/src/commands/index/scan-targets.js +107 -0
- package/dist/src/config-store.d.ts +0 -1
- package/dist/src/config-store.js +3 -4
- package/dist/src/config.d.ts +1 -1
- package/dist/src/config.js +1 -1
- package/dist/src/constants.d.ts +15 -0
- package/dist/src/constants.js +34 -6
- package/dist/src/container.d.ts +4 -2
- package/dist/src/container.js +7 -5
- package/dist/src/db/drizzle-types.d.ts +7 -0
- package/dist/src/db/index.d.ts +1 -0
- package/dist/src/db/schema-steps.d.ts +8 -0
- package/dist/src/db/schema-steps.js +105 -0
- package/dist/src/declarations/DeclarationTree.d.ts +1 -1
- package/dist/src/eval-results.js +16 -2
- package/dist/src/eval-vector-index.d.ts +5 -0
- package/dist/src/eval-vector-index.js +60 -0
- package/dist/src/help.js +9 -1
- package/dist/src/index-args.d.ts +13 -0
- package/dist/src/index-args.js +48 -0
- package/dist/src/indexing/AnalysisBatcher.d.ts +12 -0
- package/dist/src/indexing/AnalysisBatcher.js +28 -0
- package/dist/src/indexing/ChunkEmbedder.d.ts +24 -0
- package/dist/src/indexing/ChunkEmbedder.js +37 -0
- package/dist/src/indexing/ConsoleScanReporter.d.ts +10 -0
- package/dist/src/indexing/ConsoleScanReporter.js +73 -0
- package/dist/src/indexing/EmbeddingSchemaPreparer.d.ts +14 -0
- package/dist/src/indexing/EmbeddingSchemaPreparer.js +66 -0
- package/dist/src/indexing/FileIngestor.d.ts +22 -0
- package/dist/src/indexing/FileIngestor.js +116 -0
- package/dist/src/indexing/IndexStore.d.ts +24 -0
- package/dist/src/indexing/IndexStore.js +69 -0
- package/dist/src/indexing/StaleFilePruner.d.ts +8 -0
- package/dist/src/indexing/StaleFilePruner.js +55 -0
- package/dist/src/indexing/analysis-rows.d.ts +4 -0
- package/dist/src/indexing/analysis-rows.js +65 -0
- package/dist/src/indexing/project-notices.d.ts +2 -0
- package/dist/src/indexing/project-notices.js +63 -0
- package/dist/src/interfaces/IAstParser.d.ts +2 -1
- package/dist/src/interfaces/ICallGraph.d.ts +28 -0
- package/dist/src/interfaces/ICallGraph.js +2 -0
- package/dist/src/interfaces/IChunkSearch.d.ts +42 -0
- package/dist/src/interfaces/IChunkSearch.js +2 -0
- package/dist/src/interfaces/IDatabase.d.ts +6 -2
- package/dist/src/interfaces/IDependencyGraph.d.ts +24 -0
- package/dist/src/interfaces/IDependencyGraph.js +2 -0
- package/dist/src/interfaces/IIndexIdentityGuard.d.ts +3 -0
- package/dist/src/interfaces/IIndexIdentityGuard.js +2 -0
- package/dist/src/interfaces/IIndexedNodeResolver.d.ts +10 -0
- package/dist/src/interfaces/IIndexedNodeResolver.js +2 -0
- package/dist/src/interfaces/IMcpReadiness.d.ts +12 -0
- package/dist/src/interfaces/IMcpReadiness.js +2 -0
- package/dist/src/interfaces/IProjectCatalog.d.ts +13 -0
- package/dist/src/interfaces/IProjectCatalog.js +2 -0
- package/dist/src/interfaces/IProjectRoots.d.ts +3 -0
- package/dist/src/interfaces/IProjectRoots.js +2 -0
- package/dist/src/interfaces/IRetrievalEvaluator.d.ts +21 -2
- package/dist/src/interfaces/IScanReporter.d.ts +13 -0
- package/dist/src/interfaces/IScanReporter.js +2 -0
- package/dist/src/interfaces/ISymbolLookup.d.ts +49 -0
- package/dist/src/interfaces/ISymbolLookup.js +2 -0
- package/dist/src/interfaces/IToolPaths.d.ts +62 -0
- package/dist/src/interfaces/IToolPaths.js +2 -0
- package/dist/src/interfaces/IWorkspaceFiles.d.ts +28 -0
- package/dist/src/interfaces/IWorkspaceFiles.js +2 -0
- package/dist/src/interfaces/SymbolKind.d.ts +1 -0
- package/dist/src/interfaces/SymbolKind.js +2 -0
- package/dist/src/interfaces/index.d.ts +12 -1
- package/dist/src/project-scope.d.ts +1 -0
- package/dist/src/project-scope.js +5 -0
- package/dist/src/retrieval-eval.d.ts +2 -1
- package/dist/src/retrieval-eval.js +45 -2
- package/dist/src/search/CallGraph.d.ts +9 -0
- package/dist/src/search/CallGraph.js +84 -0
- package/dist/src/search/ChunkSearch.d.ts +23 -0
- package/dist/src/search/ChunkSearch.js +115 -0
- package/dist/src/search/DependencyGraph.d.ts +19 -0
- package/dist/src/search/DependencyGraph.js +180 -0
- package/dist/src/search/IndexIdentityGuard.d.ts +11 -0
- package/dist/src/search/IndexIdentityGuard.js +37 -0
- package/dist/src/search/ProjectCatalog.d.ts +11 -0
- package/dist/src/search/ProjectCatalog.js +38 -0
- package/dist/src/search/SymbolLookup.d.ts +24 -0
- package/dist/src/search/SymbolLookup.js +97 -0
- package/dist/src/search/chunks/CandidateFetcher.d.ts +19 -0
- package/dist/src/search/chunks/CandidateFetcher.js +40 -0
- package/dist/src/search/chunks/IterativeScanGate.d.ts +10 -0
- package/dist/src/search/chunks/IterativeScanGate.js +42 -0
- package/dist/src/search/chunks/RerankStage.d.ts +15 -0
- package/dist/src/search/chunks/RerankStage.js +42 -0
- package/dist/src/search/chunks/search-metrics.d.ts +18 -0
- package/dist/src/search/chunks/search-metrics.js +32 -0
- package/dist/src/search/chunks/search-plan.d.ts +18 -0
- package/dist/src/search/chunks/search-plan.js +19 -0
- package/dist/src/search/create-search-services.d.ts +26 -0
- package/dist/src/search/create-search-services.js +29 -0
- package/dist/src/search/index-paths.d.ts +1 -0
- package/dist/src/search/index-paths.js +5 -0
- package/dist/src/search/symbol-sql.d.ts +13 -0
- package/dist/src/search/symbol-sql.js +66 -0
- package/dist/src/search/symbols/SymbolGrepFallback.d.ts +19 -0
- package/dist/src/search/symbols/SymbolGrepFallback.js +106 -0
- package/dist/src/search/symbols/SymbolIndexQuery.d.ts +14 -0
- package/dist/src/search/symbols/SymbolIndexQuery.js +52 -0
- package/dist/src/search/symbols/SymbolTextSearch.d.ts +13 -0
- package/dist/src/search/symbols/SymbolTextSearch.js +84 -0
- package/dist/src/search/symbols/TargetColumnGate.d.ts +7 -0
- package/dist/src/search/symbols/TargetColumnGate.js +23 -0
- package/dist/src/search/symbols/find-symbol-constants.d.ts +4 -0
- package/dist/src/search/symbols/find-symbol-constants.js +8 -0
- package/dist/src/search/symbols/grep-args.d.ts +3 -0
- package/dist/src/search/symbols/grep-args.js +7 -0
- package/dist/src/search/symbols/grep-patterns.d.ts +4 -0
- package/dist/src/search/symbols/grep-patterns.js +60 -0
- package/dist/src/services/AstParserService.d.ts +8 -44
- package/dist/src/services/AstParserService.js +19 -938
- package/dist/src/services/DatabaseService.d.ts +8 -10
- package/dist/src/services/DatabaseService.js +29 -86
- package/dist/src/services/IndexerService.d.ts +7 -10
- package/dist/src/services/IndexerService.js +36 -255
- package/dist/src/services/McpReadiness.d.ts +18 -0
- package/dist/src/services/McpReadiness.js +73 -0
- package/dist/src/services/McpServer.d.ts +32 -9
- package/dist/src/services/McpServer.js +78 -41
- package/dist/src/services/WebServerService.d.ts +4 -3
- package/dist/src/services/WebServerService.js +16 -8
- package/dist/src/setup.d.ts +16 -4
- package/dist/src/setup.js +115 -40
- package/dist/src/symbol-name.d.ts +1 -0
- package/dist/src/symbol-name.js +16 -12
- package/dist/src/tools/FindSymbolTool.d.ts +17 -7
- package/dist/src/tools/FindSymbolTool.js +54 -35
- package/dist/src/tools/GetCallersTool.d.ts +16 -8
- package/dist/src/tools/GetCallersTool.js +65 -35
- package/dist/src/tools/GetDependenciesTool.d.ts +16 -8
- package/dist/src/tools/GetDependenciesTool.js +35 -28
- package/dist/src/tools/GetDependentsTool.d.ts +20 -8
- package/dist/src/tools/GetDependentsTool.js +47 -33
- package/dist/src/tools/ListFilesTool.d.ts +16 -8
- package/dist/src/tools/ListFilesTool.js +20 -26
- package/dist/src/tools/ListProjectsTool.d.ts +11 -7
- package/dist/src/tools/ListProjectsTool.js +13 -11
- package/dist/src/tools/McpTool.d.ts +17 -0
- package/dist/src/tools/McpTool.js +28 -0
- package/dist/src/tools/ReadFileRangeTool.d.ts +16 -8
- package/dist/src/tools/ReadFileRangeTool.js +23 -25
- package/dist/src/tools/ReadFileTool.d.ts +15 -8
- package/dist/src/tools/ReadFileTool.js +19 -24
- package/dist/src/tools/SearchCodebaseTool.d.ts +22 -10
- package/dist/src/tools/SearchCodebaseTool.js +69 -51
- package/dist/src/tools/format-helpers.d.ts +9 -3
- package/dist/src/tools/format-helpers.js +59 -14
- package/dist/src/tools/tool-args.d.ts +38 -0
- package/dist/src/tools/tool-args.js +95 -0
- package/dist/src/workspace/GrepRunner.d.ts +15 -0
- package/dist/src/workspace/GrepRunner.js +104 -0
- package/dist/src/workspace/ProjectRoots.d.ts +22 -0
- package/dist/src/workspace/ProjectRoots.js +102 -0
- package/dist/src/workspace/ToolPaths.d.ts +31 -0
- package/dist/src/workspace/ToolPaths.js +312 -0
- package/dist/src/workspace/WorkspaceFiles.d.ts +19 -0
- package/dist/src/workspace/WorkspaceFiles.js +285 -0
- package/dist/src/workspace/fs-access.d.ts +9 -0
- package/dist/src/workspace/fs-access.js +47 -0
- package/dist/src/workspace/path-helpers.d.ts +3 -0
- package/dist/src/workspace/path-helpers.js +43 -0
- package/package.json +4 -3
- package/dist/src/interfaces/ISearcher.d.ts +0 -46
- package/dist/src/services/SearcherService.d.ts +0 -235
- package/dist/src/services/SearcherService.js +0 -1118
- /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
|
|
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. `
|
|
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": ["
|
|
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
|
-
**
|
|
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`
|
|
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`
|
|
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;
|
|
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** —
|
|
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
|
|
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`, `
|
|
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
|
|
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
|
-
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
}
|