codesearch-mcp 0.2.1-alpha → 0.2.3-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +15 -8
  3. package/dist/app.js +45 -7
  4. package/dist/src/commands/EvalCommand.d.ts +10 -0
  5. package/dist/src/commands/EvalCommand.js +231 -0
  6. package/dist/src/commands/IndexCommand.js +79 -53
  7. package/dist/src/db/index.d.ts +1 -1
  8. package/dist/src/db/index.js +1 -2
  9. package/dist/src/db/schema.d.ts +5 -29
  10. package/dist/src/db/schema.js +2 -8
  11. package/dist/src/dependency-traversal.d.ts +16 -0
  12. package/dist/src/dependency-traversal.js +45 -0
  13. package/dist/src/enriched-prompt.d.ts +3 -2
  14. package/dist/src/enriched-prompt.js +3 -2
  15. package/dist/src/eval-results.d.ts +12 -0
  16. package/dist/src/eval-results.js +114 -0
  17. package/dist/src/file-utils.d.ts +15 -0
  18. package/dist/src/file-utils.js +24 -1
  19. package/dist/src/help.js +4 -1
  20. package/dist/src/interfaces/IAstParser.d.ts +13 -1
  21. package/dist/src/interfaces/IDatabase.d.ts +0 -14
  22. package/dist/src/interfaces/IRetrievalEvaluator.d.ts +94 -0
  23. package/dist/src/interfaces/IRetrievalEvaluator.js +2 -0
  24. package/dist/src/interfaces/ISearcher.d.ts +11 -16
  25. package/dist/src/interfaces/index.d.ts +1 -0
  26. package/dist/src/project-scope.d.ts +19 -0
  27. package/dist/src/project-scope.js +49 -0
  28. package/dist/src/project-selector.d.ts +6 -0
  29. package/dist/src/project-selector.js +18 -0
  30. package/dist/src/retrieval-eval.d.ts +16 -0
  31. package/dist/src/retrieval-eval.js +214 -0
  32. package/dist/src/services/AstParserService.d.ts +66 -1
  33. package/dist/src/services/AstParserService.js +432 -28
  34. package/dist/src/services/DatabaseService.d.ts +0 -12
  35. package/dist/src/services/DatabaseService.js +2 -52
  36. package/dist/src/services/IndexerService.d.ts +8 -0
  37. package/dist/src/services/IndexerService.js +24 -11
  38. package/dist/src/services/McpServer.d.ts +7 -0
  39. package/dist/src/services/McpServer.js +42 -38
  40. package/dist/src/services/OpenAICompatibleEmbedder.js +2 -1
  41. package/dist/src/services/SearcherService.d.ts +189 -25
  42. package/dist/src/services/SearcherService.js +505 -203
  43. package/dist/src/symbol-rows.d.ts +14 -9
  44. package/dist/src/symbol-rows.js +17 -11
  45. package/dist/src/tools/FindSymbolTool.js +14 -5
  46. package/dist/src/tools/GetCallersTool.d.ts +3 -1
  47. package/dist/src/tools/GetCallersTool.js +16 -6
  48. package/dist/src/tools/GetDependenciesTool.d.ts +3 -1
  49. package/dist/src/tools/GetDependenciesTool.js +15 -8
  50. package/dist/src/tools/GetDependentsTool.d.ts +3 -1
  51. package/dist/src/tools/GetDependentsTool.js +31 -16
  52. package/dist/src/tools/ListFilesTool.d.ts +3 -1
  53. package/dist/src/tools/ListFilesTool.js +14 -12
  54. package/dist/src/tools/ListProjectsTool.js +5 -1
  55. package/dist/src/tools/ReadFileRangeTool.d.ts +3 -1
  56. package/dist/src/tools/ReadFileRangeTool.js +12 -11
  57. package/dist/src/tools/ReadFileTool.d.ts +3 -1
  58. package/dist/src/tools/ReadFileTool.js +12 -10
  59. package/dist/src/tools/SearchCodebaseTool.js +3 -1
  60. package/dist/src/tools/format-helpers.d.ts +16 -1
  61. package/dist/src/tools/format-helpers.js +64 -0
  62. package/package.json +9 -8
package/CHANGELOG.md CHANGED
@@ -5,6 +5,46 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.2.3-alpha] - 2026-10-21
9
+
10
+ ### Measured, no change
11
+
12
+ - **Embedding prompt: members in `Symbols:` and an `Owner:` line both measured, neither shipped** — `prompt-owner-line` ran `rag-index eval` against two prompt variants (members overlapping the chunk added to `Symbols:`; a new `Owner: <ownerPath>` line) against the `retrieval-eval-set` reference baseline (41 cases). Members: overall MRR@10 +0.0192, short of the required ≥ 0.02. Owner line: overall MRR@10 -0.0112, with the `identifier` category dropping -0.0268 (over the -0.02 limit). Neither met the decision rule fixed in advance, so the embedding prompt is unchanged. Results: `benchmarks/results/prompt-owner-line-decision.md`. Separately, re-running the same suite with the reranker disabled scored far higher than with it enabled for both variants (+0.16 to +0.21 MRR@10) — unrelated to the prompt tested, but worth its own investigation into the configured reranker.
13
+
14
+ ### Added
15
+
16
+ - **`rag-index --project [name]` scopes a run to one project or monorepo unit** — matched exactly against the same names `list_projects` prints; a unit name (e.g. `micros-learning`) expands to all of its children. Bypasses the `.ragprojects`/`.ragskip` selector and project pruning entirely, leaving every other indexed project untouched. Composes with `--reindex` to force a full reindex of just that project. Omitting the name opens a one-off interactive single-choice prompt (distinct from the `--select` multi-select checkbox). An unresolved name is reported with the full list of available projects and exits non-zero before any database or embedding-backend work. The post-index project graph (`project-graph.html`) now always reflects the full discovered workspace regardless of `--select`/`--project` scoping.
17
+ - **`search_codebase` gains a per-call `rerank` override** — pass `rerank: false` to skip reranking regardless of `reranker.enabled` (uses `fetchLimit = limit`, no candidate floor), or `rerank: true` to force it on when a reranker is configured (no-op otherwise). A 41-case / 82-query benchmark across both projects' `benchmarks/eval/*.json` suites found `rerank: false` performs same-or-better in a strong majority of identifier/behaviour/concept cases at `threshold=0.6`/`limit=5`; guidance in `McpServer.ts` and `CLAUDE.md` recommends `false` for "what does X do" / "where is X" lookups described without an exact name, and points exact-name lookups at `find_symbol` instead of `search_codebase` (the rerank setting is moot there — `find_symbol` resolved every sampled identifier exactly at rank 1).
18
+ - **`find_symbol` finds NestJS GraphQL operations, resolve fields and types by their GraphQL name** — `@Query`/`@Mutation`/`@Subscription`/`@ResolveField`/`@ObjectType`/`@InputType`/`@InterfaceType` decorators imported from `@nestjs/graphql` get an extra `code_symbol` row named by the GraphQL name (e.g. `adminPendingSubscriptionOrders`, not the method's TypeScript name), with new kinds `graphql_query`/`graphql_mutation`/`graphql_subscription`/`graphql_field`/`graphql_type` and a ` → \`<target>\`` in the output naming the TypeScript declaration. These rows never affect retrieval, chunk ownership or the call graph. Run `rag-index --reindex` to get the new rows for a project already indexed.
19
+
20
+ ### Changed (breaking)
21
+
22
+ - **One path form for every MCP tool: workspace-relative** — the form `search_codebase`, `find_symbol` and `get_callers` already print (`my-app/src/index.ts`; for a nested-monorepo project the on-disk path, `micros-learning/apps/account/...`). `read_file` takes it instead of an absolute path; `list_files` takes a workspace directory (`.` for the root) and loses its `project` argument; `get_dependencies` / `get_dependents` lose their `project` argument (a passed `project` is ignored). An absolute path is refused with its workspace-relative equivalent, so update prompts or skills that pass absolute paths or `project` to these tools.
23
+
24
+ ### Changed
25
+
26
+ - **A path that names nothing is an error, not an empty result** — every path-taking tool answers `Not found:` with the expected form (example `my-app/src` for `list_files`, `my-app/src/index.ts` elsewhere) and up to three `Did you mean:` candidates, `Not indexed:` (with the reason — excluded file type, ignored directory, `.ragignore`, unselected project, or not yet indexed), `Deleted from disk:`, `Not a file:` / `Not a directory:` or `Cannot read:`, all as MCP error responses. A database or discovery failure is an error response, never "not found".
27
+ - **`Did you mean:` resolves nested-monorepo project names** — a path that starts with a project's name instead of its directory (`micros-learning/account/src/main.ts`) is offered at its on-disk location (`micros-learning/apps/account/src/main.ts`) first, when that file exists; `list_files` given a project name is pointed at the project's directory.
28
+ - **Graph tools span projects** — `get_dependencies`, `get_dependents` and `include_graph_context` list edges into and out of other projects; an edge stored under two overlapping projects prints once; import targets outside the workspace (`../…`) are never printed.
29
+ - **`read_file_range` reads unindexed files** — both read tools read any file under the workspace (symlinks resolved before the boundary check; a symlinked file already in the index stays readable). `start_line` past the end and `end_line` before `start_line` are errors; an unreadable file is `Cannot read:`.
30
+ - **`list_projects` prints each project's directory** as a `path:` line, marked `(not on disk)` when it is gone. `list_files` prints a `**Directory:**` header and marks unreadable subdirectories `(unreadable)`.
31
+ - **`get_dependents` text-search fallback** runs in the project owning the file and reports `failed` / `partial` when grep cannot search every file, instead of `No results.`
32
+ - **Transitive `get_dependencies` / `get_dependents` traverse each reachable file once** — a breadth-first traversal replaces the recursive query, issuing one query per depth level instead of enumerating every import path. A file that is a direct dependency/dependent is no longer also listed under `**Transitive:**` at a larger depth; `via` on a tie is now the first predecessor in UTF-8 byte order, deterministic instead of arbitrary; indirect rows of equal depth are ordered by path in byte order instead of the database's default collation. Direct rows are unchanged.
33
+
34
+ ### Fixed
35
+
36
+ - **Combined and side-effect imports no longer lose edges** — `import Layout, { NavItem } from './layout'` previously recorded only the named import (`NavItem`), losing the default; `import X, * as N from './m'` recorded only the default, losing the namespace import. Each part of an import declaration — default, namespace, each named import — now produces its own edge, in that order. A side-effect import (`import './x'`) now gets a new `depType` `import-side-effect`, where before it produced no edge at all, so `get_dependents` under-reported its target as unused. Default and namespace edges also get `fromSymbol` from the local name, the same rule named imports already use. The embedding prompt is unaffected — side-effect edges are excluded from it, and the other edges already deduped by file. Run `rag-index --reindex` to get the new edges for files already indexed.
37
+
38
+ ### Removed
39
+
40
+ - **Per-project symbol-index version check** — `rag-index` no longer refuses to run with `Indexed before member indexing … run rag-index --reindex`, and `find_symbol`/`get_callers` no longer show that warning. Interrupting the first indexing of a project (ESC/Ctrl+C) used to trigger it on the next run even though every row was already in the current format. If you are upgrading from 0.2.0 or earlier with an existing index, run `rag-index --reindex` once yourself — it is no longer enforced. The `symbol_index_version` table is no longer created or read; an existing one is left in place, unused.
41
+
42
+ ## [0.2.2-alpha] - 2026-10-02
43
+
44
+ ### Fixed
45
+
46
+ - **Global install crashed on startup with `Cannot find module 'typescript'`** — `AstParserService` imported `NodeFlags`/`SyntaxKind` as runtime values from `typescript`, a `devDependency` not installed by `npm install -g`. Now sourced from `ts-morph`, which already bundles its own copy of the compiler — no separate runtime dependency on `typescript`.
47
+
8
48
  ## [0.2.1-alpha] - 2026-10-02
9
49
 
10
50
  ### Added
package/README.md CHANGED
@@ -73,8 +73,13 @@ rag-index
73
73
  rag-index --select # re-run project selection
74
74
  rag-index --reindex # force re-embed all files (use after model change)
75
75
  rag-index --select --reindex
76
+ rag-index --project billing-service # scope this run to one project, by exact name
77
+ rag-index --reindex --project billing-service # force a full reindex of just that project
78
+ rag-index --project # no name — pick one from an interactive prompt
76
79
  ```
77
80
 
81
+ `--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
+
78
83
  ## Monorepo support
79
84
 
80
85
  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.
@@ -97,16 +102,18 @@ See [docs/web-dashboard.md](docs/web-dashboard.md) for the full reference.
97
102
 
98
103
  ## MCP Tools
99
104
 
105
+ Every file or directory argument is **workspace-relative** (relative to `workspaceRoot`, `/`-separated — e.g. `my-app/src/index.ts`, `.` for the root), exactly as `search_codebase` prints it, so a path copied from one tool works in every other. A path that names nothing is an error with the expected form and up to three `Did you mean:` candidates, never an empty result.
106
+
100
107
  | Tool | Description |
101
108
  |------|-------------|
102
- | `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, and `threshold` to override the similarity threshold for that query |
103
- | `list_projects` | List projects with metadata and chunk counts |
104
- | `find_symbol` | Find a declaration or member by exact name — bare, qualified (`Class.method`), quoted, or computed — faster than semantic search |
105
- | `list_files` | Browse the directory tree of a project |
106
- | `read_file` | Read a complete file by absolute path |
107
- | `read_file_range` | Read a specific line range from an indexed file |
108
- | `get_dependencies` | Show what files a given file imports (direct or transitive) |
109
- | `get_dependents` | Show which files import a given file (direct or transitive) |
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, and `rerank` to force reranking on/off for that call regardless of `reranker.enabled` |
110
+ | `list_projects` | List projects with metadata, chunk counts and each project's directory (`path:` line) |
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
+ | `list_files` | Browse the directory tree of any workspace directory |
113
+ | `read_file` | Read a complete file under the workspace, indexed or not |
114
+ | `read_file_range` | Read a specific line range from any file under the workspace |
115
+ | `get_dependencies` | Show what files a given file imports (direct or transitive), across projects |
116
+ | `get_dependents` | Show which files import a given file (direct or transitive), across projects |
110
117
  | `get_callers` | Find all call sites of a function or method by exact name (bare or qualified) |
111
118
 
112
119
  ## Project metadata
package/dist/app.js CHANGED
@@ -41,6 +41,7 @@ const container_1 = require("./src/container");
41
41
  const config_1 = require("./src/config");
42
42
  const config_store_1 = require("./src/config-store");
43
43
  const IndexCommand_1 = require("./src/commands/IndexCommand");
44
+ const EvalCommand_1 = require("./src/commands/EvalCommand");
44
45
  const McpCommand_1 = require("./src/commands/McpCommand");
45
46
  const ConfigCommand_1 = require("./src/commands/ConfigCommand");
46
47
  const WebCommand_1 = require("./src/commands/WebCommand");
@@ -86,17 +87,53 @@ async function main() {
86
87
  const isSetup = argv1 === 'rag-setup' || process.argv[2] === 'setup' || process.argv[2] === 'rag-setup';
87
88
  const isConfig = argv1 === 'rag-config' || process.argv[2] === 'config' || process.argv[2] === 'rag-config';
88
89
  const isWeb = argv1 === 'rag-web' || process.argv[2] === 'web' || process.argv[2] === 'rag-web';
89
- const mode = isIndex ? 'index' : isMcp ? 'mcp' : isSetup ? 'setup' : isConfig ? 'config' : isWeb ? 'web' : (process.argv[2] ?? '');
90
90
  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
+ const evalSubArg = isBinary ? process.argv[2] : process.argv[3];
94
+ const isEval = isIndex && evalSubArg === 'eval';
95
+ const mode = isEval ? 'eval' : isIndex ? 'index' : isMcp ? 'mcp' : isSetup ? 'setup' : isConfig ? 'config' : isWeb ? 'web' : (process.argv[2] ?? '');
96
+ const helpKey = mode === 'eval' ? 'eval' : (isBinary ? argv1 : mode);
91
97
  if (process.argv.includes('--help') || process.argv.includes('-h')) {
92
- (0, help_1.printHelp)(isBinary ? argv1 : mode);
98
+ (0, help_1.printHelp)(helpKey);
93
99
  process.exit(0);
94
100
  }
95
- const effectiveArgs = process.argv.slice(isBinary ? 2 : 3);
101
+ const effectiveArgs = process.argv.slice(isEval ? (isBinary ? 3 : 4) : (isBinary ? 2 : 3));
96
102
  // Arg validation
97
- if (mode === 'index') {
98
- const knownFlags = new Set(['--reindex', '--select']);
99
- const unknown = effectiveArgs.filter(a => !a.startsWith('-') || !knownFlags.has(a));
103
+ if (mode === 'eval') {
104
+ const valueFlags = new Set(['--suite', '--baseline', '--project']);
105
+ const unknown = [];
106
+ for (let i = 0; i < effectiveArgs.length; i++) {
107
+ const a = effectiveArgs[i];
108
+ if (valueFlags.has(a)) {
109
+ const next = effectiveArgs[i + 1];
110
+ if (next !== undefined && !next.startsWith('-'))
111
+ i++; // consume the value
112
+ continue;
113
+ }
114
+ unknown.push(a);
115
+ }
116
+ if (unknown.length > 0) {
117
+ console.error(`Unexpected argument: ${unknown[0]}\n`);
118
+ (0, help_1.printHelp)(helpKey);
119
+ process.exit(1);
120
+ }
121
+ }
122
+ else if (mode === 'index') {
123
+ const boolFlags = new Set(['--reindex', '--select']);
124
+ const unknown = [];
125
+ for (let i = 0; i < effectiveArgs.length; i++) {
126
+ const a = effectiveArgs[i];
127
+ if (boolFlags.has(a))
128
+ continue;
129
+ if (a === '--project') {
130
+ const next = effectiveArgs[i + 1];
131
+ if (next !== undefined && !next.startsWith('-'))
132
+ i++; // consume the name
133
+ continue;
134
+ }
135
+ unknown.push(a);
136
+ }
100
137
  if (unknown.length > 0) {
101
138
  console.error(`Unexpected argument: ${unknown[0]}\n`);
102
139
  (0, help_1.printHelp)(isBinary ? argv1 : 'rag-index');
@@ -137,7 +174,7 @@ async function main() {
137
174
  process.exit(0);
138
175
  }
139
176
  // All other modes require a valid config.json
140
- const loggerName = mode === 'index' ? 'indexer' : mode === 'mcp' ? 'mcp' : mode === 'web' ? 'web' : undefined;
177
+ const loggerName = mode === 'index' || mode === 'eval' ? 'indexer' : mode === 'mcp' ? 'mcp' : mode === 'web' ? 'web' : undefined;
141
178
  let container;
142
179
  try {
143
180
  container = (0, container_1.createContainer)(loggerName);
@@ -161,6 +198,7 @@ async function main() {
161
198
  // never use it.
162
199
  const factories = {
163
200
  index: () => new IndexCommand_1.IndexCommand(container, argv1),
201
+ eval: () => new EvalCommand_1.EvalCommand(container, argv1),
164
202
  mcp: () => new McpCommand_1.McpCommand(container),
165
203
  web: () => new WebCommand_1.WebCommand(container, effectiveArgs),
166
204
  };
@@ -0,0 +1,10 @@
1
+ import type { Container } from '../container';
2
+ import type { ICommand } from './ICommand';
3
+ export declare class EvalCommand implements ICommand {
4
+ private container;
5
+ private argv1;
6
+ constructor(container: Container, argv1: string);
7
+ execute(): Promise<void>;
8
+ private printSummary;
9
+ private printComparison;
10
+ }
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.EvalCommand = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const crypto = __importStar(require("crypto"));
40
+ const child_process_1 = require("child_process");
41
+ const retrieval_eval_1 = require("../retrieval-eval");
42
+ const eval_results_1 = require("../eval-results");
43
+ const DEFAULT_SUITE_DIR = path.join('benchmarks', 'eval');
44
+ const RESULTS_DIR_NAME = path.join('benchmarks', 'results');
45
+ // Resolves to the repo root in both `ts-node app.ts` (one level above `src/`) and
46
+ // the compiled `dist/` layout (two levels above `dist/src/commands/`).
47
+ const _projectRoot = fs.existsSync(path.join(__dirname, '..', '..', 'package.json'))
48
+ ? path.join(__dirname, '..', '..')
49
+ : path.join(__dirname, '..', '..', '..');
50
+ function gitCommit() {
51
+ try {
52
+ return (0, child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd: _projectRoot, encoding: 'utf-8' }).trim();
53
+ }
54
+ catch {
55
+ return 'unknown';
56
+ }
57
+ }
58
+ function hashContent(text) {
59
+ return crypto.createHash('sha256').update(text).digest('hex').slice(0, 16);
60
+ }
61
+ /** `--suite <path>` — a file or a directory of `*.json` suite files (default: `benchmarks/eval/`). */
62
+ function parseSuiteFlag(args) {
63
+ const i = args.indexOf('--suite');
64
+ if (i === -1)
65
+ return DEFAULT_SUITE_DIR;
66
+ const next = args[i + 1];
67
+ if (next === undefined || next.startsWith('-'))
68
+ throw new Error('--suite requires a path');
69
+ return next;
70
+ }
71
+ function parseBaselineFlag(args) {
72
+ const i = args.indexOf('--baseline');
73
+ if (i === -1)
74
+ return null;
75
+ const next = args[i + 1];
76
+ if (next === undefined || next.startsWith('-'))
77
+ throw new Error('--baseline requires a path');
78
+ return next;
79
+ }
80
+ function parseProjectFlag(args) {
81
+ const i = args.indexOf('--project');
82
+ if (i === -1)
83
+ return null;
84
+ const next = args[i + 1];
85
+ if (next === undefined || next.startsWith('-'))
86
+ throw new Error('--project requires a name');
87
+ return next;
88
+ }
89
+ /** Loads one suite file, or every `*.json` file directly under a directory, merged into one. */
90
+ function loadSuite(suitePath) {
91
+ const resolved = path.resolve(_projectRoot, suitePath);
92
+ const stat = fs.statSync(resolved);
93
+ if (stat.isDirectory()) {
94
+ const files = fs.readdirSync(resolved).filter(f => f.endsWith('.json')).sort();
95
+ const parts = files.map(f => fs.readFileSync(path.join(resolved, f), 'utf-8'));
96
+ const suites = parts.map(text => (0, retrieval_eval_1.parseSuite)(JSON.parse(text)));
97
+ return { suite: (0, retrieval_eval_1.mergeSuites)(suites), rawContent: parts.join('\n'), label: 'all' };
98
+ }
99
+ const text = fs.readFileSync(resolved, 'utf-8');
100
+ return { suite: (0, retrieval_eval_1.parseSuite)(JSON.parse(text)), rawContent: text, label: path.basename(resolved, '.json') };
101
+ }
102
+ class EvalCommand {
103
+ container;
104
+ argv1;
105
+ constructor(container, argv1) {
106
+ this.container = container;
107
+ this.argv1 = argv1;
108
+ }
109
+ async execute() {
110
+ const { db, searcher, config } = this.container;
111
+ const args = process.argv.slice(this.argv1 === 'rag-index' ? 3 : 4); // after `rag-index eval` / `index eval`
112
+ let suitePath, baselinePath, projectFilter;
113
+ try {
114
+ suitePath = parseSuiteFlag(args);
115
+ baselinePath = parseBaselineFlag(args);
116
+ projectFilter = parseProjectFlag(args);
117
+ }
118
+ catch (err) {
119
+ console.error(`\n${err.message}`);
120
+ process.exitCode = 1;
121
+ return;
122
+ }
123
+ let suite, rawContent, label;
124
+ try {
125
+ ({ suite, rawContent, label } = loadSuite(suitePath));
126
+ }
127
+ catch (err) {
128
+ if (err instanceof retrieval_eval_1.EvalSuiteError) {
129
+ console.error(`\nInvalid suite: ${err.message}`);
130
+ }
131
+ else {
132
+ console.error(`\nFailed to load suite at '${suitePath}': ${err.message}`);
133
+ }
134
+ process.exitCode = 1;
135
+ return;
136
+ }
137
+ if (projectFilter) {
138
+ suite = { version: 1, cases: suite.cases.filter(c => c.project === projectFilter) };
139
+ }
140
+ if (suite.cases.length === 0) {
141
+ console.error('\nNo cases to evaluate (suite is empty, or --project matched nothing).');
142
+ process.exitCode = 1;
143
+ return;
144
+ }
145
+ await db.initDbWithSchema();
146
+ const knownFilesCache = new Map();
147
+ const inspect = {
148
+ async isFileIndexed(project, file) {
149
+ if (!knownFilesCache.has(project))
150
+ knownFilesCache.set(project, db.listKnownFiles(project));
151
+ const files = await knownFilesCache.get(project);
152
+ return files.includes(file);
153
+ },
154
+ async ownerPathsOfFile(project, file) {
155
+ const rows = await db.query(`SELECT DISTINCT "ownerPath" FROM code_chunk WHERE "serviceName" = $1 AND "filePath" = $2 AND "ownerPath" IS NOT NULL`, [project, file]);
156
+ return rows.map(r => r.ownerPath);
157
+ },
158
+ };
159
+ const search = async (query, limit, project, threshold) => {
160
+ const prepared = await searcher.prepareQuery(query);
161
+ const results = await searcher.searchChunks(prepared.query, limit, project, threshold);
162
+ return results.map(r => ({ filePath: r.filePath, ownerPath: r.ownerPath ?? null }));
163
+ };
164
+ const run = await new retrieval_eval_1.RetrievalEvaluator().evaluate(suite, search, inspect);
165
+ const indexMeta = await db.readIndexMeta();
166
+ const metadata = {
167
+ suitePath,
168
+ suiteHash: hashContent(rawContent),
169
+ embeddingProvider: config.embedding.provider,
170
+ embeddingModel: config.embedding.model,
171
+ rerankerModel: config.reranker.enabled ? config.reranker.model : null,
172
+ indexMeta: indexMeta ? { provider: indexMeta.provider, model: indexMeta.model, dimensions: indexMeta.dimensions } : null,
173
+ gitCommit: gitCommit(),
174
+ timestamp: new Date().toISOString(),
175
+ };
176
+ const resultFile = { metadata, run };
177
+ this.printSummary(resultFile);
178
+ let comparison;
179
+ if (baselinePath) {
180
+ try {
181
+ const baseline = (0, eval_results_1.readResultFile)(path.resolve(_projectRoot, baselinePath));
182
+ const cmp = (0, retrieval_eval_1.compare)(baseline, resultFile);
183
+ comparison = { baselinePath, result: cmp };
184
+ this.printComparison(baselinePath, cmp);
185
+ }
186
+ catch (err) {
187
+ console.error(`\nFailed to read baseline '${baselinePath}': ${err.message}`);
188
+ process.exitCode = 1;
189
+ }
190
+ }
191
+ const resultsDir = path.join(_projectRoot, RESULTS_DIR_NAME);
192
+ const { jsonPath, mdPath } = (0, eval_results_1.writeResultFiles)(resultsDir, label, resultFile, new Date(), comparison);
193
+ console.log(`\nResults written to:\n ${path.relative(_projectRoot, jsonPath)}\n ${path.relative(_projectRoot, mdPath)}`);
194
+ }
195
+ printSummary(result) {
196
+ const { run, metadata } = result;
197
+ console.log(`\nEvaluated ${run.cases.length} case(s) (${run.invalid.length} invalid) against ${metadata.embeddingProvider}/${metadata.embeddingModel}`);
198
+ const o = run.metrics.overall;
199
+ console.log(` Overall — hit@1: ${(o.hit1 * 100).toFixed(1)}% hit@5: ${(o.hit5 * 100).toFixed(1)}% MRR@10: ${o.mrr10.toFixed(4)}`);
200
+ for (const category of ['identifier', 'behaviour', 'concept']) {
201
+ const m = run.metrics.byCategory[category];
202
+ if (m)
203
+ console.log(` ${category.padEnd(10)} — hit@1: ${(m.hit1 * 100).toFixed(1)}% hit@5: ${(m.hit5 * 100).toFixed(1)}% MRR@10: ${m.mrr10.toFixed(4)} (${m.count})`);
204
+ }
205
+ if (run.invalid.length > 0) {
206
+ console.log(`\nInvalid cases:`);
207
+ for (const c of run.invalid)
208
+ console.log(` ${c.id}: ${c.reason} — ${c.invalidExpects.map(e => e.file).join(', ')}`);
209
+ }
210
+ if (run.warnings.length > 0) {
211
+ console.log(`\nWarnings (scored on remaining valid answers):`);
212
+ for (const w of run.warnings)
213
+ console.log(` ${w.id}: dropped ${w.droppedExpects.map(e => e.file).join(', ')}`);
214
+ }
215
+ }
216
+ printComparison(baselinePath, cmp) {
217
+ console.log(`\nComparison against ${baselinePath}:`);
218
+ if (cmp.conditionWarnings.length > 0) {
219
+ for (const w of cmp.conditionWarnings)
220
+ console.log(` ⚠ ${w}`);
221
+ }
222
+ const sign = (n) => (n >= 0 ? '+' : '') + n.toFixed(4);
223
+ console.log(` MRR@10: ${cmp.overall.mrr10.baseline.toFixed(4)} → ${cmp.overall.mrr10.current.toFixed(4)} (${sign(cmp.overall.mrr10.delta)})`);
224
+ console.log(` hit@5: ${(cmp.overall.hit5.baseline * 100).toFixed(1)}% → ${(cmp.overall.hit5.current * 100).toFixed(1)}%`);
225
+ if (cmp.hit5Lost.length > 0)
226
+ console.log(` Lost hit@5: ${cmp.hit5Lost.join(', ')}`);
227
+ if (cmp.hit5Gained.length > 0)
228
+ console.log(` Gained hit@5: ${cmp.hit5Gained.join(', ')}`);
229
+ }
230
+ }
231
+ exports.EvalCommand = EvalCommand;
@@ -5,8 +5,17 @@ const interrupt_1 = require("../interrupt");
5
5
  const project_selector_1 = require("../project-selector");
6
6
  const graph_1 = require("../graph");
7
7
  const MonorepoAwareDiscoveryService_1 = require("../services/MonorepoAwareDiscoveryService");
8
+ const project_scope_1 = require("../project-scope");
8
9
  const file_utils_1 = require("../file-utils");
9
10
  const legacy_ollama_index_1 = require("../services/legacy-ollama-index");
11
+ /** `--project`'s index in `args`; the following token is its name unless absent or itself a flag. */
12
+ function parseProjectFlag(args) {
13
+ const i = args.indexOf('--project');
14
+ if (i === -1)
15
+ return { present: false };
16
+ const next = args[i + 1];
17
+ return next !== undefined && !next.startsWith('-') ? { present: true, name: next } : { present: true };
18
+ }
10
19
  class IndexCommand {
11
20
  container;
12
21
  argv1;
@@ -19,6 +28,42 @@ class IndexCommand {
19
28
  const args = process.argv.slice(this.argv1 === 'rag-index' ? 2 : 3);
20
29
  const force = args.includes('--reindex');
21
30
  const forceSelect = args.includes('--select');
31
+ const projectFlag = parseProjectFlag(args);
32
+ let discovered;
33
+ try {
34
+ discovered = discovery.discover(config.workspaceRoot);
35
+ }
36
+ catch (err) {
37
+ console.error(`\n${err.message}`);
38
+ process.exitCode = 1;
39
+ return;
40
+ }
41
+ // Resolve --project before any database or embedding work, so a bad name
42
+ // (or a cancelled no-name prompt) has zero side effects.
43
+ let scanTargets;
44
+ if (projectFlag.present) {
45
+ let name = projectFlag.name;
46
+ if (name === undefined) {
47
+ if (discovered.length === 0) {
48
+ console.error('\nNo projects available to select.');
49
+ process.exitCode = 1;
50
+ return;
51
+ }
52
+ name = await (0, project_selector_1.selectSingleProject)(discovered);
53
+ if (name === undefined) {
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+ }
58
+ const { targets, notFound } = (0, project_scope_1.resolveProjectNames)([name], discovered);
59
+ if (notFound.length > 0) {
60
+ const available = (0, project_scope_1.listAvailableNames)(discovered).join(', ');
61
+ console.error(`\nProject '${notFound[0]}' not found. Available projects: ${available}`);
62
+ process.exitCode = 1;
63
+ return;
64
+ }
65
+ scanTargets = targets;
66
+ }
22
67
  await db.initDbWithSchema();
23
68
  // A former Ollama installation the config layer cannot recognise (old
24
69
  // defaults, no `embedding`/`ollama` block) is protected here: without an
@@ -43,33 +88,21 @@ class IndexCommand {
43
88
  return;
44
89
  }
45
90
  }
46
- const discovered = discovery.discover(config.workspaceRoot);
47
- const selected = await new project_selector_1.ProjectSelector(config.workspaceRoot).select(discovered, forceSelect);
48
- if (selected.length === 0)
49
- return;
50
- // Expand any selected nested-monorepo unit (e.g. an Nx repo) into its
51
- // sub-projects; ordinary projects pass through unchanged.
52
- const projects = (0, MonorepoAwareDiscoveryService_1.toLeafProjects)(selected);
53
- // A project indexed before member extraction mixes old-format rows (no
54
- // members, bare calleeSymbol) with new ones if left to an incremental run.
55
- // Refuse before any backend readiness check, Docker command, or schema
56
- // work — the same pattern as the legacy-Ollama guard above.
57
- if (!force) {
58
- let outdated;
59
- try {
60
- outdated = await db.listOutdatedProjects(projects.map(p => p.name));
61
- }
62
- catch (err) {
63
- console.error(`\n${err.message}`);
64
- process.exitCode = 1;
65
- return;
66
- }
67
- if (outdated.length > 0) {
68
- console.error(`\nIndexed before member indexing: ${[...outdated].sort().join(', ')} — run \`rag-index --reindex\`.`);
69
- process.exitCode = 1;
91
+ let projects;
92
+ if (scanTargets) {
93
+ projects = scanTargets;
94
+ }
95
+ else {
96
+ const selected = await new project_selector_1.ProjectSelector(config.workspaceRoot).select(discovered, forceSelect);
97
+ if (selected.length === 0)
70
98
  return;
71
- }
99
+ // Expand any selected nested-monorepo unit (e.g. an Nx repo) into its
100
+ // sub-projects; ordinary projects pass through unchanged.
101
+ projects = (0, MonorepoAwareDiscoveryService_1.toLeafProjects)(selected);
72
102
  }
103
+ // The post-index graph always reflects the full workspace, regardless of
104
+ // what this run actually scanned.
105
+ const fullLeafProjects = (0, MonorepoAwareDiscoveryService_1.toLeafProjects)(discovered);
73
106
  // Ensure the local model is served (Infinity only — no-op for cloud), then
74
107
  // resolve the active embedding dimension and reconcile it with index_meta
75
108
  // (recreate code_chunk on a dimension change, clear rows on a model change)
@@ -87,39 +120,32 @@ class IndexCommand {
87
120
  const controller = new AbortController();
88
121
  const teardown = (0, interrupt_1.setupInterrupt)(controller);
89
122
  try {
90
- let pruneResult;
91
- try {
92
- pruneResult = await indexer.pruneProjects(projects.map(p => p.name));
93
- }
94
- catch (err) {
95
- console.error(`\nPruning failed: ${err.message}`);
96
- process.exitCode = 1;
97
- return;
98
- }
99
- const { removed, failed } = pruneResult;
100
- for (const name of removed) {
101
- console.log(`Removed project ${name} (not selected)`);
102
- }
103
- if (failed.length > 0) {
104
- process.exitCode = 1;
123
+ // --project bypasses pruning entirely — it promises "touch only this
124
+ // project," so nothing outside the resolved target(s) is ever removed.
125
+ if (!scanTargets) {
126
+ let pruneResult;
127
+ try {
128
+ pruneResult = await indexer.pruneProjects(projects.map(p => p.name));
129
+ }
130
+ catch (err) {
131
+ console.error(`\nPruning failed: ${err.message}`);
132
+ process.exitCode = 1;
133
+ return;
134
+ }
135
+ const { removed, failed } = pruneResult;
136
+ for (const name of removed) {
137
+ console.log(`Removed project ${name} (not selected)`);
138
+ }
139
+ if (failed.length > 0) {
140
+ process.exitCode = 1;
141
+ }
105
142
  }
106
143
  const filesPerProject = projects.map(p => (0, file_utils_1.collectFiles)(p.path, (0, file_utils_1.loadIgnore)(p.path), p.path));
107
144
  const totalFiles = filesPerProject.reduce((sum, f) => sum + f.length, 0);
108
145
  console.log(`\n${projects.length} projects — ${totalFiles} files total`);
109
146
  if (force)
110
147
  console.log('Reindex mode: all files will be re-indexed.');
111
- const allStats = [];
112
- for (let i = 0; i < projects.length; i++) {
113
- if (controller.signal.aborted)
114
- break;
115
- const stats = await indexer.scanAndIndex(projects[i].path, projects[i].name, force, controller.signal, filesPerProject[i], { rootTsConfigPath: projects[i].rootTsConfigPath ?? undefined });
116
- allStats.push(stats);
117
- const remaining = projects.length - allStats.length;
118
- if (remaining > 0 && !controller.signal.aborted) {
119
- const leftFiles = filesPerProject.slice(allStats.length).reduce((sum, f) => sum + f.length, 0);
120
- console.log(` ${allStats.length}/${projects.length} projects — done in ${stats.elapsedSec}s — ${leftFiles} files left`);
121
- }
122
- }
148
+ const allStats = await indexer.scanProjects(projects, force, controller.signal, filesPerProject);
123
149
  if (controller.signal.aborted)
124
150
  console.log('\nIndexing interrupted.');
125
151
  this.printSummary(allStats);
@@ -130,7 +156,7 @@ class IndexCommand {
130
156
  try {
131
157
  // Real import edges (code_dependency, just written) → intra-repo graph links.
132
158
  const projectEdges = await searcher.getProjectEdges();
133
- (0, graph_1.generateProjectGraph)(projects, projectEdges, config.workspaceRoot);
159
+ (0, graph_1.generateProjectGraph)(fullLeafProjects, projectEdges, config.workspaceRoot);
134
160
  }
135
161
  catch (err) {
136
162
  console.error(` ⚠ Could not build project graph: ${err.message}`);
@@ -1,2 +1,2 @@
1
- export { codeChunk, codeDependency, codeSymbol, codeCall, indexMeta, symbolIndexVersion } from './schema';
1
+ export { codeChunk, codeDependency, codeSymbol, codeCall, indexMeta } from './schema';
2
2
  export type { CodeChunkInsert } from './schema';
@@ -1,10 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.symbolIndexVersion = exports.indexMeta = exports.codeCall = exports.codeSymbol = exports.codeDependency = exports.codeChunk = void 0;
3
+ exports.indexMeta = exports.codeCall = exports.codeSymbol = exports.codeDependency = exports.codeChunk = void 0;
4
4
  var schema_1 = require("./schema");
5
5
  Object.defineProperty(exports, "codeChunk", { enumerable: true, get: function () { return schema_1.codeChunk; } });
6
6
  Object.defineProperty(exports, "codeDependency", { enumerable: true, get: function () { return schema_1.codeDependency; } });
7
7
  Object.defineProperty(exports, "codeSymbol", { enumerable: true, get: function () { return schema_1.codeSymbol; } });
8
8
  Object.defineProperty(exports, "codeCall", { enumerable: true, get: function () { return schema_1.codeCall; } });
9
9
  Object.defineProperty(exports, "indexMeta", { enumerable: true, get: function () { return schema_1.indexMeta; } });
10
- Object.defineProperty(exports, "symbolIndexVersion", { enumerable: true, get: function () { return schema_1.symbolIndexVersion; } });