codesearch-mcp 0.2.3-alpha → 0.3.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 +31 -1
- package/README.md +46 -26
- package/dist/app.js +5 -19
- package/dist/src/chunking/AstChunker.d.ts +12 -0
- package/dist/src/chunking/AstChunker.js +30 -0
- package/dist/src/chunking/CharacterChunkStrategy.d.ts +8 -0
- package/dist/src/chunking/CharacterChunkStrategy.js +15 -0
- package/dist/src/chunking/DeclarationChunkStrategy.d.ts +15 -0
- package/dist/src/chunking/DeclarationChunkStrategy.js +87 -0
- package/dist/src/chunking/FragmentMerger.d.ts +11 -0
- package/dist/src/chunking/FragmentMerger.js +61 -0
- package/dist/src/chunking/ImportOnlyFilter.d.ts +5 -0
- package/dist/src/chunking/ImportOnlyFilter.js +14 -0
- package/dist/src/chunking/LangchainTextSplitter.d.ts +21 -0
- package/dist/src/chunking/LangchainTextSplitter.js +133 -0
- package/dist/src/chunking/MinSizeSplitPolicy.d.ts +9 -0
- package/dist/src/chunking/MinSizeSplitPolicy.js +18 -0
- package/dist/src/chunking/RunPacker.d.ts +12 -0
- package/dist/src/chunking/RunPacker.js +84 -0
- package/dist/src/chunking/create-chunker.d.ts +3 -0
- package/dist/src/chunking/create-chunker.js +15 -0
- package/dist/src/chunking/interfaces.d.ts +27 -0
- package/dist/src/chunking/interfaces.js +2 -0
- package/dist/src/commands/ConfigCommand.js +1 -9
- package/dist/src/commands/EvalCommand.d.ts +4 -3
- package/dist/src/commands/EvalCommand.js +85 -52
- package/dist/src/commands/IndexCommand.js +0 -22
- package/dist/src/commands/McpCommand.js +2 -8
- package/dist/src/commands/WebCommand.js +2 -5
- package/dist/src/config-error.d.ts +0 -4
- package/dist/src/config-error.js +0 -4
- package/dist/src/config-store.d.ts +1 -54
- package/dist/src/config-store.js +8 -69
- package/dist/src/config.d.ts +8 -17
- package/dist/src/config.js +35 -34
- package/dist/src/constants.d.ts +2 -28
- package/dist/src/constants.js +6 -28
- package/dist/src/container.d.ts +6 -0
- package/dist/src/container.js +12 -3
- package/dist/src/db/schema.d.ts +17 -0
- package/dist/src/db/schema.js +1 -6
- package/dist/src/declarations/DeclarationTree.d.ts +19 -0
- package/dist/src/declarations/DeclarationTree.js +66 -0
- package/dist/src/declarations/DeclarationTreeBuilder.d.ts +22 -0
- package/dist/src/declarations/DeclarationTreeBuilder.js +179 -0
- package/dist/src/declarations/SourceLines.d.ts +37 -0
- package/dist/src/declarations/SourceLines.js +415 -0
- package/dist/src/dependency-traversal.d.ts +0 -6
- package/dist/src/dependency-traversal.js +0 -6
- package/dist/src/enriched-prompt.d.ts +0 -6
- package/dist/src/enriched-prompt.js +0 -6
- package/dist/src/error-record.d.ts +12 -0
- package/dist/src/error-record.js +248 -0
- package/dist/src/eval-corpus.d.ts +16 -0
- package/dist/src/eval-corpus.js +169 -0
- package/dist/src/eval-results.d.ts +6 -3
- package/dist/src/eval-results.js +309 -26
- package/dist/src/file-utils.d.ts +0 -5
- package/dist/src/file-utils.js +0 -5
- package/dist/src/graph.d.ts +0 -25
- package/dist/src/graph.js +0 -28
- package/dist/src/hnsw-settings.d.ts +3 -0
- package/dist/src/hnsw-settings.js +24 -0
- package/dist/src/interfaces/IAstParser.d.ts +2 -39
- package/dist/src/interfaces/IChunker.d.ts +3 -12
- package/dist/src/interfaces/ICorpusInspector.d.ts +7 -0
- package/dist/src/interfaces/ICorpusInspector.js +2 -0
- package/dist/src/interfaces/IDatabase.d.ts +2 -22
- package/dist/src/interfaces/IEmbedder.d.ts +0 -9
- package/dist/src/interfaces/ILogger.d.ts +1 -11
- package/dist/src/interfaces/IMcpToolHandler.d.ts +0 -5
- package/dist/src/interfaces/IMonorepoResolver.d.ts +0 -22
- package/dist/src/interfaces/IProjectDiscovery.d.ts +0 -17
- package/dist/src/interfaces/IRetrievalEvaluator.d.ts +134 -12
- package/dist/src/interfaces/ISearcher.d.ts +16 -5
- package/dist/src/interrupt.js +0 -2
- package/dist/src/owner-path.d.ts +9 -10
- package/dist/src/owner-path.js +11 -23
- package/dist/src/project-scope.d.ts +0 -12
- package/dist/src/project-scope.js +0 -12
- package/dist/src/project-selector.d.ts +0 -5
- package/dist/src/project-selector.js +0 -11
- package/dist/src/retrieval-eval.d.ts +14 -6
- package/dist/src/retrieval-eval.js +345 -59
- package/dist/src/search/result-shaping.d.ts +4 -0
- package/dist/src/search/result-shaping.js +19 -0
- package/dist/src/services/AstParserService.d.ts +2 -62
- package/dist/src/services/AstParserService.js +25 -137
- package/dist/src/services/DatabaseService.d.ts +2 -7
- package/dist/src/services/DatabaseService.js +42 -29
- package/dist/src/services/EmbedderFactory.d.ts +0 -4
- package/dist/src/services/EmbedderFactory.js +0 -5
- package/dist/src/services/IndexerService.d.ts +0 -16
- package/dist/src/services/IndexerService.js +16 -45
- package/dist/src/services/InfinityEmbedder.d.ts +0 -21
- package/dist/src/services/InfinityEmbedder.js +0 -15
- package/dist/src/services/LoggerService.d.ts +7 -5
- package/dist/src/services/LoggerService.js +37 -41
- package/dist/src/services/McpServer.d.ts +22 -14
- package/dist/src/services/McpServer.js +70 -37
- package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +1 -45
- package/dist/src/services/MonorepoAwareDiscoveryService.js +1 -51
- package/dist/src/services/NxMonorepoResolver.d.ts +0 -29
- package/dist/src/services/NxMonorepoResolver.js +5 -56
- package/dist/src/services/OpenAICompatibleEmbedder.d.ts +0 -6
- package/dist/src/services/OpenAICompatibleEmbedder.js +0 -14
- package/dist/src/services/ProjectDiscoveryService.d.ts +0 -4
- package/dist/src/services/ProjectDiscoveryService.js +0 -4
- package/dist/src/services/SearcherService.d.ts +9 -116
- package/dist/src/services/SearcherService.js +95 -176
- package/dist/src/services/VoyageEmbedder.d.ts +0 -6
- package/dist/src/services/VoyageEmbedder.js +0 -6
- package/dist/src/services/WebServerService.d.ts +1 -53
- package/dist/src/services/WebServerService.js +11 -57
- package/dist/src/services/http-retry.d.ts +0 -7
- package/dist/src/services/http-retry.js +1 -10
- package/dist/src/services/infinity-provisioning.d.ts +0 -74
- package/dist/src/services/infinity-provisioning.js +5 -88
- package/dist/src/services/legacy-ollama-index.d.ts +0 -16
- package/dist/src/services/legacy-ollama-index.js +0 -23
- package/dist/src/services/reranker.d.ts +0 -9
- package/dist/src/services/reranker.js +46 -12
- package/dist/src/setup.d.ts +11 -23
- package/dist/src/setup.js +98 -71
- package/dist/src/symbol-name.d.ts +0 -12
- package/dist/src/symbol-name.js +0 -13
- package/dist/src/symbol-rows.d.ts +0 -14
- package/dist/src/symbol-rows.js +0 -14
- package/dist/src/tool-call-log.d.ts +2 -0
- package/dist/src/tool-call-log.js +24 -0
- package/dist/src/tools/GetDependenciesTool.js +1 -1
- package/dist/src/tools/GetDependentsTool.js +1 -1
- package/dist/src/tools/SearchCodebaseTool.js +6 -3
- package/dist/src/tools/format-helpers.d.ts +0 -6
- package/dist/src/tools/format-helpers.js +87 -14
- package/dist/src/web/dashboard.d.ts +0 -8
- package/dist/src/web/dashboard.js +7 -10
- package/package.json +2 -2
- package/dist/src/services/AstChunker.d.ts +0 -42
- package/dist/src/services/AstChunker.js +0 -158
- package/docs/web-dashboard.md +0 -206
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,37 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
5
5
|
|
|
6
|
-
## [
|
|
6
|
+
## [0.3.0-alpha] - 2026-10-31
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **One `tool call` log line per MCP tool call** — at `logging.level` `info` (or `debug`) the MCP server writes one line per completed call, whatever the outcome (result, handler error, unknown tool, refused after a failed startup, cancelled by the client): `tool`, `args` (strings cut at 500 characters, arrays at 20 items), `isError`, `responseChars` (the response size — never its content), `ms`, and the caller: `client` (`{name, version}` from `initialize`), `connectionId` (random, one per server process), `session` (`CLAUDE_CODE_SESSION_ID`, or `null`), `meta` (the request's `_meta` without `progressToken`), `callId` (a per-process counter) and, for Claude Code, `toolUseId`; `cancelled: true` marks a call the client cancelled. Find them with `jq 'select(.msg=="tool call")'`.
|
|
11
|
+
- **Call context on every MCP server log line** — every line of the MCP server carries its process's `connectionId`, and every line written during a tool call, at any level and from any service (a search debug trace, a `rerank failed` warning, a handler error), also carries that call's `callId` and `toolUseId`. `rag-index`, `rag-web` and eval logs are unchanged.
|
|
12
|
+
- **`rag-index eval` reports each search stage separately** _(internal development tool: the eval suites, corpora and results live in this repository and are not shipped in the npm package)_ — every case is searched once and scored at the `vector` stage (the candidate pool's cosine order as the reranker receives it, cut to 10 — not the same as a `rerank: false` search, which fetches a smaller pool) and, when a reranker is configured, the `reranked` stage. Each case also records where its answer sits anywhere in the candidate pool and the pool's size; the run reports pool recall and how many cases got a pool shorter than the size their search requested. Within a run, the `vector` → `reranked` change is reported per category as improved / worsened / unchanged cases, the cases that gained or lost hit@5, and an exact two-sided sign-test p-value. A failed `rerank()` is still scored on the vector-order fallback `search_codebase` returns, but counted and the `reranked` stage flagged as degraded. Result files record the search conditions (similarity threshold, `reranker.overfetch`, `reranker.candidateFloor`, and the pool sizes and HNSW settings the searches reported). `--baseline` compares per stage with the same paired counts, compares pool recall and short pools, names stages present on one side only, and warns on any differing condition or rerank failures. Result files written before this change load as a baseline in the stage their recorded reranker implies, with "no pool data" and a missing-conditions warning. Each category row shows the share of one case, so a single-case move is visible as such. No change to `search_codebase` results.
|
|
13
|
+
- **`rag-index eval` runs on frozen corpora** _(internal development tool: the eval suites, corpora and results live in this repository and are not shipped in the npm package)_ — the default suite directory `benchmarks/eval/` now holds suites written against two pinned repositories, `twenty` (Nx: NestJS + GraphQL server, React front, small packages) and `arbook-backend` (Express + Apollo, schema-first GraphQL), declared in each suite as `corpus: { root, commit }`. Before any search, eval checks that each corpus is checked out at that commit with no change to tracked files, no untracked file the indexer would collect, and untracked `.ragignore` files equal to the copies in `benchmarks/corpus/`; otherwise it exits 1 without searching or writing a result file, naming every difference. Result files record per corpus the commit, its indexed projects, chunk count and a fingerprint of the indexed chunks (positions, text, owners, embeddings), plus the number of live cases; `--baseline` warns when a corpus is on one side only, its commit or fingerprint differs, or either run includes live cases. Metrics, pool recall and stage change are also reported per corpus. The former suites on `RAG` and `fix-flow-crm-backend` moved to `benchmarks/eval-live/` (`--suite benchmarks/eval-live`) as smoke tests. Setup: keep the corpus checkouts at their commits and `rag-index --project <p> --reindex` every corpus project. While it runs, eval prints one progress line per case to stderr (`[12/153] T_ID_012 vector 3 → reranked 1 (4.2s)`).
|
|
14
|
+
- **Voyage reranker** — `reranker.provider: voyage` reranks through Voyage AI (`rerank-3` by default, `rerank-3-lite` as the cheaper alternative) with any embedder, including a local Infinity one. Its defaults are its own: `reranker.baseURL` `https://api.voyageai.com/v1`, endpoint `/v1/rerank`. The key comes only from `reranker.apiKey` — never from `embedding.apiKey` — and a Voyage reranker without one stays disabled, with a warning in the log at `logging.level` `warn` or lower; startup is unaffected.
|
|
15
|
+
- **`rag-setup` asks for a reranker for every embedding provider** — `voyage`, `openai-compatible` (base URL, model, optional key stored as `RERANKER_API_KEY`) or `none`, plus `infinity` (same container) for an Infinity embedder; the default follows the embedding provider (`infinity` → `infinity`, `voyage` → `voyage`, `openai-compatible` → `none`). The Voyage reranker key is always asked separately and stored as `VOYAGE_RERANK_API_KEY`, so it never overwrites the embedding key; a blank key offers to disable the reranker. The wizard always writes an explicit `reranker` block.
|
|
16
|
+
- **Current Voyage embedding models** — `rag-setup` suggests `voyage-code-4`, and the model registry and dashboard suggestions list `voyage-code-4`, `voyage-4-large`, `voyage-4` and `voyage-4-lite` (1024 dimensions by default, up to 2048). `voyage-code-3`, `voyage-3` and `voyage-3-lite` are no longer listed but still work — their dimension is probed at index time. The dashboard suggests `rerank-3` / `rerank-3-lite` for `reranker.model` when the saved `reranker.provider` is `voyage`.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **BREAKING (config): `logging.searchMetrics` is removed** — the `search metrics` line is now a plain `info` line: set `logging.level: info` to get it, and it is never written at `warn`/`error`. A config file that still contains the key loads and ignores it; `rag-config set logging.searchMetrics` fails as an unknown key.
|
|
21
|
+
- **Errors are logged as structured records under `err`** — every caught error in the log now carries `err` with its type, message, stack (up to 30 frames), driver fields (`code`, `errno`, `syscall`, `detail`, `hint`, …) and its cause chain as `causes[]` (up to 5 links); a failed database query keeps its SQL (≤ 500 characters) and parameter count with up to 30 values. `reason` now holds only domain codes, no longer the error message. The indexer's console line names the root cause: `[indexer] Failed: <file> — <message> (<code>)`.
|
|
22
|
+
- **The candidate query always fetches the `poolSaturated` probe row** — `fetchLimit + 1` rows on every search, so `hnsw.ef_search` is now `fetchLimit + 1` for pools of 40 or more (pools of 39 or fewer keep the floor of 40), whatever the logging config. Results near the threshold boundary can shift marginally; `rag-index eval --baseline` against a result taken without `searchMetrics` warns that `efSearch` differs — re-run the baselines once.
|
|
23
|
+
- **Chunks follow declarations and members** — **requires `rag-index --reindex`.** A declaration's chunk now starts at its doc comment (JSDoc or line comments directly above it, no blank line between) or its first decorator, so doc comments and decorators are embedded with the code they belong to, and — when they fit `indexing.chunkSize` together with the declaration's name line — no chunk is a lone `@Injectable()` or a doc comment cut off from the declaration below it; otherwise the decorators alone stay with the name line when they fit with it, and a longer doc comment or decorator is cut by the character splitter. Top-level declarations of at least 200 characters that share no line with another declaration get chunks of their own (enums and `declare module` blocks are packed like other module-scope code); smaller ones (type aliases, one-line constants) are packed together. A class or object literal longer than `indexing.chunkSize` is split by its members: each member of at least 200 characters that shares no line with a sibling or with the class's first or last line gets its own chunk, starting at its doc comment; the header, fields, getters and short constructors are packed together; only a member still longer than `chunkSize` is cut by the character splitter, inside itself. A class that fits `chunkSize` stays one chunk. Import and re-export statements — multi-line ones, `import x = require(…)` and `export … from` included — are removed before packing, so they are never mixed into a code chunk. Short leftover pieces without declaration code (`export default meta;`, a closing brace) are merged into a neighbouring chunk when the result fits `indexing.chunkSize`, no removed import block lies between and — when the leftover lies inside a declaration with chunks of its own, or a unit cut by the character splitter — the neighbour lies inside the same one. A short file prologue before the imports (`'use client';`, `/* @license … */`, a shebang, lint directives — under 60 characters) is removed with them; a longer file-header comment followed by imports stays a chunk of its own.
|
|
24
|
+
- **`Symbol:` in `search_codebase` names the smallest declaration containing all of the chunk's declaration code** — not the declaration of the chunk's first line, so a chunk is never labelled with a method it only overlaps. A chunk of several small members shows their class; a chunk of several small top-level declarations, or of module-scope code only, shows none. Module-scope code (top-level statements, enums, `declare module` blocks) belongs to no declaration, so a chunk packing it with one small declaration shows that declaration. The same rule applies to schema files and the `character` strategy.
|
|
25
|
+
- **`search_codebase` returns at most 2 results per file by default** — new optional `per_file` argument (integer ≥ 1, default 2) caps the results taken from one file; the remaining slots are filled from the next candidates (a larger pool without reranking, the full rerank order with it). Pass a higher `per_file` when several answers are expected in one file. `rag-index eval` is unaffected (it searches with `per_file` equal to its limit).
|
|
26
|
+
- **The MCP server's `rerank` guidance follows the re-measured eval** — `rerank: false` is still advised for "what does X do" / "where is X" lookups (reranking costs them −0.13 MRR@10), but conceptual/architectural queries are no longer described as helped by reranking: on the new chunking it is roughly neutral for them (−0.05, not significant).
|
|
27
|
+
- **`code_chunk` gains a nullable `endLine` column**, added automatically on the next `rag-index`; rows indexed before it keep `null` until re-indexed.
|
|
28
|
+
- **The reranker sees each chunk's file and owning symbol** — every document sent to the reranker starts with one line `File: <path> | Symbol: <owner>` (`File: <path>` for a chunk without an owner; line breaks in either name become spaces), so two same-named methods in different classes are no longer indistinguishable to it. Applies to every reranker provider, with no config key. Result format, stored chunks and the vector stage are unchanged; the order of reranked results can change. No re-index required.
|
|
29
|
+
- **Reranking is off by default for every non-`infinity` embedder without a `reranker` block** — when `reranker.enabled` is absent, reranking is on only with an `infinity` embedder or when the `reranker` block names a server (`provider`, `baseURL` or `url`); a block with only `model` does not turn it on. Before, any other embedding config without a `reranker` block sent every search's rerank request to the embedding host. On a cloud host (OpenAI, Voyage) that could not succeed and fell back to vector order; a self-hosted `openai-compatible` server that does serve `/rerank` (vLLM, an external Infinity) now needs `reranker.enabled: true` or a `reranker.provider`/`baseURL` to keep reranking. An explicit `reranker.enabled` behaves as before.
|
|
30
|
+
- **A malformed rerank response fails at once** — a 2xx response without the provider's result list, or with indices outside the document list, fails the rerank call with an error naming the URL instead of being retried as a network error; search falls back to vector order as for any rerank failure.
|
|
31
|
+
- **`get_dependents` / `get_dependencies` group their direct results by file** — one line per importing/imported file instead of one per `code_dependency` row, listing the distinct names imported (`default` / `* as namespace` / `side-effect` for a nameless import, re-exports grouped after plain imports under one `re-export ` prefix, `export *` for a namespace re-export, capped at 8 then `(+N more)`); `get_dependents` additionally lists up to 3 distinct enclosing declarations using the import (`` · in `PlansPage` ``, `(+N more)` beyond that) — only those resolved unambiguously, so the part can be partial or absent. The direct section header now states the file count (`**Direct:** 8 files`). Transitive sections and the empty-result/grep-fallback text are unchanged. Output format only — no MCP tool signature, config key or CLI command changes, no re-index required.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **"MCP server is unavailable" names the cause when PostgreSQL refuses every address** — a connection refused on both `::1` and `127.0.0.1` arrives as an `AggregateError` without a message, so the tool response read `MCP server is unavailable: ` with nothing after it. It now lists the refused addresses (`connect ECONNREFUSED ::1:5432; connect ECONNREFUSED 127.0.0.1:5432`), and the log record's `err` carries each of them under `errors`.
|
|
36
|
+
- **`search_codebase` on large projects returns the configured candidate pool** — a `project`-scoped search on a project large enough for PostgreSQL to use the HNSW index (measured from ~5.8 k chunks; the crossover depends on table statistics), or any unscoped search, returned at most 40 candidates and as few as 0 even when hundreds of chunks passed the threshold: the index scan stopped at `hnsw.ef_search` (default 40) before the project filter and threshold were applied. Each search now sets `hnsw.ef_search` to the requested row count (at least 40, at most 1000) and `hnsw.iterative_scan = relaxed_order` for its own query, so the pool sized by `reranker.candidateFloor` / `reranker.overfetch` is actually fetched unless the iterative scan stops at `hnsw.max_scan_tuples` (20 000) first. Reranked results may shift, since the reranker now sees the full pool. pgvector ≥ 0.8 is recommended: an older version gets the raised `ef_search` only, with one `warn` line in the log (hidden at the default `logging.level` `error`). `search metrics` lines and `rag-index eval` search conditions record the `efSearch` / `iterativeScan` used. No config key, MCP tool signature or index change; no re-index required.
|
|
7
37
|
|
|
8
38
|
## [0.2.3-alpha] - 2026-10-21
|
|
9
39
|
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Ask your AI agent things like *"where is the payment retry logic?"* or *"show me
|
|
|
7
7
|
**Why codesearch-mcp:**
|
|
8
8
|
|
|
9
9
|
- **Dramatically fewer tokens** — without RAG, answering a cross-project question means discovery calls, directory listings, and reading several wrong files before finding the right one; a workspace of 10 projects can easily mean 50 000+ lines to sift through. With RAG, one `search_codebase` call returns 5 targeted chunks (~200–500 lines total) from across *all* projects at once — the model gets the right code immediately, with no exploration overhead
|
|
10
|
-
- **Reranker narrows the result further** — a cross-encoder reranker (
|
|
10
|
+
- **Reranker narrows the result further** — a cross-encoder reranker (on by default with the local Infinity backend; local or Voyage AI) re-scores the candidate pool so the top results are the most relevant, not just the closest vector; fewer results means less context consumed per answer
|
|
11
11
|
- **Semantic, not grep** — embeddings capture intent; queries in plain English or pseudocode find the relevant code regardless of naming
|
|
12
12
|
- **Local and private** — default setup runs entirely on your machine (Infinity embeddings + PostgreSQL); no code leaves your network
|
|
13
13
|
- **Code-structure aware** — chunks align to function and class boundaries; each result shows which declaration it belongs to (`InvoiceService.create`, `collectFiles`, …), so the model understands context without reading the surrounding file
|
|
@@ -31,7 +31,7 @@ See `CHANGELOG.md` for version history.
|
|
|
31
31
|
## Requirements
|
|
32
32
|
|
|
33
33
|
- Node.js 18+
|
|
34
|
-
- PostgreSQL 15+ with pgvector (the symbol index's unique index relies on `NULLS NOT DISTINCT`, added in PostgreSQL 15)
|
|
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
|
|
|
37
37
|
## Installation
|
|
@@ -106,7 +106,7 @@ Every file or directory argument is **workspace-relative** (relative to `workspa
|
|
|
106
106
|
|
|
107
107
|
| Tool | Description |
|
|
108
108
|
|------|-------------|
|
|
109
|
-
| `search_codebase` | Semantic search across all indexed projects. Accepts `include_graph_context` to append direct imports/dependents to each result, `include_neighbors` for surrounding chunks, `threshold` to override the similarity threshold for that query,
|
|
109
|
+
| `search_codebase` | Semantic search across all indexed projects. Accepts `include_graph_context` to append direct imports/dependents to each result, `include_neighbors` for surrounding chunks, `threshold` to override the similarity threshold for that query, `rerank` to force reranking on/off for that call regardless of `reranker.enabled`, and `per_file` (default 2) to cap how many results come from one file |
|
|
110
110
|
| `list_projects` | List projects with metadata, chunk counts and each project's directory (`path:` line) |
|
|
111
111
|
| `find_symbol` | Find a declaration or member by exact name — bare, qualified (`Class.method`), quoted, or computed — faster than semantic search. Also finds a NestJS GraphQL operation, resolve field or type by its GraphQL name (`kind: graphql_query`/`graphql_mutation`/`graphql_subscription`/`graphql_field`/`graphql_type`) |
|
|
112
112
|
| `list_files` | Browse the directory tree of any workspace directory |
|
|
@@ -150,7 +150,7 @@ Pass `include_graph_context: true` to `search_codebase` to see direct imports an
|
|
|
150
150
|
|
|
151
151
|
Useful for immediately understanding a file's role in the dependency graph.
|
|
152
152
|
|
|
153
|
-
Each result also shows a **Symbol** breadcrumb — the declaration the chunk
|
|
153
|
+
Each result also shows a **Symbol** breadcrumb — the smallest declaration containing all of the chunk's declaration code (`InvoiceService.create`, `collectFiles`, …) — so you can see the enclosing class/method without opening the file. A chunk packing several small members shows their class. Module-scope code (top-level statements, enums, `declare module`) belongs to no declaration, so a chunk packing it with one small declaration shows that declaration; the breadcrumb is omitted for a chunk of module-scope code only, for chunks packing several small top-level declarations, and for projects indexed before this feature (re-index to populate).
|
|
154
154
|
|
|
155
155
|
## Excluding files
|
|
156
156
|
|
|
@@ -174,6 +174,12 @@ npx pino-pretty < ~/.config/codesearch-mcp/logs/indexer/current.log
|
|
|
174
174
|
# Detailed indexing diagnostics (timing per file: deps / embed / db)
|
|
175
175
|
rag-config set logging.level debug
|
|
176
176
|
|
|
177
|
+
# See what an agent called (needs logging.level info or debug), grouped by session and connection
|
|
178
|
+
jq -c 'select(.msg=="tool call") | {session, connectionId, callId, tool, args, isError, ms}' \
|
|
179
|
+
~/.config/codesearch-mcp/logs/mcp/current.log
|
|
180
|
+
jq -s 'map(select(.msg=="tool call")) | group_by(.session, .connectionId) | map({session: .[0].session, connectionId: .[0].connectionId, calls: map(.tool)})' \
|
|
181
|
+
~/.config/codesearch-mcp/logs/mcp/current.log
|
|
182
|
+
|
|
177
183
|
# Disable file logging
|
|
178
184
|
rag-config set logging.dir false
|
|
179
185
|
```
|
|
@@ -205,10 +211,10 @@ rag-config edit # open config in $EDITOR; validates on save
|
|
|
205
211
|
| `embedding.dimensions` | (derived) | Vector width override; required for models whose native width exceeds 2000 (Matryoshka). Otherwise derived from model → registry → runtime probe |
|
|
206
212
|
| `embedding.batchSize` | `32` | Texts per batch embedding request |
|
|
207
213
|
| `search.similarityThreshold` | `0.6` | Max cosine distance — raise if search returns nothing |
|
|
208
|
-
| `reranker.enabled` |
|
|
209
|
-
| `reranker.provider` | `openai-compatible` | Rerank backend — `openai-compatible` `/rerank`
|
|
210
|
-
| `reranker.baseURL` |
|
|
211
|
-
| `reranker.model` | `mixedbread-ai/mxbai-rerank-xsmall-v1`
|
|
214
|
+
| `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
|
+
| `reranker.provider` | `openai-compatible` | Rerank backend — `openai-compatible` `/rerank` (Infinity, vLLM) or `voyage` (Voyage AI, needs `reranker.apiKey`) |
|
|
216
|
+
| `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
|
+
| `reranker.model` | per-provider | Reranker model name — `mixedbread-ai/mxbai-rerank-xsmall-v1` for `openai-compatible`, `rerank-3` for `voyage` |
|
|
212
218
|
| `reranker.overfetch` | `5` | Candidate multiplier before reranking (`limit × N`) |
|
|
213
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 |
|
|
214
220
|
| `indexing.chunkSize` | `1500` | Max characters per chunk |
|
|
@@ -218,9 +224,8 @@ rag-config edit # open config in $EDITOR; validates on save
|
|
|
218
224
|
| `indexing.chunkStrategy` | `ast` | How TS/JS files are chunked: `ast` = boundaries aligned to functions/classes, `character` = fixed-size splitter. Re-index to apply |
|
|
219
225
|
| `search.defaultLimit` | `5` | Default result count returned by `search_codebase` |
|
|
220
226
|
| `search.maxLimit` | `20` | Hard cap on result count |
|
|
221
|
-
| `logging.level` | `error` | Log level written to file: `error` \| `warn` \| `info` \| `debug` |
|
|
227
|
+
| `logging.level` | `error` | Log level written to file: `error` \| `warn` \| `info` \| `debug`. `info` adds one `tool call` line per MCP tool call and one `search metrics` line per search |
|
|
222
228
|
| `logging.dir` | `~/.config/codesearch-mcp/logs` | Base log directory, or `"false"` to disable |
|
|
223
|
-
| `logging.searchMetrics` | `false` | Emit one structured search-tuning line per query (at `info`, independent of `logging.level`) — use it to size `candidateFloor`/`overfetch`/`threshold` for your hardware |
|
|
224
229
|
|
|
225
230
|
## Embedding providers
|
|
226
231
|
|
|
@@ -234,7 +239,7 @@ rag-config edit # open config in $EDITOR; validates on save
|
|
|
234
239
|
| `openai-compatible` | OpenAI cloud, or any OpenAI-style server (TEI, vLLM, LM Studio, LocalAI, **or Ollama via its `/v1` endpoint**). | `baseURL` (+ `apiKey` for cloud) |
|
|
235
240
|
| `voyage` | Voyage AI cloud, code-specialized embeddings. | Voyage `apiKey` |
|
|
236
241
|
|
|
237
|
-
Easiest path: `rag-setup` asks for the provider, model,
|
|
242
|
+
Easiest path: `rag-setup` asks for the provider, model, any API key, and a reranker. Haven't run setup? It's the place to start. To change things later, edit config directly (below).
|
|
238
243
|
|
|
239
244
|
### Switching provider or changing the model
|
|
240
245
|
|
|
@@ -246,14 +251,14 @@ rag-config set embedding.provider openai-compatible
|
|
|
246
251
|
rag-config set embedding.baseURL https://api.openai.com/v1
|
|
247
252
|
rag-config set embedding.model text-embedding-3-small
|
|
248
253
|
rag-config set embedding.apiKey env:OPENAI_API_KEY # key lives in .env, not config.json
|
|
249
|
-
rag-config set reranker.enabled false #
|
|
254
|
+
rag-config set reranker.enabled false # only if rag-setup chose the `infinity` reranker: it stays aimed at the old server
|
|
250
255
|
rag-index --reindex
|
|
251
256
|
|
|
252
257
|
# Switch to Voyage AI
|
|
253
258
|
rag-config set embedding.provider voyage
|
|
254
|
-
rag-config set embedding.model voyage-code-
|
|
259
|
+
rag-config set embedding.model voyage-code-4
|
|
255
260
|
rag-config set embedding.apiKey env:VOYAGE_API_KEY # key lives in .env, not config.json
|
|
256
|
-
rag-config set reranker.enabled false #
|
|
261
|
+
rag-config set reranker.enabled false # only if rag-setup chose the `infinity` reranker (or configure the Voyage reranker, below)
|
|
257
262
|
rag-index --reindex
|
|
258
263
|
|
|
259
264
|
# Change only the model on the current provider
|
|
@@ -272,9 +277,9 @@ rag-index --reindex
|
|
|
272
277
|
Per-provider notes:
|
|
273
278
|
|
|
274
279
|
- **infinity** — the model set is bound at container startup, so changing `embedding.model` (or the reranker model) **recreates** the managed container on the next `rag-setup`/`rag-index`/dashboard save. Defaults: `jinaai/jina-embeddings-v2-base-code` + reranker `mixedbread-ai/mxbai-rerank-xsmall-v1`. Both models are included in the compose template and reranking is **enabled by default** (`reranker.enabled: true`).
|
|
275
|
-
- **openai-compatible** — point `embedding.baseURL` at the server; set `embedding.apiKey` (as `env:VAR`) for cloud, leave empty for local servers (TEI/vLLM/LM Studio/**Ollama**). E.g. `text-embedding-3-small`, `text-embedding-3-large`. Cloud endpoints (OpenAI, Azure) don't expose `/rerank
|
|
280
|
+
- **openai-compatible** — point `embedding.baseURL` at the server; set `embedding.apiKey` (as `env:VAR`) for cloud, leave empty for local servers (TEI/vLLM/LM Studio/**Ollama**). E.g. `text-embedding-3-small`, `text-embedding-3-large`. Cloud endpoints (OpenAI, Azure) don't expose `/rerank`, so without a `reranker` block reranking stays off unless you configure a separate rerank server; a block from `rag-setup`'s `infinity` reranker choice keeps reranking on at the old server until you set `reranker.enabled false` (see [Reranker with a cloud embedder](#reranker-with-a-cloud-or-external-embedder)).
|
|
276
281
|
- **Ollama** (via `openai-compatible`) — set `embedding.baseURL` to `http://localhost:11434/v1` and `embedding.model` to any model you've pulled (e.g. `bge-m3`). No dedicated code path, no auto-pull — pull the model yourself with `ollama pull <model>` first.
|
|
277
|
-
- **voyage** — set `embedding.apiKey` (as `env:VAR`); e.g. `voyage-code-
|
|
282
|
+
- **voyage** — set `embedding.apiKey` (as `env:VAR`); e.g. `voyage-code-4` (default in `rag-setup`), `voyage-4-large`, `voyage-4`, `voyage-4-lite`. Older names such as `voyage-code-3` still work — their dimension is probed. Without a `reranker` block, reranking stays off unless you configure one (a block from `rag-setup`'s `infinity` reranker choice keeps it on at the old server until you set `reranker.enabled false`) — e.g. the Voyage reranker (see [Reranker with a cloud embedder](#reranker-with-a-cloud-or-external-embedder)).
|
|
278
283
|
|
|
279
284
|
The vector **dimension** is derived automatically (model registry → one-shot probe); set `embedding.dimensions` only to force a Matryoshka-reduced width for models whose native size exceeds 2000.
|
|
280
285
|
|
|
@@ -291,23 +296,36 @@ The same "already running?" probe applies everywhere: a reachable backend defaul
|
|
|
291
296
|
|
|
292
297
|
### Reranker with a cloud or external embedder
|
|
293
298
|
|
|
294
|
-
|
|
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:
|
|
300
|
+
|
|
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.
|
|
295
302
|
|
|
296
|
-
**
|
|
303
|
+
**Voyage reranker** (works with any embedder; its key is separate from the embedding key):
|
|
297
304
|
|
|
298
305
|
```bash
|
|
299
|
-
|
|
306
|
+
echo 'VOYAGE_RERANK_API_KEY=pa-…' >> ~/.config/codesearch-mcp/.env
|
|
307
|
+
rag-config set reranker.enabled true
|
|
308
|
+
rag-config set reranker.provider voyage
|
|
309
|
+
rag-config set reranker.model rerank-3 # or rerank-3-lite
|
|
310
|
+
rag-config set reranker.apiKey env:VOYAGE_RERANK_API_KEY
|
|
311
|
+
rag-config reset reranker.baseURL # a stored Infinity baseURL would win over the Voyage default
|
|
312
|
+
rag-config reset reranker.url
|
|
300
313
|
```
|
|
301
314
|
|
|
302
|
-
|
|
315
|
+
Without `reranker.apiKey` a Voyage reranker stays disabled and a warning is written to the log — visible only at `logging.level` `warn` or lower (the default `error` hides it).
|
|
316
|
+
|
|
317
|
+
**A separate `/rerank` server** (e.g. a local Infinity or vLLM instance while using Voyage/OpenAI for embeddings):
|
|
303
318
|
|
|
304
319
|
```bash
|
|
305
|
-
rag-config set
|
|
306
|
-
rag-config set
|
|
307
|
-
|
|
320
|
+
rag-config set reranker.enabled true
|
|
321
|
+
rag-config set reranker.provider openai-compatible # a stored voyage provider would send the Voyage wire format
|
|
322
|
+
rag-config set reranker.baseURL http://localhost:7997
|
|
323
|
+
rag-config set reranker.model mixedbread-ai/mxbai-rerank-xsmall-v1
|
|
324
|
+
rag-config reset reranker.url
|
|
325
|
+
rag-config reset reranker.apiKey # unless the server needs one
|
|
308
326
|
```
|
|
309
327
|
|
|
310
|
-
The embedding and reranking servers are fully independent — any combination works: Voyage embeddings + local Infinity reranker, OpenAI embeddings + remote
|
|
328
|
+
The embedding and reranking servers are fully independent — any combination works: Voyage embeddings + local Infinity reranker, OpenAI embeddings + remote vLLM reranker, etc.
|
|
311
329
|
|
|
312
330
|
### Secrets & external embedders
|
|
313
331
|
|
|
@@ -323,13 +341,15 @@ When reranking is enabled, `search_codebase` fetches a **candidate pool** by cos
|
|
|
323
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.
|
|
324
342
|
- **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.
|
|
325
343
|
|
|
344
|
+
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.
|
|
345
|
+
|
|
326
346
|
To size these for **your** hardware, turn on telemetry:
|
|
327
347
|
|
|
328
348
|
```bash
|
|
329
|
-
rag-config set logging.
|
|
349
|
+
rag-config set logging.level info
|
|
330
350
|
```
|
|
331
351
|
|
|
332
|
-
Each search then writes one `
|
|
352
|
+
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:
|
|
333
353
|
|
|
334
354
|
- **`poolSaturated: true`** — more chunks were under the threshold than the window took → recall is being truncated; raise `candidateFloor`/`overfetch`.
|
|
335
355
|
- **`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.
|
package/dist/app.js
CHANGED
|
@@ -46,20 +46,13 @@ const McpCommand_1 = require("./src/commands/McpCommand");
|
|
|
46
46
|
const ConfigCommand_1 = require("./src/commands/ConfigCommand");
|
|
47
47
|
const WebCommand_1 = require("./src/commands/WebCommand");
|
|
48
48
|
const setup_1 = require("./src/setup");
|
|
49
|
-
/**
|
|
50
|
-
* Load `~/.config/codesearch-mcp/.env` into `process.env` before config is read.
|
|
51
|
-
* All five binaries share this file — `rag-mcp` in particular is spawned by the
|
|
52
|
-
* MCP client and does not inherit shell exports. A real `process.env` value
|
|
53
|
-
* always wins over the file. Minimal `KEY=VALUE` parser (no `dotenv` dep):
|
|
54
|
-
* skips blanks and `#` comments, strips one layer of surrounding quotes.
|
|
55
|
-
*/
|
|
56
49
|
function loadDotEnv(file) {
|
|
57
50
|
let text;
|
|
58
51
|
try {
|
|
59
52
|
text = fs.readFileSync(file, 'utf-8');
|
|
60
53
|
}
|
|
61
54
|
catch {
|
|
62
|
-
return;
|
|
55
|
+
return;
|
|
63
56
|
}
|
|
64
57
|
for (const rawLine of text.split(/\r?\n/)) {
|
|
65
58
|
const line = rawLine.trim();
|
|
@@ -76,7 +69,7 @@ function loadDotEnv(file) {
|
|
|
76
69
|
val = val.slice(1, -1);
|
|
77
70
|
}
|
|
78
71
|
if (!(key in process.env))
|
|
79
|
-
process.env[key] = val;
|
|
72
|
+
process.env[key] = val;
|
|
80
73
|
}
|
|
81
74
|
}
|
|
82
75
|
async function main() {
|
|
@@ -88,8 +81,6 @@ async function main() {
|
|
|
88
81
|
const isConfig = argv1 === 'rag-config' || process.argv[2] === 'config' || process.argv[2] === 'rag-config';
|
|
89
82
|
const isWeb = argv1 === 'rag-web' || process.argv[2] === 'web' || process.argv[2] === 'rag-web';
|
|
90
83
|
const isBinary = ['rag-index', 'rag-mcp', 'rag-setup', 'rag-config', 'rag-web'].includes(argv1);
|
|
91
|
-
// `rag-index eval` — a sub-mode of index, not its own binary, so it sits one
|
|
92
|
-
// token deeper than the other modes (`rag-index eval` / `index eval`).
|
|
93
84
|
const evalSubArg = isBinary ? process.argv[2] : process.argv[3];
|
|
94
85
|
const isEval = isIndex && evalSubArg === 'eval';
|
|
95
86
|
const mode = isEval ? 'eval' : isIndex ? 'index' : isMcp ? 'mcp' : isSetup ? 'setup' : isConfig ? 'config' : isWeb ? 'web' : (process.argv[2] ?? '');
|
|
@@ -99,7 +90,6 @@ async function main() {
|
|
|
99
90
|
process.exit(0);
|
|
100
91
|
}
|
|
101
92
|
const effectiveArgs = process.argv.slice(isEval ? (isBinary ? 3 : 4) : (isBinary ? 2 : 3));
|
|
102
|
-
// Arg validation
|
|
103
93
|
if (mode === 'eval') {
|
|
104
94
|
const valueFlags = new Set(['--suite', '--baseline', '--project']);
|
|
105
95
|
const unknown = [];
|
|
@@ -108,7 +98,7 @@ async function main() {
|
|
|
108
98
|
if (valueFlags.has(a)) {
|
|
109
99
|
const next = effectiveArgs[i + 1];
|
|
110
100
|
if (next !== undefined && !next.startsWith('-'))
|
|
111
|
-
i++;
|
|
101
|
+
i++;
|
|
112
102
|
continue;
|
|
113
103
|
}
|
|
114
104
|
unknown.push(a);
|
|
@@ -129,7 +119,7 @@ async function main() {
|
|
|
129
119
|
if (a === '--project') {
|
|
130
120
|
const next = effectiveArgs[i + 1];
|
|
131
121
|
if (next !== undefined && !next.startsWith('-'))
|
|
132
|
-
i++;
|
|
122
|
+
i++;
|
|
133
123
|
continue;
|
|
134
124
|
}
|
|
135
125
|
unknown.push(a);
|
|
@@ -158,7 +148,7 @@ async function main() {
|
|
|
158
148
|
process.exit(1);
|
|
159
149
|
}
|
|
160
150
|
i++;
|
|
161
|
-
continue;
|
|
151
|
+
continue;
|
|
162
152
|
}
|
|
163
153
|
console.error(`Unexpected argument: ${effectiveArgs[i]}\n`);
|
|
164
154
|
(0, help_1.printHelp)(isBinary ? argv1 : 'rag-web');
|
|
@@ -173,7 +163,6 @@ async function main() {
|
|
|
173
163
|
await new ConfigCommand_1.ConfigCommand(effectiveArgs).execute();
|
|
174
164
|
process.exit(0);
|
|
175
165
|
}
|
|
176
|
-
// All other modes require a valid config.json
|
|
177
166
|
const loggerName = mode === 'index' || mode === 'eval' ? 'indexer' : mode === 'mcp' ? 'mcp' : mode === 'web' ? 'web' : undefined;
|
|
178
167
|
let container;
|
|
179
168
|
try {
|
|
@@ -193,9 +182,6 @@ async function main() {
|
|
|
193
182
|
console.error(`workspaceRoot is not configured. ${hint}`);
|
|
194
183
|
process.exit(1);
|
|
195
184
|
}
|
|
196
|
-
// Construct only the selected command — building every command eagerly would
|
|
197
|
-
// run each constructor's arg parsing (e.g. WebCommand's --port) for modes that
|
|
198
|
-
// never use it.
|
|
199
185
|
const factories = {
|
|
200
186
|
index: () => new IndexCommand_1.IndexCommand(container, argv1),
|
|
201
187
|
eval: () => new EvalCommand_1.EvalCommand(container, argv1),
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Config } from '../config';
|
|
2
|
+
import type { IChunker, ChunkPiece, ChunkOpts } from '../interfaces/IChunker';
|
|
3
|
+
import type { IChunkPostProcessor, IChunkStrategy } from './interfaces';
|
|
4
|
+
export declare class AstChunker implements IChunker {
|
|
5
|
+
private readonly declarationStrategy;
|
|
6
|
+
private readonly characterStrategy;
|
|
7
|
+
private readonly postProcessors;
|
|
8
|
+
private readonly declarationPostProcessors;
|
|
9
|
+
private readonly chunkSize;
|
|
10
|
+
constructor(declarationStrategy: IChunkStrategy, characterStrategy: IChunkStrategy, postProcessors: IChunkPostProcessor[], declarationPostProcessors: IChunkPostProcessor[], config: Config);
|
|
11
|
+
chunk(code: string, opts: ChunkOpts): Promise<ChunkPiece[]>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AstChunker = void 0;
|
|
4
|
+
const DeclarationTree_1 = require("../declarations/DeclarationTree");
|
|
5
|
+
const SourceLines_1 = require("../declarations/SourceLines");
|
|
6
|
+
class AstChunker {
|
|
7
|
+
declarationStrategy;
|
|
8
|
+
characterStrategy;
|
|
9
|
+
postProcessors;
|
|
10
|
+
declarationPostProcessors;
|
|
11
|
+
chunkSize;
|
|
12
|
+
constructor(declarationStrategy, characterStrategy, postProcessors, declarationPostProcessors, config) {
|
|
13
|
+
this.declarationStrategy = declarationStrategy;
|
|
14
|
+
this.characterStrategy = characterStrategy;
|
|
15
|
+
this.postProcessors = postProcessors;
|
|
16
|
+
this.declarationPostProcessors = declarationPostProcessors;
|
|
17
|
+
this.chunkSize = config.indexer.chunkSize;
|
|
18
|
+
}
|
|
19
|
+
async chunk(code, opts) {
|
|
20
|
+
const declarations = opts.declarations ?? DeclarationTree_1.DeclarationTree.EMPTY;
|
|
21
|
+
const ctx = { lines: new SourceLines_1.SourceLines(code, { jsx: opts.jsx }), declarations, chunkSize: this.chunkSize, removed: [], splitPoints: [], characterSplit: [] };
|
|
22
|
+
const byDeclarations = !opts.isSchemaFile && opts.strategy !== 'character' && declarations.roots.length > 0;
|
|
23
|
+
let pieces = await (byDeclarations ? this.declarationStrategy : this.characterStrategy).chunk(ctx, opts);
|
|
24
|
+
const processors = byDeclarations ? [...this.postProcessors, ...this.declarationPostProcessors] : this.postProcessors;
|
|
25
|
+
for (const processor of processors)
|
|
26
|
+
pieces = processor.process(pieces, ctx);
|
|
27
|
+
return pieces;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.AstChunker = AstChunker;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ChunkOpts, ChunkPiece } from '../interfaces/IChunker';
|
|
2
|
+
import type { ChunkContext, IChunkStrategy, ITextSplitter } from './interfaces';
|
|
3
|
+
export declare class CharacterChunkStrategy implements IChunkStrategy {
|
|
4
|
+
private readonly jsSplitter;
|
|
5
|
+
private readonly schemaSplitter;
|
|
6
|
+
constructor(jsSplitter: ITextSplitter, schemaSplitter: ITextSplitter);
|
|
7
|
+
chunk(ctx: ChunkContext, opts: ChunkOpts): Promise<ChunkPiece[]>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CharacterChunkStrategy = void 0;
|
|
4
|
+
class CharacterChunkStrategy {
|
|
5
|
+
jsSplitter;
|
|
6
|
+
schemaSplitter;
|
|
7
|
+
constructor(jsSplitter, schemaSplitter) {
|
|
8
|
+
this.jsSplitter = jsSplitter;
|
|
9
|
+
this.schemaSplitter = schemaSplitter;
|
|
10
|
+
}
|
|
11
|
+
chunk(ctx, opts) {
|
|
12
|
+
return (opts.isSchemaFile ? this.schemaSplitter : this.jsSplitter).split(ctx.lines.text, 1);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.CharacterChunkStrategy = CharacterChunkStrategy;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ChunkPiece } from '../interfaces/IChunker';
|
|
2
|
+
import type { ChunkContext, IChunkStrategy, ISplitPolicy } from './interfaces';
|
|
3
|
+
import { RunPacker } from './RunPacker';
|
|
4
|
+
export declare class DeclarationChunkStrategy implements IChunkStrategy {
|
|
5
|
+
private readonly policy;
|
|
6
|
+
private readonly packer;
|
|
7
|
+
constructor(policy: ISplitPolicy, packer: RunPacker);
|
|
8
|
+
chunk(ctx: ChunkContext): Promise<ChunkPiece[]>;
|
|
9
|
+
private splitNode;
|
|
10
|
+
private coversParent;
|
|
11
|
+
private splitLevel;
|
|
12
|
+
private packRuns;
|
|
13
|
+
private static isShortPrologue;
|
|
14
|
+
private static gaps;
|
|
15
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DeclarationChunkStrategy = void 0;
|
|
4
|
+
const FragmentMerger_1 = require("./FragmentMerger");
|
|
5
|
+
const RunPacker_1 = require("./RunPacker");
|
|
6
|
+
class DeclarationChunkStrategy {
|
|
7
|
+
policy;
|
|
8
|
+
packer;
|
|
9
|
+
constructor(policy, packer) {
|
|
10
|
+
this.policy = policy;
|
|
11
|
+
this.packer = packer;
|
|
12
|
+
}
|
|
13
|
+
async chunk(ctx) {
|
|
14
|
+
const roots = ctx.declarations.roots;
|
|
15
|
+
const removed = DeclarationChunkStrategy.gaps({ start: 1, end: ctx.lines.count }, roots)
|
|
16
|
+
.flatMap(gap => ctx.lines.importRegions(gap.start, gap.end));
|
|
17
|
+
if (removed.length > 0 && DeclarationChunkStrategy.isShortPrologue(removed[0].start - 1, ctx.lines)) {
|
|
18
|
+
removed[0] = { start: 1, end: removed[0].end };
|
|
19
|
+
}
|
|
20
|
+
ctx.removed.push(...removed);
|
|
21
|
+
return this.splitLevel({ start: 1, end: ctx.lines.count }, roots, null, removed, ctx);
|
|
22
|
+
}
|
|
23
|
+
async splitNode(node, ctx) {
|
|
24
|
+
if (ctx.lines.length(node.start, node.end) <= ctx.chunkSize)
|
|
25
|
+
return RunPacker_1.RunPacker.piece(node, ctx.lines);
|
|
26
|
+
let children = node.children;
|
|
27
|
+
while (children.length === 1 && this.coversParent(children[0], node, ctx))
|
|
28
|
+
children = children[0].children;
|
|
29
|
+
return this.splitLevel(node, children, node, [], ctx);
|
|
30
|
+
}
|
|
31
|
+
coversParent(child, parent, ctx) {
|
|
32
|
+
if (child.end !== parent.end || child.start < parent.start)
|
|
33
|
+
return false;
|
|
34
|
+
for (let n = parent.start; n < child.start; n++) {
|
|
35
|
+
if (!ctx.lines.isBlank(n) && !ctx.lines.isCommentOnly(n))
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
async splitLevel(range, children, boundary, removed, ctx) {
|
|
41
|
+
const splitPoints = children.filter(c => this.policy.isSplitPoint(c, children, boundary, ctx.lines));
|
|
42
|
+
const packed = children.filter(c => !splitPoints.includes(c));
|
|
43
|
+
ctx.splitPoints.push(...splitPoints);
|
|
44
|
+
const pieces = [];
|
|
45
|
+
let cursor = range.start;
|
|
46
|
+
for (const sp of splitPoints) {
|
|
47
|
+
if (cursor < sp.start)
|
|
48
|
+
pieces.push(...await this.packRuns({ start: cursor, end: sp.start - 1 }, packed, boundary, removed, ctx));
|
|
49
|
+
pieces.push(...await this.splitNode(sp, ctx));
|
|
50
|
+
cursor = sp.end + 1;
|
|
51
|
+
}
|
|
52
|
+
if (cursor <= range.end)
|
|
53
|
+
pieces.push(...await this.packRuns({ start: cursor, end: range.end }, packed, boundary, removed, ctx));
|
|
54
|
+
return pieces;
|
|
55
|
+
}
|
|
56
|
+
async packRuns(span, children, owner, removed, ctx) {
|
|
57
|
+
const pieces = [];
|
|
58
|
+
for (const run of DeclarationChunkStrategy.gaps(span, removed)) {
|
|
59
|
+
pieces.push(...await this.packer.pack(run, children, ctx.lines, ctx.chunkSize, ctx.characterSplit, owner));
|
|
60
|
+
}
|
|
61
|
+
return pieces;
|
|
62
|
+
}
|
|
63
|
+
static isShortPrologue(end, lines) {
|
|
64
|
+
if (end < 1)
|
|
65
|
+
return false;
|
|
66
|
+
for (let n = lines.line(1).startsWith('#!') ? 2 : 1; n <= end; n++) {
|
|
67
|
+
if (!lines.isBlank(n) && !lines.isCommentOnly(n) && !lines.isDirective(n))
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return lines.slice(1, end).replace(/\s+/g, '').length < FragmentMerger_1.MIN_FRAGMENT_CHARS;
|
|
71
|
+
}
|
|
72
|
+
static gaps(span, taken) {
|
|
73
|
+
const out = [];
|
|
74
|
+
let cursor = span.start;
|
|
75
|
+
for (const t of [...taken].sort((a, b) => a.start - b.start)) {
|
|
76
|
+
if (t.end < cursor || t.start > span.end)
|
|
77
|
+
continue;
|
|
78
|
+
if (cursor < t.start)
|
|
79
|
+
out.push({ start: cursor, end: t.start - 1 });
|
|
80
|
+
cursor = Math.max(cursor, t.end + 1);
|
|
81
|
+
}
|
|
82
|
+
if (cursor <= span.end)
|
|
83
|
+
out.push({ start: cursor, end: span.end });
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.DeclarationChunkStrategy = DeclarationChunkStrategy;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ChunkPiece } from '../interfaces/IChunker';
|
|
2
|
+
import type { ChunkContext, IChunkPostProcessor } from './interfaces';
|
|
3
|
+
export declare const MIN_FRAGMENT_CHARS = 60;
|
|
4
|
+
export declare class FragmentMerger implements IChunkPostProcessor {
|
|
5
|
+
process(pieces: ChunkPiece[], ctx: ChunkContext): ChunkPiece[];
|
|
6
|
+
private isFragment;
|
|
7
|
+
private canMerge;
|
|
8
|
+
private static scope;
|
|
9
|
+
private static within;
|
|
10
|
+
private static merge;
|
|
11
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FragmentMerger = exports.MIN_FRAGMENT_CHARS = void 0;
|
|
4
|
+
const owner_path_1 = require("../owner-path");
|
|
5
|
+
exports.MIN_FRAGMENT_CHARS = 60;
|
|
6
|
+
class FragmentMerger {
|
|
7
|
+
process(pieces, ctx) {
|
|
8
|
+
const queue = [...pieces].sort((a, b) => a.startLine - b.startLine);
|
|
9
|
+
const out = [];
|
|
10
|
+
for (let i = 0; i < queue.length; i++) {
|
|
11
|
+
const piece = queue[i];
|
|
12
|
+
if (!this.isFragment(piece, ctx)) {
|
|
13
|
+
out.push(piece);
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const scope = FragmentMerger.scope(piece, ctx);
|
|
17
|
+
const prev = out[out.length - 1];
|
|
18
|
+
if (prev && FragmentMerger.within(prev, scope) && this.canMerge(prev, piece, ctx)) {
|
|
19
|
+
out[out.length - 1] = FragmentMerger.merge(prev, piece, ctx);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const next = queue[i + 1];
|
|
23
|
+
if (next && FragmentMerger.within(next, scope) && this.canMerge(piece, next, ctx)) {
|
|
24
|
+
queue[i + 1] = FragmentMerger.merge(piece, next, ctx);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
out.push(piece);
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
isFragment(piece, ctx) {
|
|
32
|
+
return ctx.lines.significantLength(piece.startLine, piece.endLine) < exports.MIN_FRAGMENT_CHARS
|
|
33
|
+
&& (0, owner_path_1.ownedLines)(piece, ctx.declarations, ctx.lines).length === 0;
|
|
34
|
+
}
|
|
35
|
+
canMerge(first, second, ctx) {
|
|
36
|
+
const start = Math.min(first.startLine, second.startLine);
|
|
37
|
+
const end = Math.max(first.endLine, second.endLine);
|
|
38
|
+
if (ctx.removed.some(r => r.start > first.endLine && r.end < second.startLine))
|
|
39
|
+
return false;
|
|
40
|
+
return ctx.lines.length(start, end) <= ctx.chunkSize;
|
|
41
|
+
}
|
|
42
|
+
static scope(piece, ctx) {
|
|
43
|
+
let best = null;
|
|
44
|
+
for (const sp of [...ctx.splitPoints, ...ctx.characterSplit]) {
|
|
45
|
+
if (sp.start > piece.startLine || sp.end < piece.endLine)
|
|
46
|
+
continue;
|
|
47
|
+
if (!best || sp.end - sp.start < best.end - best.start)
|
|
48
|
+
best = sp;
|
|
49
|
+
}
|
|
50
|
+
return best;
|
|
51
|
+
}
|
|
52
|
+
static within(piece, scope) {
|
|
53
|
+
return !scope || (scope.start <= piece.startLine && piece.endLine <= scope.end);
|
|
54
|
+
}
|
|
55
|
+
static merge(first, second, ctx) {
|
|
56
|
+
const startLine = Math.min(first.startLine, second.startLine);
|
|
57
|
+
const endLine = Math.max(first.endLine, second.endLine);
|
|
58
|
+
return { content: ctx.lines.slice(startLine, endLine), startLine, endLine };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.FragmentMerger = FragmentMerger;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ChunkPiece } from '../interfaces/IChunker';
|
|
2
|
+
import type { ChunkContext, IChunkPostProcessor } from './interfaces';
|
|
3
|
+
export declare class ImportOnlyFilter implements IChunkPostProcessor {
|
|
4
|
+
process(pieces: ChunkPiece[], ctx: ChunkContext): ChunkPiece[];
|
|
5
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ImportOnlyFilter = void 0;
|
|
4
|
+
class ImportOnlyFilter {
|
|
5
|
+
process(pieces, ctx) {
|
|
6
|
+
return pieces.filter((p) => {
|
|
7
|
+
if (!ctx.lines.isImportOnly(p.startLine, p.endLine))
|
|
8
|
+
return true;
|
|
9
|
+
ctx.removed.push({ start: p.startLine, end: p.endLine });
|
|
10
|
+
return false;
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.ImportOnlyFilter = ImportOnlyFilter;
|