@code-rag/cli 0.1.7 → 0.1.8
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/dist/commands/benchmark-cmd.d.ts +14 -0
- package/dist/commands/benchmark-cmd.js +198 -0
- package/dist/commands/search.js +9 -37
- package/dist/index.js +2 -0
- package/package.json +4 -4
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: coderag benchmark
|
|
3
|
+
*
|
|
4
|
+
* Auto-generates a benchmark dataset from an indexed project and evaluates
|
|
5
|
+
* CodeRAG search quality against it. Prints a summary table and optionally
|
|
6
|
+
* saves a detailed JSON report.
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import type { BenchmarkReport } from '@code-rag/core';
|
|
10
|
+
/**
|
|
11
|
+
* Format a BenchmarkReport as a colored terminal summary.
|
|
12
|
+
*/
|
|
13
|
+
export declare function formatColoredSummary(report: BenchmarkReport): string;
|
|
14
|
+
export declare function registerBenchmarkCommand(program: Command): void;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: coderag benchmark
|
|
3
|
+
*
|
|
4
|
+
* Auto-generates a benchmark dataset from an indexed project and evaluates
|
|
5
|
+
* CodeRAG search quality against it. Prints a summary table and optionally
|
|
6
|
+
* saves a detailed JSON report.
|
|
7
|
+
*/
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { writeFile, readFile } from 'node:fs/promises';
|
|
10
|
+
import { resolve, join } from 'node:path';
|
|
11
|
+
import { createRuntime, parseIndexRows, buildCallerMap, buildTestMap, generateQueries, runBenchmark, } from '@code-rag/core';
|
|
12
|
+
/**
|
|
13
|
+
* Format a BenchmarkReport as a colored terminal summary.
|
|
14
|
+
*/
|
|
15
|
+
export function formatColoredSummary(report) {
|
|
16
|
+
const lines = [];
|
|
17
|
+
const a = report.aggregate;
|
|
18
|
+
lines.push(chalk.bold('Benchmark Results'));
|
|
19
|
+
lines.push(chalk.dim('================='));
|
|
20
|
+
lines.push(`Queries: ${chalk.cyan(String(report.metadata.totalQueries))}`);
|
|
21
|
+
lines.push(`Index size: ${chalk.cyan(String(report.metadata.totalChunksInIndex))} chunks`);
|
|
22
|
+
lines.push(`Duration: ${chalk.cyan((report.metadata.durationMs / 1000).toFixed(1))}s`);
|
|
23
|
+
lines.push('');
|
|
24
|
+
lines.push(chalk.bold('Aggregate Metrics:'));
|
|
25
|
+
lines.push(` P@5: ${colorMetric(a.precisionAt5)}`);
|
|
26
|
+
lines.push(` P@10: ${colorMetric(a.precisionAt10)}`);
|
|
27
|
+
lines.push(` Recall@10: ${colorMetric(a.recallAt10)}`);
|
|
28
|
+
lines.push(` MRR: ${colorMetric(a.mrr)}`);
|
|
29
|
+
lines.push(` nDCG@10: ${colorMetric(a.ndcgAt10)}`);
|
|
30
|
+
if (report.byQueryType.length > 0) {
|
|
31
|
+
lines.push('');
|
|
32
|
+
lines.push(chalk.bold('By Query Type:'));
|
|
33
|
+
lines.push(chalk.dim(' Type | Count | P@5 | P@10 | R@10 | MRR | nDCG@10'));
|
|
34
|
+
lines.push(chalk.dim(' ---------------------|-------|-------|-------|-------|-------|--------'));
|
|
35
|
+
for (const bt of report.byQueryType) {
|
|
36
|
+
const m = bt.metrics;
|
|
37
|
+
const type = bt.queryType.padEnd(20);
|
|
38
|
+
lines.push(` ${chalk.cyan(type)} | ${String(m.queryCount).padStart(5)} | ${fmt(m.precisionAt5)} | ${fmt(m.precisionAt10)} | ${fmt(m.recallAt10)} | ${fmt(m.mrr)} | ${fmt(m.ndcgAt10)}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return lines.join('\n');
|
|
42
|
+
}
|
|
43
|
+
function colorMetric(value) {
|
|
44
|
+
const formatted = value.toFixed(4).padStart(6);
|
|
45
|
+
if (value >= 0.7)
|
|
46
|
+
return chalk.green(formatted);
|
|
47
|
+
if (value >= 0.4)
|
|
48
|
+
return chalk.yellow(formatted);
|
|
49
|
+
return chalk.red(formatted);
|
|
50
|
+
}
|
|
51
|
+
function fmt(value) {
|
|
52
|
+
return value.toFixed(4).padStart(6);
|
|
53
|
+
}
|
|
54
|
+
export function registerBenchmarkCommand(program) {
|
|
55
|
+
program
|
|
56
|
+
.command('benchmark')
|
|
57
|
+
.description('Auto-generate benchmarks from the index and evaluate search quality')
|
|
58
|
+
.option('--queries <n>', 'Number of benchmark queries to generate', '100')
|
|
59
|
+
.option('--output <path>', 'Save detailed JSON report to this path')
|
|
60
|
+
.option('--top-k <n>', 'Number of search results per query', '10')
|
|
61
|
+
.option('--seed <n>', 'Random seed for reproducible generation', '42')
|
|
62
|
+
.action(async (options) => {
|
|
63
|
+
try {
|
|
64
|
+
const rootDir = process.cwd();
|
|
65
|
+
const queryCount = parseInt(options.queries, 10);
|
|
66
|
+
const topK = parseInt(options.topK, 10);
|
|
67
|
+
const seed = parseInt(options.seed, 10);
|
|
68
|
+
if (isNaN(queryCount) || queryCount < 1) {
|
|
69
|
+
// eslint-disable-next-line no-console
|
|
70
|
+
console.error(chalk.red('Invalid --queries value. Must be a positive integer.'));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
if (isNaN(topK) || topK < 1) {
|
|
74
|
+
// eslint-disable-next-line no-console
|
|
75
|
+
console.error(chalk.red('Invalid --top-k value. Must be a positive integer.'));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
if (isNaN(seed)) {
|
|
79
|
+
// eslint-disable-next-line no-console
|
|
80
|
+
console.error(chalk.red('Invalid --seed value. Must be an integer.'));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
// --- Initialize runtime ---
|
|
84
|
+
// eslint-disable-next-line no-console
|
|
85
|
+
console.log(chalk.dim('Initializing runtime...'));
|
|
86
|
+
const runtimeResult = await createRuntime({ rootDir, searchOnly: true });
|
|
87
|
+
if (runtimeResult.isErr()) {
|
|
88
|
+
// eslint-disable-next-line no-console
|
|
89
|
+
console.error(chalk.red('Initialization failed.'), runtimeResult.error.message);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
const runtime = runtimeResult.value;
|
|
93
|
+
// --- Scan index ---
|
|
94
|
+
// eslint-disable-next-line no-console
|
|
95
|
+
console.log(chalk.dim('Scanning index...'));
|
|
96
|
+
const allRowsResult = await runtime.store.getAll();
|
|
97
|
+
if (allRowsResult.isErr()) {
|
|
98
|
+
runtime.close();
|
|
99
|
+
// eslint-disable-next-line no-console
|
|
100
|
+
console.error(chalk.red('Failed to scan index:'), allRowsResult.error.message);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
const rows = allRowsResult.value;
|
|
104
|
+
if (rows.length === 0) {
|
|
105
|
+
runtime.close();
|
|
106
|
+
// eslint-disable-next-line no-console
|
|
107
|
+
console.log(chalk.yellow('Index is empty. Run "coderag index" first.'));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const scanResult = parseIndexRows(rows);
|
|
111
|
+
if (scanResult.isErr()) {
|
|
112
|
+
runtime.close();
|
|
113
|
+
// eslint-disable-next-line no-console
|
|
114
|
+
console.error(chalk.red('Failed to parse index:'), scanResult.error.message);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
const scan = scanResult.value;
|
|
118
|
+
// eslint-disable-next-line no-console
|
|
119
|
+
console.log(chalk.dim(`Found ${scan.totalChunks} chunks in index.`));
|
|
120
|
+
// --- Load dependency graph ---
|
|
121
|
+
let edges = [];
|
|
122
|
+
const storagePath = resolve(rootDir, runtime.config.storage.path);
|
|
123
|
+
const graphPath = join(storagePath, 'graph.json');
|
|
124
|
+
try {
|
|
125
|
+
const graphData = await readFile(graphPath, 'utf-8');
|
|
126
|
+
const parsed = JSON.parse(graphData);
|
|
127
|
+
if (parsed !== null &&
|
|
128
|
+
typeof parsed === 'object' &&
|
|
129
|
+
'edges' in parsed &&
|
|
130
|
+
Array.isArray(parsed['edges'])) {
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Runtime-checked graph JSON structure
|
|
132
|
+
edges = parsed.edges;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// No graph available, proceed with empty edges
|
|
137
|
+
// eslint-disable-next-line no-console
|
|
138
|
+
console.log(chalk.dim('No dependency graph found, skipping caller/import queries.'));
|
|
139
|
+
}
|
|
140
|
+
// --- Build relationship maps ---
|
|
141
|
+
const callerMap = buildCallerMap(edges);
|
|
142
|
+
const testMap = buildTestMap(scan.fileToChunkIds);
|
|
143
|
+
// --- Generate queries ---
|
|
144
|
+
// eslint-disable-next-line no-console
|
|
145
|
+
console.log(chalk.dim(`Generating ${queryCount} benchmark queries...`));
|
|
146
|
+
const queries = generateQueries(scan, edges, callerMap, testMap, { maxQueries: queryCount }, seed);
|
|
147
|
+
if (queries.length === 0) {
|
|
148
|
+
runtime.close();
|
|
149
|
+
// eslint-disable-next-line no-console
|
|
150
|
+
console.log(chalk.yellow('Could not generate any benchmark queries from this index.'));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// eslint-disable-next-line no-console
|
|
154
|
+
console.log(chalk.dim(`Generated ${queries.length} queries. Running evaluation...`));
|
|
155
|
+
// --- Run benchmark ---
|
|
156
|
+
const searchFn = async (query) => {
|
|
157
|
+
const result = await runtime.hybridSearch.search(query, { topK });
|
|
158
|
+
if (result.isErr())
|
|
159
|
+
return [];
|
|
160
|
+
return result.value.map((r) => r.chunkId);
|
|
161
|
+
};
|
|
162
|
+
const reportResult = await runBenchmark(queries, searchFn, scan.totalChunks, (completed, total) => {
|
|
163
|
+
if (completed % 10 === 0 || completed === total) {
|
|
164
|
+
// eslint-disable-next-line no-console
|
|
165
|
+
process.stdout.write(`\r${chalk.dim(` Progress: ${completed}/${total}`)}`);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
runtime.close();
|
|
169
|
+
if (reportResult.isErr()) {
|
|
170
|
+
// eslint-disable-next-line no-console
|
|
171
|
+
console.error(chalk.red('\nBenchmark failed:'), reportResult.error.message);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
const report = reportResult.value;
|
|
175
|
+
// --- Print summary ---
|
|
176
|
+
// eslint-disable-next-line no-console
|
|
177
|
+
console.log('\n');
|
|
178
|
+
// eslint-disable-next-line no-console
|
|
179
|
+
console.log(formatColoredSummary(report));
|
|
180
|
+
// --- Save JSON report ---
|
|
181
|
+
if (options.output) {
|
|
182
|
+
const outputPath = resolve(options.output);
|
|
183
|
+
const jsonReport = JSON.stringify(report, null, 2);
|
|
184
|
+
await writeFile(outputPath, jsonReport, 'utf-8');
|
|
185
|
+
// eslint-disable-next-line no-console
|
|
186
|
+
console.log('');
|
|
187
|
+
// eslint-disable-next-line no-console
|
|
188
|
+
console.log(chalk.green(`Detailed report saved to: ${outputPath}`));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
193
|
+
// eslint-disable-next-line no-console
|
|
194
|
+
console.error(chalk.red('Benchmark failed:'), message);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
package/dist/commands/search.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import {
|
|
3
|
-
import { join, resolve, sep } from 'node:path';
|
|
4
|
-
import { loadConfig, OllamaEmbeddingProvider, LanceDBStore, BM25Index, HybridSearch, } from '@code-rag/core';
|
|
2
|
+
import { createRuntime, } from '@code-rag/core';
|
|
5
3
|
/**
|
|
6
4
|
* Format a single search result for terminal display.
|
|
7
5
|
*/
|
|
@@ -39,44 +37,18 @@ export function registerSearchCommand(program) {
|
|
|
39
37
|
console.error(chalk.red('Invalid --top-k value. Must be a positive integer.'));
|
|
40
38
|
process.exit(1);
|
|
41
39
|
}
|
|
42
|
-
//
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
40
|
+
// Initialize runtime (search-only mode: skip graph, reranker, context expander)
|
|
41
|
+
const runtimeResult = await createRuntime({ rootDir, searchOnly: true });
|
|
42
|
+
if (runtimeResult.isErr()) {
|
|
45
43
|
// eslint-disable-next-line no-console
|
|
46
|
-
console.error(chalk.red('
|
|
44
|
+
console.error(chalk.red('Initialization failed.'), runtimeResult.error.message);
|
|
47
45
|
process.exit(1);
|
|
48
46
|
}
|
|
49
|
-
const
|
|
50
|
-
const storagePath = resolve(rootDir, config.storage.path);
|
|
51
|
-
// Prevent path traversal outside project root
|
|
52
|
-
if (!storagePath.startsWith(resolve(rootDir) + sep) && storagePath !== resolve(rootDir)) {
|
|
53
|
-
// eslint-disable-next-line no-console
|
|
54
|
-
console.error(chalk.red('Storage path escapes project root'));
|
|
55
|
-
process.exit(1);
|
|
56
|
-
}
|
|
57
|
-
// Create services
|
|
58
|
-
const embeddingProvider = new OllamaEmbeddingProvider({
|
|
59
|
-
model: config.embedding.model,
|
|
60
|
-
dimensions: config.embedding.dimensions,
|
|
61
|
-
});
|
|
62
|
-
const store = new LanceDBStore(storagePath, config.embedding.dimensions);
|
|
63
|
-
await store.connect();
|
|
64
|
-
// Load BM25 index
|
|
65
|
-
let bm25 = new BM25Index();
|
|
66
|
-
const bm25Path = join(storagePath, 'bm25-index.json');
|
|
67
|
-
try {
|
|
68
|
-
const bm25Data = await readFile(bm25Path, 'utf-8');
|
|
69
|
-
bm25 = BM25Index.deserialize(bm25Data);
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
// No BM25 index yet
|
|
73
|
-
}
|
|
74
|
-
// Create hybrid search
|
|
75
|
-
const hybridSearch = new HybridSearch(store, bm25, embeddingProvider, config.search);
|
|
47
|
+
const runtime = runtimeResult.value;
|
|
76
48
|
// Run search
|
|
77
|
-
const searchResult = await hybridSearch.search(query, { topK });
|
|
49
|
+
const searchResult = await runtime.hybridSearch.search(query, { topK });
|
|
78
50
|
if (searchResult.isErr()) {
|
|
79
|
-
|
|
51
|
+
runtime.close();
|
|
80
52
|
// eslint-disable-next-line no-console
|
|
81
53
|
console.error(chalk.red('Search failed:'), searchResult.error.message);
|
|
82
54
|
process.exit(1);
|
|
@@ -95,7 +67,7 @@ export function registerSearchCommand(program) {
|
|
|
95
67
|
const fileFilter = options.file.toLowerCase();
|
|
96
68
|
results = results.filter((r) => (r.chunk?.filePath ?? '').toLowerCase().includes(fileFilter));
|
|
97
69
|
}
|
|
98
|
-
|
|
70
|
+
runtime.close();
|
|
99
71
|
// Display results
|
|
100
72
|
if (results.length === 0) {
|
|
101
73
|
// eslint-disable-next-line no-console
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { registerStatusCommand } from './commands/status.js';
|
|
|
9
9
|
import { registerViewerCommand } from './commands/viewer.js';
|
|
10
10
|
import { registerWatchCommand } from './commands/watch-cmd.js';
|
|
11
11
|
import { registerHooksCommand } from './commands/hooks-cmd.js';
|
|
12
|
+
import { registerBenchmarkCommand } from './commands/benchmark-cmd.js';
|
|
12
13
|
const require = createRequire(import.meta.url);
|
|
13
14
|
const pkg = require('../package.json');
|
|
14
15
|
const program = new Command();
|
|
@@ -24,4 +25,5 @@ registerStatusCommand(program);
|
|
|
24
25
|
registerViewerCommand(program);
|
|
25
26
|
registerWatchCommand(program);
|
|
26
27
|
registerHooksCommand(program);
|
|
28
|
+
registerBenchmarkCommand(program);
|
|
27
29
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-rag/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "CLI tool for CodeRAG — init, index, search, serve, and status commands for codebase context engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
"commander": "^13.1.0",
|
|
49
49
|
"ora": "^8.2.0",
|
|
50
50
|
"yaml": "^2.7.0",
|
|
51
|
-
"@code-rag/api-server": "0.1.
|
|
52
|
-
"@code-rag/core": "0.1.
|
|
53
|
-
"@code-rag/mcp-server": "0.1.
|
|
51
|
+
"@code-rag/api-server": "0.1.8",
|
|
52
|
+
"@code-rag/core": "0.1.8",
|
|
53
|
+
"@code-rag/mcp-server": "0.1.8"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/node": "^22.13.4",
|