codesearch-mcp 0.2.2-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 +65 -1
- package/README.md +60 -33
- package/dist/app.js +46 -22
- 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 +11 -0
- package/dist/src/commands/EvalCommand.js +264 -0
- package/dist/src/commands/IndexCommand.js +70 -66
- 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/index.d.ts +1 -1
- package/dist/src/db/index.js +1 -2
- package/dist/src/db/schema.d.ts +22 -29
- package/dist/src/db/schema.js +3 -14
- 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 +10 -0
- package/dist/src/dependency-traversal.js +39 -0
- package/dist/src/enriched-prompt.d.ts +0 -5
- package/dist/src/enriched-prompt.js +0 -5
- 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 +15 -0
- package/dist/src/eval-results.js +397 -0
- package/dist/src/file-utils.d.ts +10 -0
- package/dist/src/file-utils.js +19 -1
- package/dist/src/graph.d.ts +0 -25
- package/dist/src/graph.js +0 -28
- package/dist/src/help.js +4 -1
- package/dist/src/hnsw-settings.d.ts +3 -0
- package/dist/src/hnsw-settings.js +24 -0
- package/dist/src/interfaces/IAstParser.d.ts +5 -30
- 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 -36
- 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 +216 -0
- package/dist/src/interfaces/IRetrievalEvaluator.js +2 -0
- package/dist/src/interfaces/ISearcher.d.ts +26 -20
- package/dist/src/interfaces/index.d.ts +1 -0
- 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 +7 -0
- package/dist/src/project-scope.js +37 -0
- package/dist/src/project-selector.d.ts +1 -0
- package/dist/src/project-selector.js +13 -6
- package/dist/src/retrieval-eval.d.ts +24 -0
- package/dist/src/retrieval-eval.js +500 -0
- 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 +28 -23
- package/dist/src/services/AstParserService.js +391 -98
- package/dist/src/services/DatabaseService.d.ts +2 -19
- package/dist/src/services/DatabaseService.js +44 -81
- package/dist/src/services/EmbedderFactory.d.ts +0 -4
- package/dist/src/services/EmbedderFactory.js +0 -5
- package/dist/src/services/IndexerService.d.ts +2 -10
- package/dist/src/services/IndexerService.js +29 -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 +25 -10
- package/dist/src/services/McpServer.js +101 -64
- 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 +1 -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 +127 -70
- package/dist/src/services/SearcherService.js +526 -305
- 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 +2 -11
- package/dist/src/symbol-rows.js +5 -13
- package/dist/src/tool-call-log.d.ts +2 -0
- package/dist/src/tool-call-log.js +24 -0
- package/dist/src/tools/FindSymbolTool.js +14 -5
- package/dist/src/tools/GetCallersTool.d.ts +3 -1
- package/dist/src/tools/GetCallersTool.js +16 -6
- package/dist/src/tools/GetDependenciesTool.d.ts +3 -1
- package/dist/src/tools/GetDependenciesTool.js +15 -8
- package/dist/src/tools/GetDependentsTool.d.ts +3 -1
- package/dist/src/tools/GetDependentsTool.js +31 -16
- package/dist/src/tools/ListFilesTool.d.ts +3 -1
- package/dist/src/tools/ListFilesTool.js +14 -12
- package/dist/src/tools/ListProjectsTool.js +5 -1
- package/dist/src/tools/ReadFileRangeTool.d.ts +3 -1
- package/dist/src/tools/ReadFileRangeTool.js +12 -11
- package/dist/src/tools/ReadFileTool.d.ts +3 -1
- package/dist/src/tools/ReadFileTool.js +12 -10
- package/dist/src/tools/SearchCodebaseTool.js +7 -2
- package/dist/src/tools/format-helpers.d.ts +10 -1
- package/dist/src/tools/format-helpers.js +145 -8
- package/dist/src/web/dashboard.d.ts +0 -8
- package/dist/src/web/dashboard.js +7 -10
- package/package.json +10 -9
- package/dist/src/services/AstChunker.d.ts +0 -42
- package/dist/src/services/AstChunker.js +0 -158
- package/docs/web-dashboard.md +0 -206
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveProjectNames = resolveProjectNames;
|
|
4
|
+
exports.listAvailableNames = listAvailableNames;
|
|
5
|
+
const MonorepoAwareDiscoveryService_1 = require("./services/MonorepoAwareDiscoveryService");
|
|
6
|
+
function resolveProjectNames(names, units) {
|
|
7
|
+
const leaves = (0, MonorepoAwareDiscoveryService_1.toLeafProjects)(units);
|
|
8
|
+
const targets = [];
|
|
9
|
+
const seen = new Set();
|
|
10
|
+
const notFound = [];
|
|
11
|
+
for (const name of names) {
|
|
12
|
+
const unit = units.find(u => u.name === name);
|
|
13
|
+
const matches = unit ? (0, MonorepoAwareDiscoveryService_1.toLeafProjects)([unit]) : [leaves.find(l => l.name === name)].filter((p) => p !== undefined);
|
|
14
|
+
if (matches.length === 0) {
|
|
15
|
+
notFound.push(name);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
for (const leaf of matches) {
|
|
19
|
+
if (seen.has(leaf.name))
|
|
20
|
+
continue;
|
|
21
|
+
seen.add(leaf.name);
|
|
22
|
+
targets.push(leaf);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { targets, notFound };
|
|
26
|
+
}
|
|
27
|
+
function listAvailableNames(units) {
|
|
28
|
+
const names = [];
|
|
29
|
+
for (const unit of units) {
|
|
30
|
+
names.push(unit.name);
|
|
31
|
+
if (unit.children && unit.children.length > 0) {
|
|
32
|
+
for (const child of unit.children)
|
|
33
|
+
names.push(child.name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return names;
|
|
37
|
+
}
|
|
@@ -34,9 +34,22 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.ProjectSelector = void 0;
|
|
37
|
+
exports.selectSingleProject = selectSingleProject;
|
|
37
38
|
const fs = __importStar(require("fs"));
|
|
38
39
|
const path = __importStar(require("path"));
|
|
39
40
|
const prompts_1 = require("@inquirer/prompts");
|
|
41
|
+
async function selectSingleProject(units) {
|
|
42
|
+
try {
|
|
43
|
+
return await (0, prompts_1.select)({
|
|
44
|
+
message: 'Select a project to index',
|
|
45
|
+
choices: units.map(p => ({ name: p.name, value: p.name })),
|
|
46
|
+
pageSize: 15,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
40
53
|
async function loadLines(filePath) {
|
|
41
54
|
try {
|
|
42
55
|
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
@@ -48,9 +61,6 @@ async function loadLines(filePath) {
|
|
|
48
61
|
return new Set();
|
|
49
62
|
}
|
|
50
63
|
}
|
|
51
|
-
// Write the set if non-empty, otherwise delete the file so state is deterministic.
|
|
52
|
-
// Previously .ragskip could be left stale (last entry removed but file not rewritten);
|
|
53
|
-
// treating both files the same way avoids that drift.
|
|
54
64
|
async function saveLines(filePath, names) {
|
|
55
65
|
if (names.size > 0) {
|
|
56
66
|
await fs.promises.writeFile(filePath, [...names].sort().join('\n') + '\n');
|
|
@@ -76,13 +86,10 @@ class ProjectSelector {
|
|
|
76
86
|
await this.load();
|
|
77
87
|
const newProjects = allProjects.filter(p => !this.included.has(p.name) && !this.skipped.has(p.name));
|
|
78
88
|
const previouslySelected = allProjects.filter(p => this.included.has(p.name));
|
|
79
|
-
// Nothing new and something already selected — return silently
|
|
80
89
|
if (newProjects.length === 0 && !forceSelect && previouslySelected.length > 0) {
|
|
81
90
|
return previouslySelected;
|
|
82
91
|
}
|
|
83
|
-
// No previous selection — treat as implicit --select so the user picks projects now
|
|
84
92
|
const autoSelect = !forceSelect && previouslySelected.length === 0;
|
|
85
|
-
// In --select / auto-select mode show all non-skipped projects; otherwise only new ones
|
|
86
93
|
const toShow = (forceSelect || autoSelect)
|
|
87
94
|
? allProjects.filter(p => !this.skipped.has(p.name))
|
|
88
95
|
: newProjects;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { EvalSuite, EvalCase, EvalCategory, CorpusRef, SearchFn, IndexInspector, IRetrievalEvaluator, EvalOutcome, StageName, PairedChange, CaseProgress, EvalResultFile, CompareResult } from './interfaces/IRetrievalEvaluator';
|
|
2
|
+
export declare const STAGE_ORDER: readonly StageName[];
|
|
3
|
+
export declare class EvalSuiteError extends Error {
|
|
4
|
+
readonly caseId: string | null;
|
|
5
|
+
readonly field: string | null;
|
|
6
|
+
constructor(message: string, caseId: string | null, field: string | null);
|
|
7
|
+
}
|
|
8
|
+
export declare function parseSuite(raw: unknown): EvalSuite;
|
|
9
|
+
export declare function corporaOf(suites: EvalSuite[]): CorpusRef[];
|
|
10
|
+
export declare function isUnderRoot(dir: string, root: string): boolean;
|
|
11
|
+
export declare function checkCaseCorpora(cases: EvalCase[], projectDir: (project: string) => Promise<string | null>): Promise<void>;
|
|
12
|
+
export declare function mergeSuites(suites: EvalSuite[]): EvalSuite;
|
|
13
|
+
export declare function signTest(improved: number, worsened: number): number;
|
|
14
|
+
export interface RankedCase {
|
|
15
|
+
id: string;
|
|
16
|
+
category: EvalCategory;
|
|
17
|
+
rank: number | null;
|
|
18
|
+
}
|
|
19
|
+
export declare function pairedChange(from: RankedCase[], to: RankedCase[]): PairedChange;
|
|
20
|
+
export declare const LIVE_GROUP = "live";
|
|
21
|
+
export declare class RetrievalEvaluator implements IRetrievalEvaluator {
|
|
22
|
+
evaluate(suite: EvalSuite, search: SearchFn, inspect: IndexInspector, onCase?: (p: CaseProgress) => void): Promise<EvalOutcome>;
|
|
23
|
+
}
|
|
24
|
+
export declare function compare(baseline: EvalResultFile, current: EvalResultFile): CompareResult;
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RetrievalEvaluator = exports.LIVE_GROUP = exports.EvalSuiteError = exports.STAGE_ORDER = void 0;
|
|
4
|
+
exports.parseSuite = parseSuite;
|
|
5
|
+
exports.corporaOf = corporaOf;
|
|
6
|
+
exports.isUnderRoot = isUnderRoot;
|
|
7
|
+
exports.checkCaseCorpora = checkCaseCorpora;
|
|
8
|
+
exports.mergeSuites = mergeSuites;
|
|
9
|
+
exports.signTest = signTest;
|
|
10
|
+
exports.pairedChange = pairedChange;
|
|
11
|
+
exports.compare = compare;
|
|
12
|
+
const zod_1 = require("zod");
|
|
13
|
+
const CATEGORIES = ['identifier', 'behaviour', 'concept'];
|
|
14
|
+
exports.STAGE_ORDER = ['vector', 'reranked'];
|
|
15
|
+
const STAGE_SOURCE = { reranked: 'vector' };
|
|
16
|
+
const UNRANKED = 11;
|
|
17
|
+
class EvalSuiteError extends Error {
|
|
18
|
+
caseId;
|
|
19
|
+
field;
|
|
20
|
+
constructor(message, caseId, field) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.caseId = caseId;
|
|
23
|
+
this.field = field;
|
|
24
|
+
this.name = 'EvalSuiteError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.EvalSuiteError = EvalSuiteError;
|
|
28
|
+
const ExpectedAnswerSchema = zod_1.z.object({
|
|
29
|
+
file: zod_1.z.string().min(1),
|
|
30
|
+
owner: zod_1.z.string().min(1).optional(),
|
|
31
|
+
});
|
|
32
|
+
const CaseSchema = zod_1.z.object({
|
|
33
|
+
id: zod_1.z.string().min(1),
|
|
34
|
+
project: zod_1.z.string().min(1),
|
|
35
|
+
category: zod_1.z.enum(['identifier', 'behaviour', 'concept']),
|
|
36
|
+
query: zod_1.z.string().min(1),
|
|
37
|
+
expect: zod_1.z.array(ExpectedAnswerSchema).min(1),
|
|
38
|
+
threshold: zod_1.z.number().min(0).max(1).optional(),
|
|
39
|
+
});
|
|
40
|
+
const CorpusSchema = zod_1.z.object({
|
|
41
|
+
root: zod_1.z.string().min(1).refine(r => !r.startsWith('/') && !r.split('/').includes('..') && !r.endsWith('/'), 'must be a workspace-relative directory'),
|
|
42
|
+
commit: zod_1.z.string().regex(/^[0-9a-f]{40}$/, 'must be a full 40-hex commit SHA'),
|
|
43
|
+
}).strict();
|
|
44
|
+
const SuiteSchema = zod_1.z.object({
|
|
45
|
+
version: zod_1.z.literal(1),
|
|
46
|
+
corpus: CorpusSchema.optional(),
|
|
47
|
+
cases: zod_1.z.array(CaseSchema),
|
|
48
|
+
});
|
|
49
|
+
function rawCaseId(raw, index) {
|
|
50
|
+
if (typeof raw !== 'object' || raw === null)
|
|
51
|
+
return null;
|
|
52
|
+
const cases = raw.cases;
|
|
53
|
+
if (!Array.isArray(cases))
|
|
54
|
+
return null;
|
|
55
|
+
const entry = cases[index];
|
|
56
|
+
if (typeof entry !== 'object' || entry === null)
|
|
57
|
+
return null;
|
|
58
|
+
const id = entry.id;
|
|
59
|
+
return typeof id === 'string' ? id : null;
|
|
60
|
+
}
|
|
61
|
+
function parseSuite(raw) {
|
|
62
|
+
const result = SuiteSchema.safeParse(raw);
|
|
63
|
+
if (!result.success) {
|
|
64
|
+
const issue = result.error.issues[0];
|
|
65
|
+
const pathParts = issue.path;
|
|
66
|
+
const caseIndex = typeof pathParts[1] === 'number' ? pathParts[1] : null;
|
|
67
|
+
const caseId = caseIndex !== null ? rawCaseId(raw, caseIndex) ?? `#${caseIndex}` : null;
|
|
68
|
+
const field = pathParts.length > 2 ? String(pathParts[pathParts.length - 1]) : pathParts.length === 2 ? 'id' : String(pathParts[0] ?? '');
|
|
69
|
+
throw new EvalSuiteError(`${issue.message} (case ${caseId ?? '?'}, field ${field})`, caseId, field);
|
|
70
|
+
}
|
|
71
|
+
const seen = new Set();
|
|
72
|
+
for (const c of result.data.cases) {
|
|
73
|
+
if (seen.has(c.id)) {
|
|
74
|
+
throw new EvalSuiteError(`Duplicate case id (case ${c.id}, field id)`, c.id, 'id');
|
|
75
|
+
}
|
|
76
|
+
seen.add(c.id);
|
|
77
|
+
}
|
|
78
|
+
const corpus = result.data.corpus?.root ?? null;
|
|
79
|
+
return { ...result.data, cases: result.data.cases.map(c => ({ ...c, corpus })) };
|
|
80
|
+
}
|
|
81
|
+
function corporaOf(suites) {
|
|
82
|
+
const byRoot = new Map();
|
|
83
|
+
for (const suite of suites) {
|
|
84
|
+
if (!suite.corpus)
|
|
85
|
+
continue;
|
|
86
|
+
const prev = byRoot.get(suite.corpus.root);
|
|
87
|
+
if (prev && prev.commit !== suite.corpus.commit) {
|
|
88
|
+
throw new EvalSuiteError(`Corpus ${suite.corpus.root} is declared at two commits: ${prev.commit} and ${suite.corpus.commit}`, null, 'corpus');
|
|
89
|
+
}
|
|
90
|
+
byRoot.set(suite.corpus.root, { root: suite.corpus.root, commit: suite.corpus.commit });
|
|
91
|
+
}
|
|
92
|
+
return [...byRoot.values()].sort((a, b) => (a.root < b.root ? -1 : a.root > b.root ? 1 : 0));
|
|
93
|
+
}
|
|
94
|
+
function isUnderRoot(dir, root) {
|
|
95
|
+
return dir === root || dir.startsWith(root + '/');
|
|
96
|
+
}
|
|
97
|
+
async function checkCaseCorpora(cases, projectDir) {
|
|
98
|
+
for (const c of cases) {
|
|
99
|
+
if (!c.corpus)
|
|
100
|
+
continue;
|
|
101
|
+
const dir = await projectDir(c.project);
|
|
102
|
+
if (dir === null) {
|
|
103
|
+
throw new EvalSuiteError(`Project ${c.project} is not indexed, so it cannot be checked against corpus ${c.corpus} (case ${c.id}, field project)`, c.id, 'project');
|
|
104
|
+
}
|
|
105
|
+
if (!isUnderRoot(dir, c.corpus)) {
|
|
106
|
+
throw new EvalSuiteError(`Project ${c.project} (${dir}) is outside corpus ${c.corpus} (case ${c.id}, field project)`, c.id, 'project');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function mergeSuites(suites) {
|
|
111
|
+
corporaOf(suites);
|
|
112
|
+
const cases = [];
|
|
113
|
+
const seen = new Set();
|
|
114
|
+
for (const suite of suites) {
|
|
115
|
+
for (const c of suite.cases) {
|
|
116
|
+
if (seen.has(c.id))
|
|
117
|
+
throw new EvalSuiteError(`Duplicate case id across suites (case ${c.id}, field id)`, c.id, 'id');
|
|
118
|
+
seen.add(c.id);
|
|
119
|
+
cases.push(c);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { version: 1, cases };
|
|
123
|
+
}
|
|
124
|
+
function ownerMatches(ownerPath, expectedOwner) {
|
|
125
|
+
return ownerPath === expectedOwner || ownerPath.startsWith(expectedOwner + '.');
|
|
126
|
+
}
|
|
127
|
+
async function validateCase(c, inspect) {
|
|
128
|
+
const valid = [];
|
|
129
|
+
const invalid = [];
|
|
130
|
+
for (const expected of c.expect) {
|
|
131
|
+
const indexed = await inspect.isFileIndexed(c.project, expected.file);
|
|
132
|
+
if (!indexed) {
|
|
133
|
+
invalid.push(expected);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (expected.owner) {
|
|
137
|
+
const owners = await inspect.ownerPathsOfFile(c.project, expected.file);
|
|
138
|
+
const found = owners.some(o => ownerMatches(o, expected.owner));
|
|
139
|
+
if (!found) {
|
|
140
|
+
invalid.push(expected);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
valid.push(expected);
|
|
145
|
+
}
|
|
146
|
+
return { valid, invalid };
|
|
147
|
+
}
|
|
148
|
+
function rankOf(hits, valid) {
|
|
149
|
+
for (let i = 0; i < hits.length; i++) {
|
|
150
|
+
const hit = hits[i];
|
|
151
|
+
const matches = valid.some(e => hit.filePath === e.file && (!e.owner || (hit.ownerPath !== null && ownerMatches(hit.ownerPath, e.owner))));
|
|
152
|
+
if (matches)
|
|
153
|
+
return { rank: i + 1, matched: hit };
|
|
154
|
+
}
|
|
155
|
+
return { rank: null, matched: null };
|
|
156
|
+
}
|
|
157
|
+
function emptyMetrics() {
|
|
158
|
+
return { count: 0, hit1: 0, hit5: 0, mrr10: 0 };
|
|
159
|
+
}
|
|
160
|
+
function accumulate(ranks) {
|
|
161
|
+
const n = ranks.length;
|
|
162
|
+
if (n === 0)
|
|
163
|
+
return emptyMetrics();
|
|
164
|
+
let hit1 = 0, hit5 = 0, mrrSum = 0;
|
|
165
|
+
for (const rank of ranks) {
|
|
166
|
+
if (rank === 1)
|
|
167
|
+
hit1++;
|
|
168
|
+
if (rank !== null && rank <= 5)
|
|
169
|
+
hit5++;
|
|
170
|
+
mrrSum += rank !== null ? 1 / rank : 0;
|
|
171
|
+
}
|
|
172
|
+
return { count: n, hit1: hit1 / n, hit5: hit5 / n, mrr10: mrrSum / n };
|
|
173
|
+
}
|
|
174
|
+
function stageRank(c, stage) {
|
|
175
|
+
return c.stages[stage]?.rank ?? null;
|
|
176
|
+
}
|
|
177
|
+
function stageMetrics(cases, stage) {
|
|
178
|
+
const byCategory = {};
|
|
179
|
+
for (const category of CATEGORIES) {
|
|
180
|
+
const subset = cases.filter(c => c.category === category);
|
|
181
|
+
if (subset.length > 0)
|
|
182
|
+
byCategory[category] = accumulate(subset.map(c => stageRank(c, stage)));
|
|
183
|
+
}
|
|
184
|
+
return { overall: accumulate(cases.map(c => stageRank(c, stage))), byCategory };
|
|
185
|
+
}
|
|
186
|
+
function poolMetricsOf(cases) {
|
|
187
|
+
const count = cases.length;
|
|
188
|
+
return {
|
|
189
|
+
count,
|
|
190
|
+
recall: count === 0 ? 0 : cases.filter(c => c.poolRank !== null).length / count,
|
|
191
|
+
short: cases.filter(c => c.short).length,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function binomialCdfHalf(k, n) {
|
|
195
|
+
let pmf = Math.pow(0.5, n);
|
|
196
|
+
let cdf = pmf;
|
|
197
|
+
for (let i = 1; i <= k; i++) {
|
|
198
|
+
pmf = pmf * (n - i + 1) / i;
|
|
199
|
+
cdf += pmf;
|
|
200
|
+
}
|
|
201
|
+
return cdf;
|
|
202
|
+
}
|
|
203
|
+
function signTest(improved, worsened) {
|
|
204
|
+
const n = improved + worsened;
|
|
205
|
+
if (n === 0)
|
|
206
|
+
return 1;
|
|
207
|
+
return Math.min(1, 2 * binomialCdfHalf(Math.min(improved, worsened), n));
|
|
208
|
+
}
|
|
209
|
+
function pairCounts(pairs) {
|
|
210
|
+
let improved = 0, worsened = 0, unchanged = 0;
|
|
211
|
+
for (const p of pairs) {
|
|
212
|
+
if (p.to < p.from)
|
|
213
|
+
improved++;
|
|
214
|
+
else if (p.to > p.from)
|
|
215
|
+
worsened++;
|
|
216
|
+
else
|
|
217
|
+
unchanged++;
|
|
218
|
+
}
|
|
219
|
+
return { improved, worsened, unchanged, signTestP: signTest(improved, worsened) };
|
|
220
|
+
}
|
|
221
|
+
function pairedChange(from, to) {
|
|
222
|
+
const fromById = new Map(from.map(c => [c.id, c.rank]));
|
|
223
|
+
const pairs = [];
|
|
224
|
+
for (const c of to) {
|
|
225
|
+
if (!fromById.has(c.id))
|
|
226
|
+
continue;
|
|
227
|
+
pairs.push({ id: c.id, category: c.category, from: fromById.get(c.id) ?? UNRANKED, to: c.rank ?? UNRANKED });
|
|
228
|
+
}
|
|
229
|
+
const byCategory = {};
|
|
230
|
+
for (const category of CATEGORIES) {
|
|
231
|
+
const subset = pairs.filter(p => p.category === category);
|
|
232
|
+
if (subset.length > 0)
|
|
233
|
+
byCategory[category] = pairCounts(subset);
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
overall: pairCounts(pairs),
|
|
237
|
+
byCategory,
|
|
238
|
+
hit5Gained: pairs.filter(p => p.from > 5 && p.to <= 5).map(p => p.id),
|
|
239
|
+
hit5Lost: pairs.filter(p => p.from <= 5 && p.to > 5).map(p => p.id),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function rankedCases(cases, stage) {
|
|
243
|
+
return cases.map(c => ({ id: c.id, category: c.category, rank: stageRank(c, stage) }));
|
|
244
|
+
}
|
|
245
|
+
function stageChangesOf(cases, stages) {
|
|
246
|
+
const changes = [];
|
|
247
|
+
for (const to of stages) {
|
|
248
|
+
const from = STAGE_SOURCE[to];
|
|
249
|
+
if (from && stages.includes(from)) {
|
|
250
|
+
changes.push({ from, to, change: pairedChange(rankedCases(cases, from), rankedCases(cases, to)) });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return changes;
|
|
254
|
+
}
|
|
255
|
+
function reportedStages(stages) {
|
|
256
|
+
return exports.STAGE_ORDER.filter(s => stages[s] !== undefined);
|
|
257
|
+
}
|
|
258
|
+
function sortedDistinct(values) {
|
|
259
|
+
return [...new Set(values)].sort((a, b) => {
|
|
260
|
+
if (a === b)
|
|
261
|
+
return 0;
|
|
262
|
+
if (a === null)
|
|
263
|
+
return -1;
|
|
264
|
+
if (b === null)
|
|
265
|
+
return 1;
|
|
266
|
+
return a < b ? -1 : 1;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
exports.LIVE_GROUP = 'live';
|
|
270
|
+
function runFigures(cases, stages, shortPool) {
|
|
271
|
+
const metrics = {};
|
|
272
|
+
for (const stage of stages)
|
|
273
|
+
metrics[stage] = stageMetrics(cases, stage);
|
|
274
|
+
const poolInput = (subset) => subset.map(c => ({ poolRank: c.poolRank, short: shortPool.get(c.id) }));
|
|
275
|
+
const poolByCategory = {};
|
|
276
|
+
for (const category of CATEGORIES) {
|
|
277
|
+
const subset = cases.filter(c => c.category === category);
|
|
278
|
+
if (subset.length > 0)
|
|
279
|
+
poolByCategory[category] = poolMetricsOf(poolInput(subset));
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
cases: cases.length,
|
|
283
|
+
metrics,
|
|
284
|
+
pool: { overall: poolMetricsOf(poolInput(cases)), byCategory: poolByCategory },
|
|
285
|
+
stageChanges: stageChangesOf(cases, stages),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
class RetrievalEvaluator {
|
|
289
|
+
async evaluate(suite, search, inspect, onCase) {
|
|
290
|
+
await checkCaseCorpora(suite.cases, p => inspect.projectDir(p));
|
|
291
|
+
const cases = [];
|
|
292
|
+
const shortPool = new Map();
|
|
293
|
+
const invalid = [];
|
|
294
|
+
const warnings = [];
|
|
295
|
+
const fetchLimits = [];
|
|
296
|
+
const efSearches = [];
|
|
297
|
+
const iterativeScans = [];
|
|
298
|
+
let stages = null;
|
|
299
|
+
const total = suite.cases.length;
|
|
300
|
+
for (const [i, c] of suite.cases.entries()) {
|
|
301
|
+
const started = Date.now();
|
|
302
|
+
const report = (result, inv) => onCase?.({ index: i + 1, total, id: c.id, result, invalid: inv, ms: Date.now() - started });
|
|
303
|
+
const { valid, invalid: invalidExpects } = await validateCase(c, inspect);
|
|
304
|
+
if (valid.length === 0) {
|
|
305
|
+
const inv = { id: c.id, reason: 'no valid expected answer', invalidExpects };
|
|
306
|
+
invalid.push(inv);
|
|
307
|
+
report(null, inv);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (invalidExpects.length > 0) {
|
|
311
|
+
warnings.push({ id: c.id, droppedExpects: invalidExpects });
|
|
312
|
+
}
|
|
313
|
+
const hits = await search(c.query, 10, c.project, c.threshold);
|
|
314
|
+
const caseStages = reportedStages(hits.stages);
|
|
315
|
+
if (stages === null) {
|
|
316
|
+
stages = caseStages;
|
|
317
|
+
}
|
|
318
|
+
else if (caseStages.join(',') !== stages.join(',')) {
|
|
319
|
+
throw new Error(`Case ${c.id} reported stages [${caseStages.join(', ')}], but the run's stages are [${stages.join(', ')}]`);
|
|
320
|
+
}
|
|
321
|
+
const ranks = {};
|
|
322
|
+
for (const stage of caseStages)
|
|
323
|
+
ranks[stage] = rankOf(hits.stages[stage], valid);
|
|
324
|
+
const result = {
|
|
325
|
+
id: c.id, project: c.project, category: c.category, query: c.query,
|
|
326
|
+
stages: ranks,
|
|
327
|
+
poolRank: rankOf(hits.pool, valid).rank,
|
|
328
|
+
poolSize: hits.pool.length,
|
|
329
|
+
rerankFailed: hits.rerankFailed,
|
|
330
|
+
corpus: c.corpus ?? null,
|
|
331
|
+
};
|
|
332
|
+
cases.push(result);
|
|
333
|
+
shortPool.set(c.id, hits.pool.length < hits.fetchLimit);
|
|
334
|
+
fetchLimits.push(hits.fetchLimit);
|
|
335
|
+
efSearches.push(hits.indexParams.efSearch);
|
|
336
|
+
iterativeScans.push(hits.indexParams.iterativeScan);
|
|
337
|
+
report(result, null);
|
|
338
|
+
}
|
|
339
|
+
const runStages = stages ?? ['vector'];
|
|
340
|
+
const figures = (subset) => runFigures(subset, runStages, shortPool);
|
|
341
|
+
const byCorpus = {};
|
|
342
|
+
for (const key of [...new Set(cases.map(c => c.corpus ?? exports.LIVE_GROUP))].sort()) {
|
|
343
|
+
byCorpus[key] = figures(cases.filter(c => (c.corpus ?? exports.LIVE_GROUP) === key));
|
|
344
|
+
}
|
|
345
|
+
const all = figures(cases);
|
|
346
|
+
const observed = {
|
|
347
|
+
fetchLimit: sortedDistinct(fetchLimits),
|
|
348
|
+
efSearch: sortedDistinct(efSearches),
|
|
349
|
+
iterativeScan: sortedDistinct(iterativeScans),
|
|
350
|
+
};
|
|
351
|
+
return {
|
|
352
|
+
run: {
|
|
353
|
+
cases, invalid, warnings,
|
|
354
|
+
stages: runStages,
|
|
355
|
+
metrics: all.metrics,
|
|
356
|
+
pool: all.pool,
|
|
357
|
+
stageChanges: all.stageChanges,
|
|
358
|
+
rerankFailures: cases.filter(c => c.rerankFailed).length,
|
|
359
|
+
byCorpus,
|
|
360
|
+
},
|
|
361
|
+
observed,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
exports.RetrievalEvaluator = RetrievalEvaluator;
|
|
366
|
+
function metricDelta(baseline, current) {
|
|
367
|
+
return { baseline, current, delta: current - baseline };
|
|
368
|
+
}
|
|
369
|
+
function metricsDelta(baseline, current) {
|
|
370
|
+
return {
|
|
371
|
+
count: metricDelta(baseline.count, current.count),
|
|
372
|
+
hit1: metricDelta(baseline.hit1, current.hit1),
|
|
373
|
+
hit5: metricDelta(baseline.hit5, current.hit5),
|
|
374
|
+
mrr10: metricDelta(baseline.mrr10, current.mrr10),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function metricsDeltaByCategory(baseline, current) {
|
|
378
|
+
const out = {};
|
|
379
|
+
for (const category of CATEGORIES) {
|
|
380
|
+
const b = baseline[category];
|
|
381
|
+
const cur = current[category];
|
|
382
|
+
if (b && cur)
|
|
383
|
+
out[category] = metricsDelta(b, cur);
|
|
384
|
+
}
|
|
385
|
+
return out;
|
|
386
|
+
}
|
|
387
|
+
function poolValueDelta(baseline, current) {
|
|
388
|
+
return { baseline, current, delta: baseline === null ? null : current - baseline };
|
|
389
|
+
}
|
|
390
|
+
function poolDelta(baseline, current) {
|
|
391
|
+
return {
|
|
392
|
+
recall: poolValueDelta(baseline?.recall ?? null, current.recall),
|
|
393
|
+
short: poolValueDelta(baseline?.short ?? null, current.short),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function formatCondition(value) {
|
|
397
|
+
return Array.isArray(value) ? `[${value.map(v => (v === null ? 'not sent' : String(v))).join(', ')}]` : String(value);
|
|
398
|
+
}
|
|
399
|
+
function corpusWarningsOf(b, cur) {
|
|
400
|
+
const warnings = [];
|
|
401
|
+
if (b.corpora.length === 0)
|
|
402
|
+
warnings.push('baseline records no corpus');
|
|
403
|
+
const baseByRoot = new Map(b.corpora.map(c => [c.root, c]));
|
|
404
|
+
const curByRoot = new Map(cur.corpora.map(c => [c.root, c]));
|
|
405
|
+
for (const root of [...new Set([...baseByRoot.keys(), ...curByRoot.keys()])].sort()) {
|
|
406
|
+
const bc = baseByRoot.get(root), cc = curByRoot.get(root);
|
|
407
|
+
if (!bc) {
|
|
408
|
+
warnings.push(`corpus ${root} only in the current run`);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (!cc) {
|
|
412
|
+
warnings.push(`corpus ${root} only in the baseline`);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (bc.commit !== cc.commit) {
|
|
416
|
+
warnings.push(`corpus ${root} commit differs: baseline ${bc.commit} vs current ${cc.commit}`);
|
|
417
|
+
}
|
|
418
|
+
else if (bc.fingerprint !== cc.fingerprint) {
|
|
419
|
+
warnings.push(`corpus ${root} was searched on a different index build: fingerprint ${bc.fingerprint} (${bc.chunks} chunks) vs ${cc.fingerprint} (${cc.chunks} chunks)`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const live = (side, n) => `${side} includes ${n} live-corpus case(s); differences may come from code changes`;
|
|
423
|
+
if (b.liveCases > 0)
|
|
424
|
+
warnings.push(live('baseline', b.liveCases));
|
|
425
|
+
if (cur.liveCases > 0)
|
|
426
|
+
warnings.push(live('current run', cur.liveCases));
|
|
427
|
+
return warnings;
|
|
428
|
+
}
|
|
429
|
+
function conditionWarningsOf(baseline, current) {
|
|
430
|
+
const warnings = [];
|
|
431
|
+
const b = baseline.metadata, cur = current.metadata;
|
|
432
|
+
if (b.suiteHash !== cur.suiteHash) {
|
|
433
|
+
warnings.push(`suite differs: baseline ${b.suiteHash} vs current ${cur.suiteHash}`);
|
|
434
|
+
}
|
|
435
|
+
if (b.embeddingModel !== cur.embeddingModel) {
|
|
436
|
+
warnings.push(`embedding model differs: baseline ${b.embeddingModel} vs current ${cur.embeddingModel}`);
|
|
437
|
+
}
|
|
438
|
+
if (b.rerankerModel !== cur.rerankerModel) {
|
|
439
|
+
warnings.push(`reranker differs: baseline ${b.rerankerModel ?? 'none'} vs current ${cur.rerankerModel ?? 'none'}`);
|
|
440
|
+
}
|
|
441
|
+
if (!b.conditions)
|
|
442
|
+
warnings.push('baseline records no search conditions');
|
|
443
|
+
if (!cur.conditions)
|
|
444
|
+
warnings.push('current run records no search conditions');
|
|
445
|
+
if (b.conditions && cur.conditions) {
|
|
446
|
+
const bc = b.conditions;
|
|
447
|
+
const cc = cur.conditions;
|
|
448
|
+
for (const key of [...new Set([...Object.keys(bc), ...Object.keys(cc)])]) {
|
|
449
|
+
if (!(key in bc))
|
|
450
|
+
warnings.push(`search condition ${key} recorded only by the current run (${formatCondition(cc[key])})`);
|
|
451
|
+
else if (!(key in cc))
|
|
452
|
+
warnings.push(`search condition ${key} recorded only by the baseline (${formatCondition(bc[key])})`);
|
|
453
|
+
else if (JSON.stringify(bc[key]) !== JSON.stringify(cc[key])) {
|
|
454
|
+
warnings.push(`${key} differs: baseline ${formatCondition(bc[key])} vs current ${formatCondition(cc[key])}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
warnings.push(...corpusWarningsOf(b, cur));
|
|
459
|
+
if (baseline.run.rerankFailures > 0)
|
|
460
|
+
warnings.push(`baseline has ${baseline.run.rerankFailures} rerank failure(s)`);
|
|
461
|
+
if (current.run.rerankFailures > 0)
|
|
462
|
+
warnings.push(`current run has ${current.run.rerankFailures} rerank failure(s)`);
|
|
463
|
+
return warnings;
|
|
464
|
+
}
|
|
465
|
+
function compare(baseline, current) {
|
|
466
|
+
const stages = [];
|
|
467
|
+
for (const stage of current.run.stages) {
|
|
468
|
+
const b = baseline.run.metrics[stage];
|
|
469
|
+
const cur = current.run.metrics[stage];
|
|
470
|
+
if (!baseline.run.stages.includes(stage) || !b || !cur)
|
|
471
|
+
continue;
|
|
472
|
+
stages.push({
|
|
473
|
+
stage,
|
|
474
|
+
overall: metricsDelta(b.overall, cur.overall),
|
|
475
|
+
byCategory: metricsDeltaByCategory(b.byCategory, cur.byCategory),
|
|
476
|
+
change: pairedChange(rankedCases(baseline.run.cases, stage), rankedCases(current.run.cases, stage)),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
const unmatchedStages = [
|
|
480
|
+
...baseline.run.stages.filter(s => !current.run.stages.includes(s)).map(stage => ({ stage, side: 'baseline' })),
|
|
481
|
+
...current.run.stages.filter(s => !baseline.run.stages.includes(s)).map(stage => ({ stage, side: 'current' })),
|
|
482
|
+
];
|
|
483
|
+
const basePool = baseline.legacy ? null : baseline.run.pool;
|
|
484
|
+
const poolByCategory = {};
|
|
485
|
+
for (const category of CATEGORIES) {
|
|
486
|
+
const cur = current.run.pool.byCategory[category];
|
|
487
|
+
if (!cur)
|
|
488
|
+
continue;
|
|
489
|
+
if (basePool === null)
|
|
490
|
+
poolByCategory[category] = poolDelta(null, cur);
|
|
491
|
+
else if (basePool.byCategory[category])
|
|
492
|
+
poolByCategory[category] = poolDelta(basePool.byCategory[category], cur);
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
stages,
|
|
496
|
+
unmatchedStages,
|
|
497
|
+
pool: { overall: poolDelta(basePool?.overall ?? null, current.run.pool.overall), byCategory: poolByCategory },
|
|
498
|
+
conditionWarnings: conditionWarningsOf(baseline, current),
|
|
499
|
+
};
|
|
500
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_PER_FILE = void 0;
|
|
4
|
+
exports.limitPerFile = limitPerFile;
|
|
5
|
+
exports.DEFAULT_PER_FILE = 2;
|
|
6
|
+
function limitPerFile(ranked, limit, perFile) {
|
|
7
|
+
const taken = [];
|
|
8
|
+
const perFileCount = new Map();
|
|
9
|
+
for (const entry of ranked) {
|
|
10
|
+
if (taken.length >= limit)
|
|
11
|
+
break;
|
|
12
|
+
const count = perFileCount.get(entry.filePath) ?? 0;
|
|
13
|
+
if (count >= perFile)
|
|
14
|
+
continue;
|
|
15
|
+
perFileCount.set(entry.filePath, count + 1);
|
|
16
|
+
taken.push(entry);
|
|
17
|
+
}
|
|
18
|
+
return taken;
|
|
19
|
+
}
|