@jcode.labs/ragmir 2.17.2 → 2.18.0
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/README.md +53 -6
- package/dist/access-log.d.ts +10 -0
- package/dist/access-log.d.ts.map +1 -1
- package/dist/access-log.js +119 -49
- package/dist/access-log.js.map +1 -1
- package/dist/chunking.d.ts +5 -1
- package/dist/chunking.d.ts.map +1 -1
- package/dist/chunking.js +102 -16
- package/dist/chunking.js.map +1 -1
- package/dist/citation.d.ts +18 -0
- package/dist/citation.d.ts.map +1 -0
- package/dist/citation.js +54 -0
- package/dist/citation.js.map +1 -0
- package/dist/cli-entry.d.ts +3 -0
- package/dist/cli-entry.d.ts.map +1 -0
- package/dist/cli-entry.js +16 -0
- package/dist/cli-entry.js.map +1 -0
- package/dist/cli-fast.d.ts +13 -0
- package/dist/cli-fast.d.ts.map +1 -0
- package/dist/cli-fast.js +145 -0
- package/dist/cli-fast.js.map +1 -0
- package/dist/cli-options.d.ts +3 -0
- package/dist/cli-options.d.ts.map +1 -1
- package/dist/cli-options.js +7 -0
- package/dist/cli-options.js.map +1 -1
- package/dist/cli-stdin.d.ts +3 -0
- package/dist/cli-stdin.d.ts.map +1 -0
- package/dist/cli-stdin.js +15 -0
- package/dist/cli-stdin.js.map +1 -0
- package/dist/cli.js +251 -44
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +13 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +155 -10
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +227 -49
- package/dist/config.js.map +1 -1
- package/dist/context-resources.d.ts +4 -2
- package/dist/context-resources.d.ts.map +1 -1
- package/dist/context-resources.js +55 -24
- package/dist/context-resources.js.map +1 -1
- package/dist/defaults.d.ts +27 -0
- package/dist/defaults.d.ts.map +1 -1
- package/dist/defaults.js +42 -2
- package/dist/defaults.js.map +1 -1
- package/dist/destroy.d.ts +2 -2
- package/dist/destroy.d.ts.map +1 -1
- package/dist/destroy.js +34 -20
- package/dist/destroy.js.map +1 -1
- package/dist/doctor.d.ts +3 -2
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +86 -41
- package/dist/doctor.js.map +1 -1
- package/dist/durable-file.d.ts +13 -0
- package/dist/durable-file.d.ts.map +1 -0
- package/dist/durable-file.js +56 -0
- package/dist/durable-file.js.map +1 -0
- package/dist/embeddings.d.ts +14 -3
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +233 -95
- package/dist/embeddings.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/evaluate.d.ts +4 -3
- package/dist/evaluate.d.ts.map +1 -1
- package/dist/evaluate.js +611 -110
- package/dist/evaluate.js.map +1 -1
- package/dist/files.d.ts +4 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +119 -33
- package/dist/files.js.map +1 -1
- package/dist/generation-retention.d.ts +52 -0
- package/dist/generation-retention.d.ts.map +1 -0
- package/dist/generation-retention.js +318 -0
- package/dist/generation-retention.js.map +1 -0
- package/dist/index-diagnostics.d.ts +3 -2
- package/dist/index-diagnostics.d.ts.map +1 -1
- package/dist/index-diagnostics.js +32 -3
- package/dist/index-diagnostics.js.map +1 -1
- package/dist/index-policy.d.ts.map +1 -1
- package/dist/index-policy.js +3 -0
- package/dist/index-policy.js.map +1 -1
- package/dist/index-write-lock.d.ts +16 -1
- package/dist/index-write-lock.d.ts.map +1 -1
- package/dist/index-write-lock.js +392 -13
- package/dist/index-write-lock.js.map +1 -1
- package/dist/index.d.ts +12 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/ingest.d.ts +11 -2
- package/dist/ingest.d.ts.map +1 -1
- package/dist/ingest.js +680 -165
- package/dist/ingest.js.map +1 -1
- package/dist/ingestion-metrics.d.ts +58 -0
- package/dist/ingestion-metrics.d.ts.map +1 -0
- package/dist/ingestion-metrics.js +216 -0
- package/dist/ingestion-metrics.js.map +1 -0
- package/dist/ingestion-state.d.ts +11 -2
- package/dist/ingestion-state.d.ts.map +1 -1
- package/dist/ingestion-state.js +480 -49
- package/dist/ingestion-state.js.map +1 -1
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +11 -1
- package/dist/limits.js.map +1 -1
- package/dist/mcp-output.d.ts +44 -4
- package/dist/mcp-output.d.ts.map +1 -1
- package/dist/mcp-output.js +132 -159
- package/dist/mcp-output.js.map +1 -1
- package/dist/mcp-summaries.d.ts +32 -0
- package/dist/mcp-summaries.d.ts.map +1 -0
- package/dist/mcp-summaries.js +227 -0
- package/dist/mcp-summaries.js.map +1 -0
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +85 -46
- package/dist/mcp.js.map +1 -1
- package/dist/ocr-cache.d.ts +15 -0
- package/dist/ocr-cache.d.ts.map +1 -0
- package/dist/ocr-cache.js +90 -0
- package/dist/ocr-cache.js.map +1 -0
- package/dist/ocr.d.ts +31 -0
- package/dist/ocr.d.ts.map +1 -1
- package/dist/ocr.js +193 -17
- package/dist/ocr.js.map +1 -1
- package/dist/parsing.d.ts.map +1 -1
- package/dist/parsing.js +482 -85
- package/dist/parsing.js.map +1 -1
- package/dist/permissions.js +2 -2
- package/dist/permissions.js.map +1 -1
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +10 -11
- package/dist/preview.js.map +1 -1
- package/dist/quality-report.d.ts +12 -0
- package/dist/quality-report.d.ts.map +1 -0
- package/dist/quality-report.js +146 -0
- package/dist/quality-report.js.map +1 -0
- package/dist/query.d.ts +7 -4
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +371 -284
- package/dist/query.js.map +1 -1
- package/dist/ranking.d.ts +38 -0
- package/dist/ranking.d.ts.map +1 -0
- package/dist/ranking.js +278 -0
- package/dist/ranking.js.map +1 -0
- package/dist/redaction.d.ts +6 -1
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +118 -4
- package/dist/redaction.js.map +1 -1
- package/dist/research.d.ts +6 -1
- package/dist/research.d.ts.map +1 -1
- package/dist/research.js +330 -130
- package/dist/research.js.map +1 -1
- package/dist/runtime-info.d.ts +3 -0
- package/dist/runtime-info.d.ts.map +1 -0
- package/dist/runtime-info.js +81 -0
- package/dist/runtime-info.js.map +1 -0
- package/dist/security.d.ts +3 -2
- package/dist/security.d.ts.map +1 -1
- package/dist/security.js +242 -45
- package/dist/security.js.map +1 -1
- package/dist/semantic-config.d.ts +7 -1
- package/dist/semantic-config.d.ts.map +1 -1
- package/dist/semantic-config.js +30 -4
- package/dist/semantic-config.js.map +1 -1
- package/dist/setup.js +6 -3
- package/dist/setup.js.map +1 -1
- package/dist/skill.js +34 -12
- package/dist/skill.js.map +1 -1
- package/dist/source-fingerprint-cache.d.ts +20 -0
- package/dist/source-fingerprint-cache.d.ts.map +1 -0
- package/dist/source-fingerprint-cache.js +133 -0
- package/dist/source-fingerprint-cache.js.map +1 -0
- package/dist/storage-maintenance.d.ts +46 -0
- package/dist/storage-maintenance.d.ts.map +1 -0
- package/dist/storage-maintenance.js +329 -0
- package/dist/storage-maintenance.js.map +1 -0
- package/dist/store.d.ts +39 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +559 -24
- package/dist/store.js.map +1 -1
- package/dist/text.d.ts +1 -1
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +12 -2
- package/dist/text.js.map +1 -1
- package/dist/types.d.ts +414 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-index.d.ts +42 -0
- package/dist/vector-index.d.ts.map +1 -0
- package/dist/vector-index.js +249 -0
- package/dist/vector-index.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workload.d.ts +22 -0
- package/dist/workload.d.ts.map +1 -0
- package/dist/workload.js +181 -0
- package/dist/workload.js.map +1 -0
- package/examples/document-evidence-benchmark/README.md +3 -2
- package/examples/document-evidence-benchmark/golden-queries.json +1 -1
- package/package.json +36 -11
package/dist/evaluate.js
CHANGED
|
@@ -1,145 +1,332 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { readFile, stat } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { z } from "zod";
|
|
4
|
-
import { recordAccess } from "./access-log.js";
|
|
5
|
+
import { flushAccessLog, recordAccess } from "./access-log.js";
|
|
5
6
|
import { loadConfig } from "./config.js";
|
|
7
|
+
import { retainEmbeddingModel } from "./embeddings.js";
|
|
6
8
|
import { RagmirError } from "./errors.js";
|
|
9
|
+
import { acquireGenerationReadLease } from "./generation-retention.js";
|
|
10
|
+
import { withIndexWriteLock } from "./index-write-lock.js";
|
|
7
11
|
import { operationSignal, throwIfAborted } from "./operation.js";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
import { fingerprintIndexManifest, fingerprintQualityReport } from "./quality-report.js";
|
|
13
|
+
import { searchWithConfig } from "./query.js";
|
|
14
|
+
import { rankingPolicyFingerprint, rankingPolicyFor } from "./ranking.js";
|
|
15
|
+
import { closeIndexReadSnapshot, closeStoreConnection, connectStore, indexReadSnapshotCurrent, loadIndexReadSnapshot, readIndexManifest, writeIndexManifest, } from "./store.js";
|
|
16
|
+
export const MAX_GOLDEN_FILE_BYTES = 16_777_216;
|
|
17
|
+
export const MAX_GOLDEN_CASES = 1_000;
|
|
11
18
|
export const MAX_GOLDEN_QUERY_CHARACTERS = 20_000;
|
|
12
19
|
export const MAX_GOLDEN_EXPECTED_VALUES = 100;
|
|
13
20
|
export const MAX_GOLDEN_EXPECTED_VALUE_CHARACTERS = 500;
|
|
21
|
+
const EVALUATION_DEPTH = 10;
|
|
22
|
+
const DEFAULT_MINIMUM_VERIFICATION_CASES = 100;
|
|
14
23
|
const expectedValueSchema = z.string().min(1).max(MAX_GOLDEN_EXPECTED_VALUE_CHARACTERS);
|
|
24
|
+
const unitMetricSchema = z.number().min(0).max(1);
|
|
25
|
+
const relevanceJudgmentSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
kind: z.enum(["path", "citation"]),
|
|
28
|
+
value: expectedValueSchema,
|
|
29
|
+
relevance: z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)]),
|
|
30
|
+
})
|
|
31
|
+
.strict();
|
|
32
|
+
const qualityThresholdSchema = z
|
|
33
|
+
.object({
|
|
34
|
+
recallAt1: unitMetricSchema.optional(),
|
|
35
|
+
recallAt3: unitMetricSchema.optional(),
|
|
36
|
+
recallAt5: unitMetricSchema.optional(),
|
|
37
|
+
recallAt10: unitMetricSchema.optional(),
|
|
38
|
+
precisionAt5: unitMetricSchema.optional(),
|
|
39
|
+
meanReciprocalRankAt10: unitMetricSchema.optional(),
|
|
40
|
+
ndcgAt10: unitMetricSchema.optional(),
|
|
41
|
+
exactCitationRate: unitMetricSchema.optional(),
|
|
42
|
+
maximumFalsePositiveRate: unitMetricSchema.optional(),
|
|
43
|
+
})
|
|
44
|
+
.strict();
|
|
15
45
|
const goldenQuerySchema = z
|
|
16
46
|
.object({
|
|
17
47
|
id: z.string().min(1).optional(),
|
|
18
48
|
query: z.string().min(1).max(MAX_GOLDEN_QUERY_CHARACTERS),
|
|
19
|
-
expectedPaths: z.array(expectedValueSchema).
|
|
49
|
+
expectedPaths: z.array(expectedValueSchema).max(MAX_GOLDEN_EXPECTED_VALUES).default([]),
|
|
20
50
|
expectedCitations: z
|
|
21
51
|
.array(expectedValueSchema)
|
|
22
52
|
.min(1)
|
|
23
53
|
.max(MAX_GOLDEN_EXPECTED_VALUES)
|
|
24
54
|
.optional(),
|
|
55
|
+
answerable: z.boolean().optional(),
|
|
56
|
+
category: z.string().min(1).max(100).optional(),
|
|
57
|
+
locale: z.string().min(2).max(50).optional(),
|
|
58
|
+
relevanceJudgments: z.array(relevanceJudgmentSchema).max(MAX_GOLDEN_EXPECTED_VALUES).optional(),
|
|
59
|
+
maximumVectorDistance: z.number().nonnegative().optional(),
|
|
25
60
|
includePaths: z.array(z.string().min(1).max(500)).max(20).optional(),
|
|
26
61
|
excludePaths: z.array(z.string().min(1).max(500)).max(20).optional(),
|
|
27
62
|
contextPaths: z.array(z.string().min(1).max(500)).max(20).optional(),
|
|
28
63
|
topK: z.number().int().positive().optional(),
|
|
64
|
+
})
|
|
65
|
+
.strict()
|
|
66
|
+
.superRefine((value, context) => {
|
|
67
|
+
const answerable = value.answerable ?? true;
|
|
68
|
+
const positiveJudgments = (value.relevanceJudgments ?? []).filter((judgment) => judgment.relevance > 0);
|
|
69
|
+
if (answerable &&
|
|
70
|
+
value.expectedPaths.length === 0 &&
|
|
71
|
+
value.expectedCitations === undefined &&
|
|
72
|
+
positiveJudgments.length === 0) {
|
|
73
|
+
context.addIssue({
|
|
74
|
+
code: "custom",
|
|
75
|
+
message: "Answerable golden queries require at least one relevant path or citation.",
|
|
76
|
+
path: ["expectedPaths"],
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (!answerable && (value.expectedPaths.length > 0 || positiveJudgments.length > 0)) {
|
|
80
|
+
context.addIssue({
|
|
81
|
+
code: "custom",
|
|
82
|
+
message: "Unanswerable golden queries cannot declare relevant evidence.",
|
|
83
|
+
path: ["answerable"],
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
const wrappedGoldenFileSchema = z
|
|
88
|
+
.object({
|
|
89
|
+
topK: z.number().int().positive().optional(),
|
|
90
|
+
minimumCasesForVerification: z.number().int().positive().max(MAX_GOLDEN_CASES).optional(),
|
|
91
|
+
thresholds: qualityThresholdSchema.optional(),
|
|
92
|
+
queries: z.array(goldenQuerySchema).min(1).max(MAX_GOLDEN_CASES),
|
|
29
93
|
})
|
|
30
94
|
.strict();
|
|
31
95
|
const goldenFileSchema = z.union([
|
|
32
96
|
z.array(goldenQuerySchema).min(1).max(MAX_GOLDEN_CASES),
|
|
33
|
-
|
|
34
|
-
.object({
|
|
35
|
-
topK: z.number().int().positive().optional(),
|
|
36
|
-
queries: z.array(goldenQuerySchema).min(1).max(MAX_GOLDEN_CASES),
|
|
37
|
-
})
|
|
38
|
-
.strict(),
|
|
97
|
+
wrappedGoldenFileSchema,
|
|
39
98
|
]);
|
|
99
|
+
async function createEvaluationClient(config, signal) {
|
|
100
|
+
const connection = await connectStore(config);
|
|
101
|
+
const releaseEmbeddingModel = retainEmbeddingModel(config);
|
|
102
|
+
let snapshot;
|
|
103
|
+
try {
|
|
104
|
+
snapshot = await loadEvaluationSnapshot(config, connection);
|
|
105
|
+
const generationLease = await acquireGenerationReadLease(snapshot.tableName, config);
|
|
106
|
+
return evaluationClient(config, connection, snapshot, generationLease, signal, () => releaseEmbeddingModel());
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (snapshot) {
|
|
110
|
+
closeIndexReadSnapshot(snapshot, config);
|
|
111
|
+
}
|
|
112
|
+
closeStoreConnection(connection, config);
|
|
113
|
+
await releaseEmbeddingModel();
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function evaluationClient(config, connection, snapshot, generationLease, signal, releaseEmbeddingModel) {
|
|
118
|
+
let closed = false;
|
|
119
|
+
return {
|
|
120
|
+
snapshot,
|
|
121
|
+
search(query, options) {
|
|
122
|
+
return searchWithConfig(query, options, config, connection, signal, snapshot, true);
|
|
123
|
+
},
|
|
124
|
+
async close() {
|
|
125
|
+
if (closed) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
closed = true;
|
|
129
|
+
try {
|
|
130
|
+
closeIndexReadSnapshot(snapshot, config);
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
try {
|
|
134
|
+
await generationLease.release();
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
try {
|
|
138
|
+
closeStoreConnection(connection, config);
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
await releaseEmbeddingModel();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async function loadEvaluationSnapshot(config, connection) {
|
|
149
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
150
|
+
const snapshot = await loadIndexReadSnapshot(config, connection);
|
|
151
|
+
const manifest = await readIndexManifest(config);
|
|
152
|
+
const current = await indexReadSnapshotCurrent(snapshot, config);
|
|
153
|
+
const matchingPresence = (snapshot.manifest === null) === (manifest === null);
|
|
154
|
+
const manifestTableName = manifest?.tableName ?? config.tableName;
|
|
155
|
+
if (current && matchingPresence && manifestTableName === snapshot.tableName) {
|
|
156
|
+
return { ...snapshot, manifest: manifest ? Object.freeze({ ...manifest }) : null };
|
|
157
|
+
}
|
|
158
|
+
closeIndexReadSnapshot(snapshot, config);
|
|
159
|
+
}
|
|
160
|
+
throw new Error("Index generation changed repeatedly while opening an evaluation snapshot. Retry evaluation.");
|
|
161
|
+
}
|
|
40
162
|
export async function evaluateGoldenQueries(options) {
|
|
41
163
|
const signal = operationSignal(options);
|
|
42
164
|
throwIfAborted(signal);
|
|
43
165
|
const cwd = path.resolve(String(options.cwd ?? process.cwd()));
|
|
44
166
|
const config = await loadConfig(cwd);
|
|
167
|
+
return evaluateGoldenQueriesWithConfig(options, config);
|
|
168
|
+
}
|
|
169
|
+
export async function evaluateGoldenQueriesWithConfig(options, config) {
|
|
170
|
+
const signal = operationSignal(options);
|
|
171
|
+
throwIfAborted(signal);
|
|
172
|
+
const cwd = path.resolve(String(options.cwd ?? config.projectRoot));
|
|
173
|
+
const activeRankingPolicyFingerprint = rankingPolicyFingerprint(rankingPolicyFor(config.embeddingProvider, config.retrievalProfile));
|
|
45
174
|
throwIfAborted(signal);
|
|
46
175
|
const goldenPath = path.resolve(cwd, String(options.goldenPath));
|
|
47
176
|
const goldenFile = await readGoldenFile(goldenPath, signal);
|
|
48
|
-
throwIfAborted(signal);
|
|
49
177
|
const defaultTopK = boundedTopK(options.topK ?? goldenFile.topK ?? 3, options.maxTopK);
|
|
178
|
+
const caseDetailLimit = evaluationCaseDetailLimit(options.caseDetailLimit);
|
|
50
179
|
const cases = [];
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
...(signal === undefined ? {} : { signal }),
|
|
59
|
-
...(goldenQuery.includePaths === undefined ? {} : { includePaths: goldenQuery.includePaths }),
|
|
60
|
-
...(goldenQuery.excludePaths === undefined ? {} : { excludePaths: goldenQuery.excludePaths }),
|
|
61
|
-
...(goldenQuery.contextPaths === undefined ? {} : { contextPaths: goldenQuery.contextPaths }),
|
|
62
|
-
});
|
|
180
|
+
const evaluationConcurrency = Math.max(1, Math.min(config.workloadLimits.search.concurrency, config.workloadLimits.embedding.concurrency + config.workloadLimits.embedding.maxQueue));
|
|
181
|
+
const evaluationClient = await createEvaluationClient(config, signal);
|
|
182
|
+
const initialManifest = evaluationClient.snapshot.manifest;
|
|
183
|
+
const indexFingerprint = initialManifest
|
|
184
|
+
? fingerprintIndexManifest(initialManifest)
|
|
185
|
+
: "missing-index";
|
|
186
|
+
try {
|
|
63
187
|
throwIfAborted(signal);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
returnedCitations,
|
|
89
|
-
matchedPaths,
|
|
90
|
-
matchedCitations,
|
|
91
|
-
hit: requiresExactCitation ? matchedCitations.length > 0 : matchedPaths.length > 0,
|
|
92
|
-
bestRank: bestRank > 0 ? bestRank : null,
|
|
93
|
-
reciprocalRank,
|
|
94
|
-
recall,
|
|
95
|
-
precision,
|
|
96
|
-
ndcg,
|
|
97
|
-
latencyMs,
|
|
98
|
-
};
|
|
99
|
-
if (goldenQuery.id !== undefined) {
|
|
100
|
-
result.id = goldenQuery.id;
|
|
188
|
+
for (let offset = 0; offset < goldenFile.queries.length; offset += evaluationConcurrency) {
|
|
189
|
+
throwIfAborted(signal);
|
|
190
|
+
const batch = goldenFile.queries.slice(offset, offset + evaluationConcurrency);
|
|
191
|
+
cases.push(...(await Promise.all(batch.map(async (goldenQuery) => {
|
|
192
|
+
const topK = boundedTopK(goldenQuery.topK ?? defaultTopK, options.maxTopK);
|
|
193
|
+
const evaluationDepth = boundedTopK(Math.max(topK, EVALUATION_DEPTH), options.maxTopK);
|
|
194
|
+
const startedAt = performance.now();
|
|
195
|
+
const results = await evaluationClient.search(goldenQuery.query, {
|
|
196
|
+
cwd,
|
|
197
|
+
topK: evaluationDepth,
|
|
198
|
+
...(signal === undefined ? {} : { signal }),
|
|
199
|
+
...(goldenQuery.includePaths === undefined
|
|
200
|
+
? {}
|
|
201
|
+
: { includePaths: goldenQuery.includePaths }),
|
|
202
|
+
...(goldenQuery.excludePaths === undefined
|
|
203
|
+
? {}
|
|
204
|
+
: { excludePaths: goldenQuery.excludePaths }),
|
|
205
|
+
...(goldenQuery.contextPaths === undefined
|
|
206
|
+
? {}
|
|
207
|
+
: { contextPaths: goldenQuery.contextPaths }),
|
|
208
|
+
});
|
|
209
|
+
throwIfAborted(signal);
|
|
210
|
+
return evaluateCase(goldenQuery, applyVectorDistancePolicy(results, goldenQuery.maximumVectorDistance), topK, performance.now() - startedAt);
|
|
211
|
+
}))));
|
|
101
212
|
}
|
|
102
|
-
if (goldenQuery.expectedCitations !== undefined) {
|
|
103
|
-
result.expectedCitations = goldenQuery.expectedCitations;
|
|
104
|
-
}
|
|
105
|
-
if (goldenQuery.includePaths !== undefined) {
|
|
106
|
-
result.includePaths = goldenQuery.includePaths;
|
|
107
|
-
}
|
|
108
|
-
if (goldenQuery.excludePaths !== undefined) {
|
|
109
|
-
result.excludePaths = goldenQuery.excludePaths;
|
|
110
|
-
}
|
|
111
|
-
if (goldenQuery.contextPaths !== undefined) {
|
|
112
|
-
result.contextPaths = goldenQuery.contextPaths;
|
|
113
|
-
}
|
|
114
|
-
cases.push(result);
|
|
115
213
|
}
|
|
214
|
+
finally {
|
|
215
|
+
await evaluationClient.close();
|
|
216
|
+
}
|
|
217
|
+
const answerableCases = cases.filter((result) => result.answerable);
|
|
218
|
+
const unanswerableCases = cases.filter((result) => !result.answerable);
|
|
219
|
+
const citationCases = answerableCases.filter((result) => result.exactCitationHit !== null);
|
|
220
|
+
const gradedCases = answerableCases.filter((result) => result.relevanceJudgments.some((judgment) => judgment.relevance > 1));
|
|
221
|
+
const metrics = evaluationMetrics(answerableCases, unanswerableCases, citationCases);
|
|
222
|
+
const thresholds = { ...goldenFile.thresholds, ...options.thresholds };
|
|
223
|
+
const gates = qualityGates(metrics, thresholds);
|
|
224
|
+
const passed = gates.every((gate) => gate.passed);
|
|
225
|
+
const requiredThresholdSet = completeThresholdSet(thresholds);
|
|
226
|
+
const relativeGoldenPath = projectRelativePath(config.projectRoot, goldenPath);
|
|
227
|
+
const verificationEligible = initialManifest !== null &&
|
|
228
|
+
(initialManifest.staleFiles?.length ?? 0) === 0 &&
|
|
229
|
+
relativeGoldenPath !== null &&
|
|
230
|
+
cases.length >= goldenFile.minimumCasesForVerification &&
|
|
231
|
+
answerableCases.length > 0 &&
|
|
232
|
+
unanswerableCases.length > 0 &&
|
|
233
|
+
citationCases.length > 0 &&
|
|
234
|
+
gradedCases.length > 0 &&
|
|
235
|
+
(options.maxTopK === undefined || options.maxTopK >= EVALUATION_DEPTH) &&
|
|
236
|
+
requiredThresholdSet !== null;
|
|
237
|
+
const storedReport = verificationEligible &&
|
|
238
|
+
passed &&
|
|
239
|
+
options.persistCompatibleReport !== false &&
|
|
240
|
+
initialManifest !== null &&
|
|
241
|
+
relativeGoldenPath !== null &&
|
|
242
|
+
requiredThresholdSet !== null
|
|
243
|
+
? await persistQualityReport({
|
|
244
|
+
config,
|
|
245
|
+
indexFingerprint,
|
|
246
|
+
goldenPath: relativeGoldenPath,
|
|
247
|
+
goldenFingerprint: goldenFile.fingerprint,
|
|
248
|
+
rankingPolicyFingerprint: activeRankingPolicyFingerprint,
|
|
249
|
+
metrics,
|
|
250
|
+
thresholds: requiredThresholdSet,
|
|
251
|
+
total: cases.length,
|
|
252
|
+
signal,
|
|
253
|
+
})
|
|
254
|
+
: null;
|
|
116
255
|
const hits = cases.filter((result) => result.hit).length;
|
|
117
256
|
const latencies = cases.map((result) => result.latencyMs).sort((a, b) => a - b);
|
|
257
|
+
const recall = mean(answerableCases.map((result) => result.recall));
|
|
258
|
+
const precision = mean(answerableCases.map((result) => result.precision));
|
|
259
|
+
const meanReciprocalRank = mean(answerableCases.map((result) => result.reciprocalRank));
|
|
260
|
+
const ndcg = mean(answerableCases.map((result) => result.ndcg));
|
|
261
|
+
const abstentionAccuracy = unanswerableCases.length === 0
|
|
262
|
+
? null
|
|
263
|
+
: unanswerableCases.filter((result) => result.abstained).length / unanswerableCases.length;
|
|
118
264
|
throwIfAborted(signal);
|
|
119
265
|
await recordAccess(config, {
|
|
120
266
|
action: "evaluate",
|
|
121
267
|
topK: defaultTopK,
|
|
122
268
|
resultCount: cases.length,
|
|
123
269
|
});
|
|
270
|
+
await flushAccessLog(config);
|
|
124
271
|
throwIfAborted(signal);
|
|
272
|
+
const returnedCases = caseDetailLimit === undefined ? cases : cases.slice(0, caseDetailLimit);
|
|
273
|
+
const omittedCases = cases.length - returnedCases.length;
|
|
125
274
|
return {
|
|
126
275
|
goldenPath,
|
|
127
276
|
embeddingProvider: config.embeddingProvider,
|
|
128
277
|
embeddingModel: config.embeddingModel,
|
|
278
|
+
embeddingModelRevision: config.embeddingModelRevision,
|
|
279
|
+
embeddingModelDigest: config.embeddingModelDigest,
|
|
280
|
+
retrievalProfile: config.retrievalProfile,
|
|
281
|
+
rankingPolicyFingerprint: activeRankingPolicyFingerprint,
|
|
282
|
+
indexFingerprint,
|
|
283
|
+
goldenFingerprint: goldenFile.fingerprint,
|
|
129
284
|
topK: defaultTopK,
|
|
130
285
|
total: cases.length,
|
|
131
286
|
hits,
|
|
132
287
|
misses: cases.length - hits,
|
|
133
288
|
hitRate: hits / cases.length,
|
|
134
|
-
recall
|
|
135
|
-
precision
|
|
136
|
-
meanReciprocalRank
|
|
137
|
-
ndcg
|
|
289
|
+
recall,
|
|
290
|
+
precision,
|
|
291
|
+
meanReciprocalRank,
|
|
292
|
+
ndcg,
|
|
293
|
+
recallAt: {
|
|
294
|
+
1: metrics.recallAt1,
|
|
295
|
+
3: metrics.recallAt3,
|
|
296
|
+
5: metrics.recallAt5,
|
|
297
|
+
10: metrics.recallAt10,
|
|
298
|
+
},
|
|
299
|
+
precisionAt5: metrics.precisionAt5,
|
|
300
|
+
meanReciprocalRankAt10: metrics.meanReciprocalRankAt10,
|
|
301
|
+
ndcgAt10: metrics.ndcgAt10,
|
|
302
|
+
exactCitationRate: metrics.exactCitationRate,
|
|
303
|
+
falsePositiveRate: metrics.falsePositiveRate,
|
|
304
|
+
abstentionAccuracy,
|
|
305
|
+
thresholds,
|
|
306
|
+
gates,
|
|
307
|
+
passed,
|
|
308
|
+
verificationEligible,
|
|
309
|
+
reportStored: storedReport !== null,
|
|
310
|
+
qualityReportFingerprint: storedReport?.qualityReportFingerprint ?? null,
|
|
311
|
+
groups: {
|
|
312
|
+
categories: groupResults(cases, (result) => result.category),
|
|
313
|
+
locales: groupResults(cases, (result) => result.locale),
|
|
314
|
+
},
|
|
138
315
|
p50LatencyMs: percentile(latencies, 0.5),
|
|
139
316
|
p95LatencyMs: percentile(latencies, 0.95),
|
|
140
|
-
cases,
|
|
317
|
+
cases: returnedCases,
|
|
318
|
+
...(omittedCases === 0 ? {} : { omittedCases }),
|
|
141
319
|
};
|
|
142
320
|
}
|
|
321
|
+
function evaluationCaseDetailLimit(value) {
|
|
322
|
+
if (value === undefined) {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
326
|
+
throw new Error("Evaluation case detail limit must be a non-negative integer.");
|
|
327
|
+
}
|
|
328
|
+
return value;
|
|
329
|
+
}
|
|
143
330
|
async function readGoldenFile(goldenPath, signal) {
|
|
144
331
|
let size;
|
|
145
332
|
try {
|
|
@@ -162,62 +349,376 @@ async function readGoldenFile(goldenPath, signal) {
|
|
|
162
349
|
throw error;
|
|
163
350
|
}
|
|
164
351
|
throwIfAborted(signal);
|
|
352
|
+
const fingerprint = createHash("sha256").update(raw).digest("hex");
|
|
165
353
|
const parsed = goldenFileSchema.parse(JSON.parse(raw));
|
|
166
354
|
if (Array.isArray(parsed)) {
|
|
167
|
-
return {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
355
|
+
return {
|
|
356
|
+
minimumCasesForVerification: DEFAULT_MINIMUM_VERIFICATION_CASES,
|
|
357
|
+
thresholds: {},
|
|
358
|
+
queries: parsed.map(normalizeGoldenQuery),
|
|
359
|
+
fingerprint,
|
|
360
|
+
};
|
|
172
361
|
}
|
|
173
|
-
return
|
|
362
|
+
return {
|
|
363
|
+
...(parsed.topK === undefined ? {} : { topK: parsed.topK }),
|
|
364
|
+
minimumCasesForVerification: parsed.minimumCasesForVerification ?? DEFAULT_MINIMUM_VERIFICATION_CASES,
|
|
365
|
+
thresholds: normalizeThresholds(parsed.thresholds),
|
|
366
|
+
queries: parsed.queries.map(normalizeGoldenQuery),
|
|
367
|
+
fingerprint,
|
|
368
|
+
};
|
|
174
369
|
}
|
|
175
370
|
function normalizeGoldenQuery(value) {
|
|
176
|
-
|
|
371
|
+
return {
|
|
177
372
|
query: value.query,
|
|
178
373
|
expectedPaths: value.expectedPaths,
|
|
374
|
+
answerable: value.answerable ?? true,
|
|
375
|
+
relevanceJudgments: value.relevanceJudgments ?? [],
|
|
376
|
+
...(value.expectedCitations === undefined
|
|
377
|
+
? {}
|
|
378
|
+
: { expectedCitations: value.expectedCitations }),
|
|
379
|
+
...(value.id === undefined ? {} : { id: value.id }),
|
|
380
|
+
...(value.category === undefined ? {} : { category: value.category }),
|
|
381
|
+
...(value.locale === undefined ? {} : { locale: value.locale }),
|
|
382
|
+
...(value.maximumVectorDistance === undefined
|
|
383
|
+
? {}
|
|
384
|
+
: { maximumVectorDistance: value.maximumVectorDistance }),
|
|
385
|
+
...(value.topK === undefined ? {} : { topK: value.topK }),
|
|
386
|
+
...(value.includePaths === undefined ? {} : { includePaths: value.includePaths }),
|
|
387
|
+
...(value.excludePaths === undefined ? {} : { excludePaths: value.excludePaths }),
|
|
388
|
+
...(value.contextPaths === undefined ? {} : { contextPaths: value.contextPaths }),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function evaluateCase(goldenQuery, results, topK, latencyMs) {
|
|
392
|
+
const answerable = goldenQuery.answerable ?? true;
|
|
393
|
+
const returnedPaths = results.map((result) => result.relativePath);
|
|
394
|
+
const returnedCitations = results.map(citationForResult);
|
|
395
|
+
const pathJudgments = judgmentsFor("path", goldenQuery.relevanceJudgments ?? [], goldenQuery.expectedPaths);
|
|
396
|
+
const citationJudgments = judgmentsFor("citation", goldenQuery.relevanceJudgments ?? [], goldenQuery.expectedCitations ?? []);
|
|
397
|
+
const requiresExactCitation = citationJudgments.some((judgment) => judgment.relevance > 0);
|
|
398
|
+
const primaryJudgments = requiresExactCitation ? citationJudgments : pathJudgments;
|
|
399
|
+
const returnedValues = requiresExactCitation ? returnedCitations : returnedPaths;
|
|
400
|
+
const matchedPaths = uniqueAtK(returnedPaths, returnedPaths.length).filter((resultPath) => pathJudgments.some((judgment) => judgment.value === resultPath && judgment.relevance > 0));
|
|
401
|
+
const matchedCitations = uniqueAtK(returnedCitations, returnedCitations.length).filter((citation) => citationJudgments.some((judgment) => judgment.value === citation && judgment.relevance > 0));
|
|
402
|
+
const bestRank = firstRelevantRank(returnedValues, primaryJudgments, topK);
|
|
403
|
+
const reciprocalRank = bestRank === null ? 0 : 1 / bestRank;
|
|
404
|
+
const recallAt = {
|
|
405
|
+
1: recallAtK(returnedValues, primaryJudgments, 1),
|
|
406
|
+
3: recallAtK(returnedValues, primaryJudgments, 3),
|
|
407
|
+
5: recallAtK(returnedValues, primaryJudgments, 5),
|
|
408
|
+
10: recallAtK(returnedValues, primaryJudgments, 10),
|
|
179
409
|
};
|
|
180
|
-
|
|
181
|
-
|
|
410
|
+
const abstained = returnedValues.length === 0;
|
|
411
|
+
const falsePositive = !answerable && !abstained;
|
|
412
|
+
const pathHit = matchedPaths.length > 0;
|
|
413
|
+
const exactCitationHit = requiresExactCitation ? matchedCitations.length > 0 : null;
|
|
414
|
+
const hit = answerable ? (requiresExactCitation ? exactCitationHit === true : pathHit) : abstained;
|
|
415
|
+
const relevanceJudgments = [...pathJudgments, ...citationJudgments];
|
|
416
|
+
const result = {
|
|
417
|
+
query: goldenQuery.query,
|
|
418
|
+
expectedPaths: goldenQuery.expectedPaths,
|
|
419
|
+
topK,
|
|
420
|
+
returnedPaths,
|
|
421
|
+
returnedCitations,
|
|
422
|
+
matchedPaths,
|
|
423
|
+
matchedCitations,
|
|
424
|
+
answerable,
|
|
425
|
+
relevanceJudgments,
|
|
426
|
+
abstained,
|
|
427
|
+
falsePositive,
|
|
428
|
+
pathHit,
|
|
429
|
+
exactCitationHit,
|
|
430
|
+
hit,
|
|
431
|
+
bestRank,
|
|
432
|
+
reciprocalRank,
|
|
433
|
+
recall: answerable ? recallAtK(returnedValues, primaryJudgments, topK) : Number(abstained),
|
|
434
|
+
precision: answerable ? precisionAtK(returnedValues, primaryJudgments, topK, false) : 0,
|
|
435
|
+
ndcg: answerable ? gradedNdcgAtK(returnedValues, primaryJudgments, topK) : Number(abstained),
|
|
436
|
+
recallAt,
|
|
437
|
+
precisionAt5: answerable ? precisionAtK(returnedValues, primaryJudgments, 5, true) : 0,
|
|
438
|
+
reciprocalRankAt10: answerable ? reciprocalRankAtK(returnedValues, primaryJudgments, 10) : 0,
|
|
439
|
+
ndcgAt10: answerable ? gradedNdcgAtK(returnedValues, primaryJudgments, 10) : 0,
|
|
440
|
+
latencyMs,
|
|
441
|
+
};
|
|
442
|
+
if (goldenQuery.id !== undefined) {
|
|
443
|
+
result.id = goldenQuery.id;
|
|
444
|
+
}
|
|
445
|
+
if (goldenQuery.category !== undefined) {
|
|
446
|
+
result.category = goldenQuery.category;
|
|
182
447
|
}
|
|
183
|
-
if (
|
|
184
|
-
result.
|
|
448
|
+
if (goldenQuery.locale !== undefined) {
|
|
449
|
+
result.locale = goldenQuery.locale;
|
|
185
450
|
}
|
|
186
|
-
if (
|
|
187
|
-
result.
|
|
451
|
+
if (goldenQuery.expectedCitations !== undefined) {
|
|
452
|
+
result.expectedCitations = goldenQuery.expectedCitations;
|
|
188
453
|
}
|
|
189
|
-
if (
|
|
190
|
-
result.includePaths =
|
|
454
|
+
if (goldenQuery.includePaths !== undefined) {
|
|
455
|
+
result.includePaths = goldenQuery.includePaths;
|
|
191
456
|
}
|
|
192
|
-
if (
|
|
193
|
-
result.excludePaths =
|
|
457
|
+
if (goldenQuery.excludePaths !== undefined) {
|
|
458
|
+
result.excludePaths = goldenQuery.excludePaths;
|
|
194
459
|
}
|
|
195
|
-
if (
|
|
196
|
-
result.contextPaths =
|
|
460
|
+
if (goldenQuery.contextPaths !== undefined) {
|
|
461
|
+
result.contextPaths = goldenQuery.contextPaths;
|
|
197
462
|
}
|
|
198
463
|
return result;
|
|
199
464
|
}
|
|
200
|
-
function
|
|
201
|
-
|
|
465
|
+
function judgmentsFor(kind, declared, legacyExpectedValues) {
|
|
466
|
+
const explicit = declared.filter((judgment) => judgment.kind === kind);
|
|
467
|
+
if (explicit.length > 0) {
|
|
468
|
+
return explicit;
|
|
469
|
+
}
|
|
470
|
+
return legacyExpectedValues.map((value) => ({ kind, value, relevance: 1 }));
|
|
202
471
|
}
|
|
203
|
-
function
|
|
204
|
-
|
|
472
|
+
function applyVectorDistancePolicy(results, maximumVectorDistance) {
|
|
473
|
+
if (maximumVectorDistance === undefined) {
|
|
474
|
+
return results;
|
|
475
|
+
}
|
|
476
|
+
return results.filter((result) => result.distance !== null && result.distance <= maximumVectorDistance);
|
|
477
|
+
}
|
|
478
|
+
function evaluationMetrics(answerableCases, unanswerableCases, citationCases) {
|
|
479
|
+
return {
|
|
480
|
+
recallAt1: mean(answerableCases.map((result) => result.recallAt[1])),
|
|
481
|
+
recallAt3: mean(answerableCases.map((result) => result.recallAt[3])),
|
|
482
|
+
recallAt5: mean(answerableCases.map((result) => result.recallAt[5])),
|
|
483
|
+
recallAt10: mean(answerableCases.map((result) => result.recallAt[10])),
|
|
484
|
+
precisionAt5: mean(answerableCases.map((result) => result.precisionAt5)),
|
|
485
|
+
meanReciprocalRankAt10: mean(answerableCases.map((result) => result.reciprocalRankAt10)),
|
|
486
|
+
ndcgAt10: mean(answerableCases.map((result) => result.ndcgAt10)),
|
|
487
|
+
exactCitationRate: citationCases.length === 0
|
|
488
|
+
? null
|
|
489
|
+
: citationCases.filter((result) => result.exactCitationHit === true).length /
|
|
490
|
+
citationCases.length,
|
|
491
|
+
falsePositiveRate: unanswerableCases.length === 0
|
|
492
|
+
? null
|
|
493
|
+
: unanswerableCases.filter((result) => result.falsePositive).length /
|
|
494
|
+
unanswerableCases.length,
|
|
495
|
+
};
|
|
205
496
|
}
|
|
206
|
-
function
|
|
207
|
-
const
|
|
208
|
-
|
|
497
|
+
function qualityGates(metrics, thresholds) {
|
|
498
|
+
const gates = [];
|
|
499
|
+
addMinimumGate(gates, "recallAt1", thresholds.recallAt1, metrics.recallAt1);
|
|
500
|
+
addMinimumGate(gates, "recallAt3", thresholds.recallAt3, metrics.recallAt3);
|
|
501
|
+
addMinimumGate(gates, "recallAt5", thresholds.recallAt5, metrics.recallAt5);
|
|
502
|
+
addMinimumGate(gates, "recallAt10", thresholds.recallAt10, metrics.recallAt10);
|
|
503
|
+
addMinimumGate(gates, "precisionAt5", thresholds.precisionAt5, metrics.precisionAt5);
|
|
504
|
+
addMinimumGate(gates, "meanReciprocalRankAt10", thresholds.meanReciprocalRankAt10, metrics.meanReciprocalRankAt10);
|
|
505
|
+
addMinimumGate(gates, "ndcgAt10", thresholds.ndcgAt10, metrics.ndcgAt10);
|
|
506
|
+
addMinimumGate(gates, "exactCitationRate", thresholds.exactCitationRate, metrics.exactCitationRate);
|
|
507
|
+
addMaximumGate(gates, "maximumFalsePositiveRate", thresholds.maximumFalsePositiveRate, metrics.falsePositiveRate);
|
|
508
|
+
return gates;
|
|
509
|
+
}
|
|
510
|
+
function addMinimumGate(gates, metric, threshold, actual) {
|
|
511
|
+
if (threshold === undefined) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
gates.push({
|
|
515
|
+
metric,
|
|
516
|
+
direction: "minimum",
|
|
517
|
+
threshold,
|
|
518
|
+
actual,
|
|
519
|
+
applicable: actual !== null,
|
|
520
|
+
passed: actual !== null && actual >= threshold,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
function addMaximumGate(gates, metric, threshold, actual) {
|
|
524
|
+
if (threshold === undefined) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
gates.push({
|
|
528
|
+
metric,
|
|
529
|
+
direction: "maximum",
|
|
530
|
+
threshold,
|
|
531
|
+
actual,
|
|
532
|
+
applicable: actual !== null,
|
|
533
|
+
passed: actual !== null && actual <= threshold,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
function completeThresholdSet(thresholds) {
|
|
537
|
+
if (thresholds.recallAt1 === undefined ||
|
|
538
|
+
thresholds.recallAt3 === undefined ||
|
|
539
|
+
thresholds.recallAt5 === undefined ||
|
|
540
|
+
thresholds.recallAt10 === undefined ||
|
|
541
|
+
thresholds.precisionAt5 === undefined ||
|
|
542
|
+
thresholds.meanReciprocalRankAt10 === undefined ||
|
|
543
|
+
thresholds.ndcgAt10 === undefined ||
|
|
544
|
+
thresholds.exactCitationRate === undefined ||
|
|
545
|
+
thresholds.maximumFalsePositiveRate === undefined) {
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
return {
|
|
549
|
+
recallAt1: thresholds.recallAt1,
|
|
550
|
+
recallAt3: thresholds.recallAt3,
|
|
551
|
+
recallAt5: thresholds.recallAt5,
|
|
552
|
+
recallAt10: thresholds.recallAt10,
|
|
553
|
+
precisionAt5: thresholds.precisionAt5,
|
|
554
|
+
meanReciprocalRankAt10: thresholds.meanReciprocalRankAt10,
|
|
555
|
+
ndcgAt10: thresholds.ndcgAt10,
|
|
556
|
+
exactCitationRate: thresholds.exactCitationRate,
|
|
557
|
+
maximumFalsePositiveRate: thresholds.maximumFalsePositiveRate,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
async function persistQualityReport(input) {
|
|
561
|
+
return withIndexWriteLock(input.config.storageDir, input.signal, async () => {
|
|
562
|
+
const activeManifest = await readIndexManifest(input.config);
|
|
563
|
+
if (!activeManifest ||
|
|
564
|
+
fingerprintIndexManifest(activeManifest) !== input.indexFingerprint ||
|
|
565
|
+
input.metrics.exactCitationRate === null ||
|
|
566
|
+
input.metrics.falsePositiveRate === null) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
const unsignedReport = {
|
|
570
|
+
schemaVersion: 3,
|
|
571
|
+
createdAt: new Date().toISOString(),
|
|
572
|
+
goldenPath: input.goldenPath,
|
|
573
|
+
goldenFingerprint: input.goldenFingerprint,
|
|
574
|
+
indexFingerprint: input.indexFingerprint,
|
|
575
|
+
indexPolicyFingerprint: activeManifest.indexPolicyFingerprint ?? "",
|
|
576
|
+
embeddingProvider: input.config.embeddingProvider,
|
|
577
|
+
embeddingModel: input.config.embeddingModel,
|
|
578
|
+
embeddingModelRevision: input.config.embeddingModelRevision,
|
|
579
|
+
embeddingModelDigest: input.config.embeddingModelDigest,
|
|
580
|
+
retrievalProfile: input.config.retrievalProfile,
|
|
581
|
+
rankingPolicyFingerprint: input.rankingPolicyFingerprint,
|
|
582
|
+
total: input.total,
|
|
583
|
+
metrics: {
|
|
584
|
+
recallAt1: input.metrics.recallAt1,
|
|
585
|
+
recallAt3: input.metrics.recallAt3,
|
|
586
|
+
recallAt5: input.metrics.recallAt5,
|
|
587
|
+
recallAt10: input.metrics.recallAt10,
|
|
588
|
+
precisionAt5: input.metrics.precisionAt5,
|
|
589
|
+
meanReciprocalRankAt10: input.metrics.meanReciprocalRankAt10,
|
|
590
|
+
ndcgAt10: input.metrics.ndcgAt10,
|
|
591
|
+
exactCitationRate: input.metrics.exactCitationRate,
|
|
592
|
+
falsePositiveRate: input.metrics.falsePositiveRate,
|
|
593
|
+
},
|
|
594
|
+
thresholds: input.thresholds,
|
|
595
|
+
passed: true,
|
|
596
|
+
verificationEligible: true,
|
|
597
|
+
};
|
|
598
|
+
const qualityReport = {
|
|
599
|
+
...unsignedReport,
|
|
600
|
+
qualityReportFingerprint: fingerprintQualityReport(unsignedReport),
|
|
601
|
+
};
|
|
602
|
+
await writeIndexManifest({ ...activeManifest, qualityReport }, input.config);
|
|
603
|
+
return qualityReport;
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
function groupResults(cases, groupFor) {
|
|
607
|
+
const grouped = new Map();
|
|
608
|
+
for (const result of cases) {
|
|
609
|
+
const group = groupFor(result);
|
|
610
|
+
if (group === undefined) {
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
const members = grouped.get(group) ?? [];
|
|
614
|
+
members.push(result);
|
|
615
|
+
grouped.set(group, members);
|
|
616
|
+
}
|
|
617
|
+
return Object.fromEntries([...grouped.entries()]
|
|
618
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
619
|
+
.map(([group, members]) => [group, summarizeGroup(members)]));
|
|
620
|
+
}
|
|
621
|
+
function summarizeGroup(cases) {
|
|
622
|
+
const answerable = cases.filter((result) => result.answerable);
|
|
623
|
+
const unanswerable = cases.filter((result) => !result.answerable);
|
|
624
|
+
return {
|
|
625
|
+
total: cases.length,
|
|
626
|
+
answerable: answerable.length,
|
|
627
|
+
unanswerable: unanswerable.length,
|
|
628
|
+
recallAt10: mean(answerable.map((result) => result.recallAt[10])),
|
|
629
|
+
precisionAt5: mean(answerable.map((result) => result.precisionAt5)),
|
|
630
|
+
meanReciprocalRankAt10: mean(answerable.map((result) => result.reciprocalRankAt10)),
|
|
631
|
+
ndcgAt10: mean(answerable.map((result) => result.ndcgAt10)),
|
|
632
|
+
falsePositiveRate: unanswerable.length === 0
|
|
633
|
+
? 0
|
|
634
|
+
: unanswerable.filter((result) => result.falsePositive).length / unanswerable.length,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function projectRelativePath(projectRoot, targetPath) {
|
|
638
|
+
const relativePath = path.relative(projectRoot, targetPath);
|
|
639
|
+
if (relativePath === ".." ||
|
|
640
|
+
relativePath.startsWith(`..${path.sep}`) ||
|
|
641
|
+
path.isAbsolute(relativePath)) {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
return relativePath.split(path.sep).join("/");
|
|
645
|
+
}
|
|
646
|
+
function recallAtK(returned, judgments, topK) {
|
|
647
|
+
const relevant = new Set(judgments.filter((judgment) => judgment.relevance > 0).map((judgment) => judgment.value));
|
|
648
|
+
if (relevant.size === 0) {
|
|
649
|
+
return 0;
|
|
650
|
+
}
|
|
651
|
+
return uniqueAtK(returned, topK).filter((value) => relevant.has(value)).length / relevant.size;
|
|
652
|
+
}
|
|
653
|
+
function precisionAtK(returned, judgments, topK, fixedDenominator) {
|
|
654
|
+
const values = uniqueAtK(returned, topK);
|
|
655
|
+
const relevant = new Set(judgments.filter((judgment) => judgment.relevance > 0).map((judgment) => judgment.value));
|
|
656
|
+
const denominator = fixedDenominator ? topK : values.length;
|
|
657
|
+
if (denominator === 0) {
|
|
658
|
+
return 0;
|
|
659
|
+
}
|
|
660
|
+
return values.filter((value) => relevant.has(value)).length / denominator;
|
|
661
|
+
}
|
|
662
|
+
function reciprocalRankAtK(returned, judgments, topK) {
|
|
663
|
+
const rank = firstRelevantRank(returned, judgments, topK);
|
|
664
|
+
return rank === null ? 0 : 1 / rank;
|
|
665
|
+
}
|
|
666
|
+
function firstRelevantRank(returned, judgments, topK) {
|
|
667
|
+
const relevant = new Set(judgments.filter((judgment) => judgment.relevance > 0).map((judgment) => judgment.value));
|
|
668
|
+
const index = returned.slice(0, topK).findIndex((value) => relevant.has(value));
|
|
669
|
+
return index < 0 ? null : index + 1;
|
|
670
|
+
}
|
|
671
|
+
function gradedNdcgAtK(returned, judgments, topK) {
|
|
672
|
+
const relevance = new Map(judgments.map((judgment) => [judgment.value, judgment.relevance]));
|
|
209
673
|
const seen = new Set();
|
|
210
|
-
const dcg =
|
|
211
|
-
if (
|
|
674
|
+
const dcg = returned.slice(0, topK).reduce((score, value, index) => {
|
|
675
|
+
if (seen.has(value)) {
|
|
212
676
|
return score;
|
|
213
677
|
}
|
|
214
678
|
seen.add(value);
|
|
215
|
-
|
|
679
|
+
const grade = relevance.get(value) ?? 0;
|
|
680
|
+
return score + (2 ** grade - 1) / Math.log2(index + 2);
|
|
216
681
|
}, 0);
|
|
217
|
-
const
|
|
218
|
-
|
|
682
|
+
const idealGrades = [
|
|
683
|
+
...new Map(judgments.map((judgment) => [judgment.value, judgment.relevance])).values(),
|
|
684
|
+
]
|
|
685
|
+
.filter((grade) => grade > 0)
|
|
686
|
+
.sort((left, right) => right - left)
|
|
687
|
+
.slice(0, topK);
|
|
688
|
+
const idealDcg = idealGrades.reduce((score, grade, index) => score + (2 ** grade - 1) / Math.log2(index + 2), 0);
|
|
219
689
|
return idealDcg === 0 ? 0 : dcg / idealDcg;
|
|
220
690
|
}
|
|
691
|
+
function normalizeThresholds(value) {
|
|
692
|
+
if (value === undefined) {
|
|
693
|
+
return {};
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
...(value.recallAt1 === undefined ? {} : { recallAt1: value.recallAt1 }),
|
|
697
|
+
...(value.recallAt3 === undefined ? {} : { recallAt3: value.recallAt3 }),
|
|
698
|
+
...(value.recallAt5 === undefined ? {} : { recallAt5: value.recallAt5 }),
|
|
699
|
+
...(value.recallAt10 === undefined ? {} : { recallAt10: value.recallAt10 }),
|
|
700
|
+
...(value.precisionAt5 === undefined ? {} : { precisionAt5: value.precisionAt5 }),
|
|
701
|
+
...(value.meanReciprocalRankAt10 === undefined
|
|
702
|
+
? {}
|
|
703
|
+
: { meanReciprocalRankAt10: value.meanReciprocalRankAt10 }),
|
|
704
|
+
...(value.ndcgAt10 === undefined ? {} : { ndcgAt10: value.ndcgAt10 }),
|
|
705
|
+
...(value.exactCitationRate === undefined
|
|
706
|
+
? {}
|
|
707
|
+
: { exactCitationRate: value.exactCitationRate }),
|
|
708
|
+
...(value.maximumFalsePositiveRate === undefined
|
|
709
|
+
? {}
|
|
710
|
+
: { maximumFalsePositiveRate: value.maximumFalsePositiveRate }),
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
function uniqueAtK(values, topK) {
|
|
714
|
+
return [...new Set(values.slice(0, topK))];
|
|
715
|
+
}
|
|
716
|
+
function boundedTopK(topK, maxTopK) {
|
|
717
|
+
return maxTopK === undefined ? topK : Math.min(topK, maxTopK);
|
|
718
|
+
}
|
|
719
|
+
function citationForResult(result) {
|
|
720
|
+
return result.citation;
|
|
721
|
+
}
|
|
221
722
|
function percentile(sortedValues, quantile) {
|
|
222
723
|
if (sortedValues.length === 0) {
|
|
223
724
|
return 0;
|