@fodx/codelens 2.5.0 → 2.5.1
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 +115 -56
- package/build/src/cli.js +44 -14
- package/build/src/cli.js.map +1 -1
- package/build/src/eval/baseline.js +52 -37
- package/build/src/eval/baseline.js.map +1 -1
- package/build/src/eval/evaluator.js +295 -165
- package/build/src/eval/evaluator.js.map +1 -1
- package/build/src/eval/report.js +179 -60
- package/build/src/eval/report.js.map +1 -1
- package/build/src/eval/task-file.js +130 -0
- package/build/src/eval/task-file.js.map +1 -0
- package/build/src/eval/tasks.js +35 -25
- package/build/src/eval/tasks.js.map +1 -1
- package/package.json +1 -1
|
@@ -11,19 +11,23 @@ import { ensureFreshIndex } from "../index/reindex.js";
|
|
|
11
11
|
import { scanFiles } from "../index/scanner.js";
|
|
12
12
|
import { setPendingPaths } from "../index/staleness.js";
|
|
13
13
|
import { ctxImpact } from "../tools/impact.js";
|
|
14
|
+
import { DEFAULT_WEIGHTS } from "../search/rank.js";
|
|
14
15
|
import { ctxSearch } from "../tools/search.js";
|
|
15
16
|
import { isInsideRepo, resolveReal } from "../util/paths.js";
|
|
16
17
|
import { runRgBaseline } from "./baseline.js";
|
|
17
|
-
import { aggregateRuns, artifactPaths, evaluateObservation, writeEvalArtifacts } from "./report.js";
|
|
18
|
+
import { aggregateRuns, artifactPaths, evaluateObservation, graphAggregates, retrievalAggregates, writeEvalArtifacts, } from "./report.js";
|
|
19
|
+
import { loadEvalTaskFile, requiredTaskPaths } from "./task-file.js";
|
|
18
20
|
import { generateEvalTasks, seededShuffle } from "./tasks.js";
|
|
21
|
+
const RETRIEVAL_ARMS = ["full", "lexical", "fts", "rg"];
|
|
22
|
+
const NON_GRAPH_WEIGHT = 1 - DEFAULT_WEIGHTS.graph;
|
|
19
23
|
const LEXICAL_WEIGHTS = {
|
|
20
|
-
fts:
|
|
21
|
-
symbol:
|
|
24
|
+
fts: DEFAULT_WEIGHTS.fts / NON_GRAPH_WEIGHT,
|
|
25
|
+
symbol: DEFAULT_WEIGHTS.symbol / NON_GRAPH_WEIGHT,
|
|
22
26
|
graph: 0,
|
|
23
|
-
code:
|
|
24
|
-
pathHit:
|
|
25
|
-
exact:
|
|
26
|
-
recency:
|
|
27
|
+
code: DEFAULT_WEIGHTS.code / NON_GRAPH_WEIGHT,
|
|
28
|
+
pathHit: DEFAULT_WEIGHTS.pathHit / NON_GRAPH_WEIGHT,
|
|
29
|
+
exact: DEFAULT_WEIGHTS.exact / NON_GRAPH_WEIGHT,
|
|
30
|
+
recency: DEFAULT_WEIGHTS.recency,
|
|
27
31
|
};
|
|
28
32
|
const FTS_ONLY_WEIGHTS = {
|
|
29
33
|
fts: 1,
|
|
@@ -69,9 +73,7 @@ class EvalProgressTracker {
|
|
|
69
73
|
try {
|
|
70
74
|
this.callback?.({ ...event, elapsedMs: performance.now() - this.started });
|
|
71
75
|
}
|
|
72
|
-
catch {
|
|
73
|
-
// Progress reporting must never break an evaluation.
|
|
74
|
-
}
|
|
76
|
+
catch { /* progress is best-effort */ }
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
export function defaultEvalOptions(repoRoot) {
|
|
@@ -82,12 +84,13 @@ export function defaultEvalOptions(repoRoot) {
|
|
|
82
84
|
repeats: 1,
|
|
83
85
|
resultLimit: 10,
|
|
84
86
|
scales: [500, 2000, "all"],
|
|
87
|
+
suites: ["retrieval", "graph", "freshness"],
|
|
85
88
|
quick: false,
|
|
86
|
-
freshness: true,
|
|
87
89
|
thresholds: {
|
|
88
90
|
minRecallAtK: 0.6,
|
|
89
91
|
minMrr: 0.5,
|
|
90
92
|
minSuccessRate: 0.7,
|
|
93
|
+
minGraphPrecision: 0.5,
|
|
91
94
|
},
|
|
92
95
|
};
|
|
93
96
|
}
|
|
@@ -96,7 +99,7 @@ export function quickEvalOptions(repoRoot) {
|
|
|
96
99
|
...defaultEvalOptions(repoRoot),
|
|
97
100
|
taskLimit: 20,
|
|
98
101
|
scales: [500],
|
|
99
|
-
|
|
102
|
+
suites: ["retrieval", "graph"],
|
|
100
103
|
quick: true,
|
|
101
104
|
};
|
|
102
105
|
}
|
|
@@ -106,6 +109,7 @@ export function runRepositoryEval(input) {
|
|
|
106
109
|
const progress = new EvalProgressTracker(input.onProgress, started);
|
|
107
110
|
let temp;
|
|
108
111
|
try {
|
|
112
|
+
const suites = normalizeSuites(input.suites);
|
|
109
113
|
progress.start("repository", `Validating repository ${input.repoRoot}`);
|
|
110
114
|
const requestedRoot = resolveReal(input.repoRoot);
|
|
111
115
|
const scope = detectScope(requestedRoot);
|
|
@@ -119,21 +123,34 @@ export function runRepositoryEval(input) {
|
|
|
119
123
|
if (scanned.length === 0)
|
|
120
124
|
throw new Error(`no indexable files found: ${repoRoot}`);
|
|
121
125
|
progress.complete("scan", `Found ${scanned.length} indexable files`, { current: scanned.length, total: scanned.length });
|
|
122
|
-
progress.start("setup", "Preparing
|
|
126
|
+
progress.start("setup", "Preparing frozen tasks, nested scales, and output paths");
|
|
123
127
|
const outputDirectory = input.outputDir ? canonicalOutputPath(input.outputDir) : defaultOutputDirectory(repoRoot);
|
|
124
128
|
if (isInsideRepo(repoRoot, outputDirectory))
|
|
125
129
|
throw new Error("evaluation output must be outside the target repository");
|
|
126
130
|
const artifacts = artifactPaths(outputDirectory);
|
|
131
|
+
const loaded = input.taskFile ? loadEvalTaskFile(input.taskFile) : undefined;
|
|
132
|
+
let frozenTasks = (loaded?.tasks ?? []).filter((task) => suites.includes(task.suite));
|
|
133
|
+
const taskSuites = suites.filter((suite) => suite === "retrieval" || suite === "graph");
|
|
134
|
+
const taskSource = taskSuites.length === 0
|
|
135
|
+
? { kind: "none", independentGroundTruth: true }
|
|
136
|
+
: loaded?.source ?? { kind: "automatic-self-evaluation", independentGroundTruth: false };
|
|
137
|
+
const scaleSpecs = taskSuites.length > 0 ? resolveScaleSpecs(input.scales, scanned.length) : [];
|
|
138
|
+
const required = loaded ? requiredTaskPaths(frozenTasks) : new Set();
|
|
139
|
+
ensureRequiredPaths(scanned, required);
|
|
140
|
+
if (scaleSpecs.length > 0 && required.size > scaleSpecs[0].count) {
|
|
141
|
+
throw new Error(`smallest scale (${scaleSpecs[0].count}) cannot contain ${required.size} required task paths`);
|
|
142
|
+
}
|
|
143
|
+
const orderedFiles = nestedFileOrder(scanned, required, input.seed);
|
|
127
144
|
temp = mkdtempSync(join(tmpdir(), "codelens-eval-"));
|
|
128
145
|
const skipped = [];
|
|
129
|
-
const scaleSpecs = resolveScaleSpecs(input.scales, scanned.length);
|
|
130
146
|
const scales = [];
|
|
131
|
-
progress.complete("setup", `Prepared ${scaleSpecs.length} scale${scaleSpecs.length === 1 ? "" : "s"}
|
|
147
|
+
progress.complete("setup", `Prepared ${scaleSpecs.length} nested scale${scaleSpecs.length === 1 ? "" : "s"}${loaded ? ` from frozen tasks (${taskSource.source ?? "task file"})` : " from automatic self-evaluation"}`);
|
|
132
148
|
for (let scaleIndex = 0; scaleIndex < scaleSpecs.length; scaleIndex++) {
|
|
133
149
|
const spec = scaleSpecs[scaleIndex];
|
|
134
150
|
const scaleCounts = { scale: spec.label, current: scaleIndex + 1, total: scaleSpecs.length };
|
|
135
151
|
progress.start("scale", `Starting scale ${spec.label} with ${spec.count} files`, scaleCounts);
|
|
136
|
-
const selected =
|
|
152
|
+
const selected = orderedFiles.slice(0, spec.count).sort((a, b) => a.path.localeCompare(b.path));
|
|
153
|
+
const inventory = selected.map((file) => file.path);
|
|
137
154
|
const dbPath = join(temp, `eval-${scaleIndex}.db`);
|
|
138
155
|
const db = openDb(dbPath);
|
|
139
156
|
try {
|
|
@@ -144,28 +161,40 @@ export function runRepositoryEval(input) {
|
|
|
144
161
|
});
|
|
145
162
|
const indexMs = performance.now() - indexStart;
|
|
146
163
|
progress.complete("index", `Indexed ${build.indexedFiles} files and ${build.totalChunks} chunks for scale ${spec.label} in ${formatDuration(indexMs)} (${build.skipped} skipped)`, { scale: spec.label, current: selected.length, total: selected.length });
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
progress.complete("tasks", `Generated ${tasks.length} tasks for scale ${spec.label}`, { scale: spec.label, current: tasks.length, total: scaleTaskLimit });
|
|
152
|
-
if (scaleTaskLimit === 0)
|
|
153
|
-
skipped.push(`${spec.label}: no tasks allocated because --tasks is lower than the number of scales.`);
|
|
154
|
-
for (const type of ["locate", "callers", "tests", "history"]) {
|
|
155
|
-
if (!tasks.some((task) => task.type === type))
|
|
156
|
-
skipped.push(`${spec.label}: no ${type} tasks were generated.`);
|
|
164
|
+
if (!loaded && scaleIndex === 0) {
|
|
165
|
+
progress.start("tasks", `Generating up to ${input.taskLimit} automatic self-evaluation tasks`, { scale: spec.label });
|
|
166
|
+
frozenTasks = generateEvalTasks(db, build.indexId, repoRoot, inventory, input.taskLimit, input.seed, suites);
|
|
167
|
+
progress.complete("tasks", `Froze ${frozenTasks.length} tasks for every scale`, { scale: spec.label, current: frozenTasks.length, total: input.taskLimit });
|
|
157
168
|
}
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
169
|
+
const tasks = frozenTasks;
|
|
170
|
+
for (const suite of taskSuites) {
|
|
171
|
+
if (!tasks.some((task) => task.suite === suite))
|
|
172
|
+
skipped.push(`${suite}: no tasks were available.`);
|
|
173
|
+
}
|
|
174
|
+
const retrievalTasks = tasks.filter((task) => task.suite === "retrieval");
|
|
175
|
+
const graphTasks = tasks.filter((task) => task.suite === "graph" && (loaded !== undefined || scaleIndex === 0));
|
|
176
|
+
if (!loaded && scaleIndex > 0 && tasks.some((task) => task.suite === "graph")) {
|
|
177
|
+
skipped.push("automatic graph self-consistency runs only at the label-generating smallest tier; frozen independent graph tasks may run across every tier.");
|
|
178
|
+
}
|
|
179
|
+
const runs = [];
|
|
180
|
+
if (retrievalTasks.length > 0) {
|
|
181
|
+
const total = retrievalTasks.length * RETRIEVAL_ARMS.length * Math.max(1, input.repeats);
|
|
182
|
+
progress.start("retrieval", `Running ${total} counterbalanced retrieval checks for scale ${spec.label}`, { scale: spec.label, current: 0, total });
|
|
183
|
+
runs.push(...runRetrievalTaskSet(db, repoRoot, inventory, retrievalTasks, input.repeats, input.resultLimit, (current) => {
|
|
184
|
+
progress.update("retrieval", `Completed ${current}/${total} retrieval checks for scale ${spec.label}`, { scale: spec.label, current, total });
|
|
185
|
+
}));
|
|
186
|
+
progress.complete("retrieval", `Completed ${total} retrieval checks for scale ${spec.label}`, { scale: spec.label, current: total, total });
|
|
167
187
|
}
|
|
168
|
-
|
|
188
|
+
if (graphTasks.length > 0) {
|
|
189
|
+
const total = graphTasks.length * Math.max(1, input.repeats);
|
|
190
|
+
progress.start("graph", `Running ${total} known-target graph checks for scale ${spec.label}`, { scale: spec.label, current: 0, total });
|
|
191
|
+
runs.push(...runGraphTaskSet(db, graphTasks, input.repeats, input.resultLimit, (current) => {
|
|
192
|
+
progress.update("graph", `Completed ${current}/${total} graph checks for scale ${spec.label}`, { scale: spec.label, current, total });
|
|
193
|
+
}));
|
|
194
|
+
progress.complete("graph", `Completed ${total} graph checks for scale ${spec.label}`, { scale: spec.label, current: total, total });
|
|
195
|
+
}
|
|
196
|
+
if (runs.some((run) => run.arm === "rg" && run.error === "ripgrep is not installed"))
|
|
197
|
+
skipped.push("rg baseline unavailable because ripgrep is not installed.");
|
|
169
198
|
scales.push({
|
|
170
199
|
label: spec.label,
|
|
171
200
|
requestedFiles: spec.requested,
|
|
@@ -176,44 +205,72 @@ export function runRepositoryEval(input) {
|
|
|
176
205
|
dbBytes: databaseBytes(dbPath),
|
|
177
206
|
tasks,
|
|
178
207
|
runs,
|
|
179
|
-
|
|
208
|
+
retrieval: retrievalAggregates(tasks, runs, input.seed + scaleIndex * 1009),
|
|
209
|
+
graph: graphAggregates(tasks, runs, input.seed + scaleIndex * 2003),
|
|
180
210
|
});
|
|
181
|
-
progress.complete("scale", `Finished scale ${spec.label}: ${
|
|
211
|
+
progress.complete("scale", `Finished scale ${spec.label}: ${retrievalTasks.length} retrieval and ${graphTasks.length} graph tasks`, scaleCounts);
|
|
182
212
|
}
|
|
183
213
|
finally {
|
|
184
214
|
db.close();
|
|
185
215
|
}
|
|
186
216
|
}
|
|
187
217
|
let freshness;
|
|
188
|
-
if (
|
|
218
|
+
if (suites.includes("freshness")) {
|
|
189
219
|
progress.start("freshness", "Starting detached-worktree edit/delete freshness probe");
|
|
190
220
|
freshness = runFreshnessEval(repoRoot, input.seed, progress);
|
|
191
|
-
const
|
|
221
|
+
const message = freshness.attempted
|
|
192
222
|
? `Freshness probe ${freshness.passed ? "passed" : "failed"} in ${formatDuration(freshness.elapsedMs ?? 0)}`
|
|
193
223
|
: `Freshness probe skipped: ${freshness.skippedReason ?? "not attempted"}`;
|
|
194
224
|
if (freshness.attempted && freshness.passed)
|
|
195
|
-
progress.complete("freshness",
|
|
225
|
+
progress.complete("freshness", message);
|
|
196
226
|
else if (freshness.attempted)
|
|
197
|
-
progress.error("freshness",
|
|
227
|
+
progress.error("freshness", message);
|
|
198
228
|
else
|
|
199
|
-
progress.skipped("freshness",
|
|
229
|
+
progress.skipped("freshness", message);
|
|
200
230
|
}
|
|
201
231
|
else {
|
|
202
232
|
freshness = disabledFreshness();
|
|
203
|
-
progress.skipped("freshness", "Freshness
|
|
233
|
+
progress.skipped("freshness", "Freshness suite disabled");
|
|
204
234
|
}
|
|
205
235
|
if (freshness.skippedReason)
|
|
206
236
|
skipped.push(`freshness: ${freshness.skippedReason}`);
|
|
207
|
-
progress.start("thresholds", "Checking
|
|
237
|
+
progress.start("thresholds", "Checking quality thresholds on eligible task types");
|
|
238
|
+
const hasThresholdEligibleRetrieval = frozenTasks.some((task) => task.suite === "retrieval" && task.contributesToThresholds);
|
|
239
|
+
const hasThresholdEligibleGraph = frozenTasks.some((task) => task.suite === "graph" && task.groundTruth.independent && task.contributesToThresholds);
|
|
240
|
+
if (suites.includes("retrieval") && !hasThresholdEligibleRetrieval) {
|
|
241
|
+
skipped.push("no threshold-eligible retrieval tasks; retrieval quality is informational only.");
|
|
242
|
+
}
|
|
243
|
+
if (suites.includes("graph") && !hasThresholdEligibleGraph) {
|
|
244
|
+
skipped.push("no independently labeled threshold-eligible graph tasks; graph self-consistency is informational only.");
|
|
245
|
+
}
|
|
208
246
|
const thresholdFailures = scales.flatMap((scale) => thresholdFailuresFor(scale, input));
|
|
209
|
-
|
|
247
|
+
for (const scale of scales) {
|
|
248
|
+
const executionErrors = new Set(scale.runs
|
|
249
|
+
.filter((run) => run.error && !(run.arm === "rg" && run.error === "ripgrep is not installed"))
|
|
250
|
+
.map((run) => `${run.arm}: ${run.error}`));
|
|
251
|
+
for (const error of executionErrors)
|
|
252
|
+
thresholdFailures.push(`${scale.label}: evaluation arm error — ${error}`);
|
|
253
|
+
}
|
|
254
|
+
if (suites.includes("freshness") && freshness.attempted && !freshness.passed)
|
|
210
255
|
thresholdFailures.push("Freshness edit/delete probe failed.");
|
|
211
256
|
if (initialStatus !== gitStatus(repoRoot))
|
|
212
257
|
thresholdFailures.push("Target repository worktree changed during evaluation.");
|
|
213
|
-
progress.complete("thresholds", thresholdFailures.length === 0 ? "All
|
|
214
|
-
const
|
|
258
|
+
progress.complete("thresholds", thresholdFailures.length === 0 ? "All applicable thresholds passed" : `${thresholdFailures.length} threshold${thresholdFailures.length === 1 ? "" : "s"} failed`);
|
|
259
|
+
const graphTasks = frozenTasks.filter((task) => task.suite === "graph");
|
|
260
|
+
const graphIndependentGroundTruth = graphTasks.length > 0 && graphTasks.every((task) => task.groundTruth.independent);
|
|
261
|
+
const hasTaskEvidence = taskSuites.length === 0 || scales.some((scale) => scale.runs.length > 0);
|
|
262
|
+
const hasFreshnessEvidence = !suites.includes("freshness") || freshness.attempted || !!freshness.skippedReason;
|
|
215
263
|
const result = {
|
|
216
|
-
version:
|
|
264
|
+
version: 2,
|
|
265
|
+
methodology: {
|
|
266
|
+
taskSource,
|
|
267
|
+
taskSetDigest: taskSetDigest(frozenTasks),
|
|
268
|
+
retrievalComparable: true,
|
|
269
|
+
graphIndependentGroundTruth,
|
|
270
|
+
repeatsAreTimingSamples: true,
|
|
271
|
+
scaleTasksFrozen: true,
|
|
272
|
+
notes: methodologyNotes(taskSource, suites, graphTasks.length > 0, graphIndependentGroundTruth),
|
|
273
|
+
},
|
|
217
274
|
repository: {
|
|
218
275
|
root: repoRoot,
|
|
219
276
|
branch: scope.branch,
|
|
@@ -227,14 +284,14 @@ export function runRepositoryEval(input) {
|
|
|
227
284
|
repeats: input.repeats,
|
|
228
285
|
resultLimit: input.resultLimit,
|
|
229
286
|
scales: input.scales,
|
|
287
|
+
suites,
|
|
230
288
|
quick: input.quick,
|
|
231
|
-
freshness: input.freshness,
|
|
232
289
|
thresholds: input.thresholds,
|
|
233
290
|
},
|
|
234
291
|
startedAt,
|
|
235
|
-
completedAt,
|
|
292
|
+
completedAt: new Date().toISOString(),
|
|
236
293
|
durationMs: performance.now() - started,
|
|
237
|
-
pass:
|
|
294
|
+
pass: hasTaskEvidence && hasFreshnessEvidence && thresholdFailures.length === 0,
|
|
238
295
|
thresholdFailures,
|
|
239
296
|
skipped: [...new Set(skipped)],
|
|
240
297
|
scales,
|
|
@@ -291,64 +348,113 @@ function buildSubsetIndex(db, scope, files, onProgress) {
|
|
|
291
348
|
setPendingPaths(indexId, []);
|
|
292
349
|
return { indexId, indexedFiles, totalChunks, skipped };
|
|
293
350
|
}
|
|
294
|
-
function
|
|
351
|
+
function runRetrievalTaskSet(db, repoRoot, inventory, tasks, repeats, limit, onProgress) {
|
|
352
|
+
const repeatCount = Math.max(1, repeats);
|
|
353
|
+
if (tasks[0]) {
|
|
354
|
+
for (const arm of RETRIEVAL_ARMS)
|
|
355
|
+
runRetrievalArm(db, repoRoot, inventory, tasks[0], limit, arm);
|
|
356
|
+
}
|
|
357
|
+
const runs = [];
|
|
358
|
+
const nextTypeOffset = new Map();
|
|
359
|
+
const typeOffsets = tasks.map((task) => {
|
|
360
|
+
const offset = nextTypeOffset.get(task.type) ?? 0;
|
|
361
|
+
nextTypeOffset.set(task.type, offset + 1);
|
|
362
|
+
return offset;
|
|
363
|
+
});
|
|
364
|
+
for (let repeat = 0; repeat < repeatCount; repeat++) {
|
|
365
|
+
for (let taskIndex = 0; taskIndex < tasks.length; taskIndex++) {
|
|
366
|
+
const task = tasks[taskIndex];
|
|
367
|
+
const offset = (typeOffsets[taskIndex] + repeat) % RETRIEVAL_ARMS.length;
|
|
368
|
+
const orderedArms = [...RETRIEVAL_ARMS.slice(offset), ...RETRIEVAL_ARMS.slice(0, offset)];
|
|
369
|
+
for (let order = 0; order < orderedArms.length; order++) {
|
|
370
|
+
const arm = orderedArms[order];
|
|
371
|
+
const observation = runRetrievalArm(db, repoRoot, inventory, task, limit, arm);
|
|
372
|
+
runs.push(evaluateObservation(task, arm, observation, limit, repeat, order));
|
|
373
|
+
onProgress?.(runs.length);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return runs;
|
|
378
|
+
}
|
|
379
|
+
function runGraphTaskSet(db, tasks, repeats, limit, onProgress) {
|
|
380
|
+
if (tasks[0])
|
|
381
|
+
runGraphArm(db, tasks[0], limit);
|
|
295
382
|
const runs = [];
|
|
296
|
-
const arms = ["full", "lexical", "fts", "rg"];
|
|
297
|
-
const total = tasks.length * arms.length * Math.max(1, repeats);
|
|
298
|
-
const progressInterval = Math.max(1, Math.ceil(total / 20));
|
|
299
383
|
for (let repeat = 0; repeat < Math.max(1, repeats); repeat++) {
|
|
300
384
|
for (const task of tasks) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
: runCodeLens(db, task, limit, arm);
|
|
305
|
-
runs.push(evaluateObservation(task, arm, observation, limit));
|
|
306
|
-
if (runs.length === total || runs.length % progressInterval === 0)
|
|
307
|
-
onProgress?.(runs.length, total);
|
|
308
|
-
}
|
|
385
|
+
const observation = runGraphArm(db, task, limit);
|
|
386
|
+
runs.push(evaluateObservation(task, "graph", observation, limit, repeat, 0));
|
|
387
|
+
onProgress?.(runs.length);
|
|
309
388
|
}
|
|
310
389
|
}
|
|
311
390
|
return runs;
|
|
312
391
|
}
|
|
313
|
-
function
|
|
392
|
+
function runRetrievalArm(db, repoRoot, inventory, task, limit, arm) {
|
|
393
|
+
if (arm === "rg")
|
|
394
|
+
return runRgBaseline(repoRoot, task, limit, inventory);
|
|
314
395
|
const start = performance.now();
|
|
315
396
|
try {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
397
|
+
const weights = arm === "lexical" ? LEXICAL_WEIGHTS : arm === "fts" ? FTS_ONLY_WEIGHTS : undefined;
|
|
398
|
+
const foundPaths = [];
|
|
399
|
+
const seen = new Set();
|
|
400
|
+
let cursor;
|
|
401
|
+
let toolCalls = 0;
|
|
402
|
+
let bytesServed = 0;
|
|
403
|
+
// ctxSearch ranks chunks. Page through its bounded candidate set so every
|
|
404
|
+
// retrieval arm is scored on up to K unique files, matching rg's unit.
|
|
405
|
+
for (let page = 0; page < 4 && foundPaths.length < limit; page++) {
|
|
406
|
+
const result = ctxSearch(db, task.query, { limit, cursor, snippet: "none", weights });
|
|
407
|
+
toolCalls++;
|
|
408
|
+
bytesServed += Buffer.byteLength(JSON.stringify(result));
|
|
409
|
+
for (const item of result.results) {
|
|
410
|
+
if (seen.has(item.path))
|
|
411
|
+
continue;
|
|
412
|
+
seen.add(item.path);
|
|
413
|
+
foundPaths.push(item.path);
|
|
414
|
+
if (foundPaths.length >= limit)
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
if (!result.nextCursor)
|
|
418
|
+
break;
|
|
419
|
+
cursor = result.nextCursor;
|
|
332
420
|
}
|
|
333
|
-
foundPaths = [...new Set(foundPaths)].slice(0, limit);
|
|
334
421
|
return {
|
|
335
422
|
foundPaths,
|
|
336
|
-
toolCalls
|
|
337
|
-
bytesServed
|
|
338
|
-
bytesRead: 0,
|
|
423
|
+
toolCalls,
|
|
424
|
+
bytesServed,
|
|
339
425
|
elapsedMs: performance.now() - start,
|
|
340
426
|
};
|
|
341
427
|
}
|
|
342
428
|
catch (error) {
|
|
429
|
+
return failedObservation(start, error);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function runGraphArm(db, task, limit) {
|
|
433
|
+
const start = performance.now();
|
|
434
|
+
try {
|
|
435
|
+
if (!task.targetPath || (task.type !== "callers" && task.type !== "tests"))
|
|
436
|
+
throw new Error("graph task requires callers/tests type and targetPath");
|
|
437
|
+
const result = ctxImpact(db, { path: task.targetPath, depth: 1, includeTests: true });
|
|
438
|
+
const paths = task.type === "tests" ? result.affectedTests.map((item) => item.path) : result.callers.map((item) => item.path);
|
|
343
439
|
return {
|
|
344
|
-
foundPaths: [],
|
|
440
|
+
foundPaths: [...new Set(paths)].slice(0, limit),
|
|
345
441
|
toolCalls: 1,
|
|
346
|
-
bytesServed:
|
|
347
|
-
bytesRead: 0,
|
|
442
|
+
bytesServed: Buffer.byteLength(JSON.stringify(result)),
|
|
348
443
|
elapsedMs: performance.now() - start,
|
|
349
|
-
error: error instanceof Error ? error.message : String(error),
|
|
350
444
|
};
|
|
351
445
|
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
return failedObservation(start, error);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function failedObservation(start, error) {
|
|
451
|
+
return {
|
|
452
|
+
foundPaths: [],
|
|
453
|
+
toolCalls: 1,
|
|
454
|
+
bytesServed: 0,
|
|
455
|
+
elapsedMs: performance.now() - start,
|
|
456
|
+
error: error instanceof Error ? error.message : String(error),
|
|
457
|
+
};
|
|
352
458
|
}
|
|
353
459
|
function runFreshnessEval(repoRoot, seed, progress) {
|
|
354
460
|
const start = performance.now();
|
|
@@ -367,18 +473,14 @@ function runFreshnessEval(repoRoot, seed, progress) {
|
|
|
367
473
|
worktreeAdded = true;
|
|
368
474
|
progress.complete("freshness-worktree", `Created detached worktree at ${worktree}`);
|
|
369
475
|
const scope = detectScope(worktree);
|
|
370
|
-
if (!scope)
|
|
371
|
-
progress.skipped("freshness-worktree", "Temporary worktree scope detection failed");
|
|
476
|
+
if (!scope)
|
|
372
477
|
return { enabled: true, attempted: false, passed: false, skippedReason: "temporary worktree scope detection failed" };
|
|
373
|
-
}
|
|
374
478
|
progress.start("freshness-scan", "Scanning temporary worktree for a safe probe file");
|
|
375
479
|
const scanned = scanFiles(worktree);
|
|
376
480
|
const candidates = seededShuffle(scanned.filter(isFreshnessCandidate), seed);
|
|
377
481
|
const selected = candidates.map((file) => ({ file, path: safeFreshnessPath(worktree, file) })).find((item) => item.path !== null);
|
|
378
|
-
if (!selected?.path)
|
|
379
|
-
progress.skipped("freshness-scan", "No safe regular text file for freshness probe");
|
|
482
|
+
if (!selected?.path)
|
|
380
483
|
return { enabled: true, attempted: false, passed: false, skippedReason: "no safe regular text file for freshness probe" };
|
|
381
|
-
}
|
|
382
484
|
progress.complete("freshness-scan", `Selected ${selected.file.path} from ${scanned.length} files`);
|
|
383
485
|
db = openDb(join(temp, "freshness.db"));
|
|
384
486
|
progress.start("freshness-index", `Building freshness index for ${scanned.length} files`, { current: 0, total: scanned.length });
|
|
@@ -397,25 +499,12 @@ function runFreshnessEval(repoRoot, seed, progress) {
|
|
|
397
499
|
ensureFreshIndex(db, scope, { budgetMs: 30000 });
|
|
398
500
|
const deletedRemoved = !ctxSearch(db, token, { snippet: "none" }).results.some((result) => result.path === selected.file.path);
|
|
399
501
|
progress.complete("freshness-delete", `Deleted content ${deletedRemoved ? "was removed" : "remained searchable"}`);
|
|
400
|
-
return {
|
|
401
|
-
enabled: true,
|
|
402
|
-
attempted: true,
|
|
403
|
-
passed: modifiedVisible && deletedRemoved,
|
|
404
|
-
modifiedVisible,
|
|
405
|
-
deletedRemoved,
|
|
406
|
-
elapsedMs: performance.now() - start,
|
|
407
|
-
};
|
|
502
|
+
return { enabled: true, attempted: true, passed: modifiedVisible && deletedRemoved, modifiedVisible, deletedRemoved, elapsedMs: performance.now() - start };
|
|
408
503
|
}
|
|
409
504
|
catch (error) {
|
|
410
505
|
const message = error instanceof Error ? error.message : String(error);
|
|
411
506
|
progress.error("freshness", message);
|
|
412
|
-
return {
|
|
413
|
-
enabled: true,
|
|
414
|
-
attempted: true,
|
|
415
|
-
passed: false,
|
|
416
|
-
elapsedMs: performance.now() - start,
|
|
417
|
-
error: message,
|
|
418
|
-
};
|
|
507
|
+
return { enabled: true, attempted: true, passed: false, elapsedMs: performance.now() - start, error: message };
|
|
419
508
|
}
|
|
420
509
|
finally {
|
|
421
510
|
progress.start("freshness-cleanup", "Removing temporary worktree and Git metadata");
|
|
@@ -469,9 +558,8 @@ function cleanupTemporaryWorktree(repoRoot, temp, worktree, worktreeAdded) {
|
|
|
469
558
|
return;
|
|
470
559
|
}
|
|
471
560
|
const pruned = spawnSync("git", ["worktree", "prune", "--expire", "now"], { cwd: repoRoot, encoding: "utf-8" });
|
|
472
|
-
if (pruned.error || pruned.status !== 0)
|
|
561
|
+
if (pruned.error || pruned.status !== 0)
|
|
473
562
|
throw new Error(`failed to prune temporary worktree metadata: ${pruned.error?.message ?? (pruned.stderr ?? "").trim()}`);
|
|
474
|
-
}
|
|
475
563
|
const listed = spawnSync("git", ["worktree", "list", "--porcelain"], { cwd: repoRoot, encoding: "utf-8" });
|
|
476
564
|
if (listed.error || listed.status !== 0)
|
|
477
565
|
throw new Error(`failed to verify temporary worktree cleanup: ${listed.error?.message ?? "git worktree list failed"}`);
|
|
@@ -481,53 +569,104 @@ function cleanupTemporaryWorktree(repoRoot, temp, worktree, worktreeAdded) {
|
|
|
481
569
|
if (tempFailure)
|
|
482
570
|
throw new Error(`failed to remove evaluation temp directory: ${tempFailure}`);
|
|
483
571
|
}
|
|
572
|
+
function normalizeSuites(suites) {
|
|
573
|
+
const normalized = [...new Set(suites)];
|
|
574
|
+
if (normalized.length === 0)
|
|
575
|
+
throw new Error("at least one evaluation suite is required");
|
|
576
|
+
for (const suite of normalized) {
|
|
577
|
+
if (suite !== "retrieval" && suite !== "graph" && suite !== "freshness")
|
|
578
|
+
throw new Error(`unknown evaluation suite: ${suite}`);
|
|
579
|
+
}
|
|
580
|
+
return normalized;
|
|
581
|
+
}
|
|
484
582
|
function resolveScaleSpecs(scales, total) {
|
|
485
|
-
const
|
|
486
|
-
const
|
|
487
|
-
const requestedScales = scales.length > 0 ? scales : ["all"];
|
|
488
|
-
for (const scale of requestedScales) {
|
|
583
|
+
const byCount = new Map();
|
|
584
|
+
for (const scale of scales.length > 0 ? scales : ["all"]) {
|
|
489
585
|
const count = scale === "all" ? total : Math.max(1, Math.min(total, Math.floor(scale)));
|
|
490
|
-
if (
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
586
|
+
if (!byCount.has(count) || scale === "all")
|
|
587
|
+
byCount.set(count, scale);
|
|
588
|
+
}
|
|
589
|
+
return [...byCount.entries()].sort(([a], [b]) => a - b).map(([count, requested]) => ({
|
|
590
|
+
label: count === total ? "all" : String(count),
|
|
591
|
+
requested,
|
|
592
|
+
count,
|
|
593
|
+
}));
|
|
498
594
|
}
|
|
499
|
-
function
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
595
|
+
function nestedFileOrder(files, requiredPaths, seed) {
|
|
596
|
+
const required = [];
|
|
597
|
+
const remainder = [];
|
|
598
|
+
for (const file of files)
|
|
599
|
+
(requiredPaths.has(file.path) ? required : remainder).push(file);
|
|
600
|
+
required.sort((a, b) => a.path.localeCompare(b.path));
|
|
601
|
+
return [...required, ...seededShuffle(remainder, seed)];
|
|
503
602
|
}
|
|
504
|
-
function
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
output[arm] = aggregateRuns(armRuns);
|
|
510
|
-
}
|
|
511
|
-
return output;
|
|
603
|
+
function ensureRequiredPaths(files, required) {
|
|
604
|
+
const known = new Set(files.map((file) => file.path));
|
|
605
|
+
const missing = [...required].filter((path) => !known.has(path));
|
|
606
|
+
if (missing.length > 0)
|
|
607
|
+
throw new Error(`task paths are missing or not indexable: ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ` (+${missing.length - 5} more)` : ""}`);
|
|
512
608
|
}
|
|
513
609
|
function thresholdFailuresFor(scale, options) {
|
|
514
|
-
if (scale.tasks.length === 0)
|
|
515
|
-
return [];
|
|
516
|
-
const metric = scale.aggregates.full;
|
|
517
|
-
if (!metric)
|
|
518
|
-
return [`${scale.label}: full CodeLens arm produced no metrics.`];
|
|
519
610
|
const failures = [];
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
611
|
+
for (const type of ["locate", "history"]) {
|
|
612
|
+
const eligible = new Set(scale.tasks.filter((task) => task.suite === "retrieval" && task.type === type && task.contributesToThresholds).map((task) => task.id));
|
|
613
|
+
if (eligible.size === 0)
|
|
614
|
+
continue;
|
|
615
|
+
const runs = scale.runs.filter((run) => run.arm === "full" && eligible.has(run.taskId));
|
|
616
|
+
const metric = aggregateRuns(runs, options.seed + type.length * 313);
|
|
617
|
+
if (metric.recallAtK < options.thresholds.minRecallAtK)
|
|
618
|
+
failures.push(`${scale.label}/${type}: full recall@${options.resultLimit} ${metric.recallAtK.toFixed(3)} < ${options.thresholds.minRecallAtK.toFixed(3)}`);
|
|
619
|
+
if (metric.mrr < options.thresholds.minMrr)
|
|
620
|
+
failures.push(`${scale.label}/${type}: full MRR ${metric.mrr.toFixed(3)} < ${options.thresholds.minMrr.toFixed(3)}`);
|
|
621
|
+
if (metric.successRate < options.thresholds.minSuccessRate)
|
|
622
|
+
failures.push(`${scale.label}/${type}: full success rate ${metric.successRate.toFixed(3)} < ${options.thresholds.minSuccessRate.toFixed(3)}`);
|
|
623
|
+
}
|
|
624
|
+
for (const type of ["callers", "tests"]) {
|
|
625
|
+
const eligible = new Set(scale.tasks.filter((task) => task.suite === "graph" && task.type === type && task.groundTruth.independent && task.contributesToThresholds).map((task) => task.id));
|
|
626
|
+
if (eligible.size === 0)
|
|
627
|
+
continue;
|
|
628
|
+
const runs = scale.runs.filter((run) => run.arm === "graph" && eligible.has(run.taskId));
|
|
629
|
+
const metric = aggregateRuns(runs, options.seed + type.length * 419);
|
|
630
|
+
if (metric.recallAtK < options.thresholds.minRecallAtK)
|
|
631
|
+
failures.push(`${scale.label}/${type}: graph recall@${options.resultLimit} ${metric.recallAtK.toFixed(3)} < ${options.thresholds.minRecallAtK.toFixed(3)}`);
|
|
632
|
+
if (metric.precisionAtK < options.thresholds.minGraphPrecision)
|
|
633
|
+
failures.push(`${scale.label}/${type}: graph precision@${options.resultLimit} ${metric.precisionAtK.toFixed(3)} < ${options.thresholds.minGraphPrecision.toFixed(3)}`);
|
|
634
|
+
if (metric.successRate < options.thresholds.minSuccessRate)
|
|
635
|
+
failures.push(`${scale.label}/${type}: graph success rate ${metric.successRate.toFixed(3)} < ${options.thresholds.minSuccessRate.toFixed(3)}`);
|
|
636
|
+
}
|
|
526
637
|
return failures;
|
|
527
638
|
}
|
|
528
|
-
function
|
|
529
|
-
const
|
|
530
|
-
|
|
639
|
+
function methodologyNotes(taskSource, suites, hasGraph, graphIndependent) {
|
|
640
|
+
const notes = [];
|
|
641
|
+
const hasTaskSuite = suites.includes("retrieval") || suites.includes("graph");
|
|
642
|
+
if (suites.includes("retrieval")) {
|
|
643
|
+
notes.push("Retrieval arms receive identical natural-language queries and the same selected file inventory; chunk-ranked CodeLens pages are deduplicated to the same file-level top-K unit as rg, and the lexical ablation removes only graph weight before proportionally renormalizing the remaining default weights.", "The rg arm is a deterministic OR-term file-match heuristic with path-term tie-breaking; it is not a native ranked-search system.", "Retrieval arm order is deterministically counterbalanced and one warmup per arm is discarded before timing.", "Latency is end-to-end and environment-specific; rg measurements include process startup while CodeLens arms run in-process.");
|
|
644
|
+
}
|
|
645
|
+
if (hasTaskSuite) {
|
|
646
|
+
notes.push("Scale tiers are nested and reuse one fixed task set; larger tiers add distractor files rather than changing labels.", "Repeats are timing samples; unique tasks determine quality metrics and confidence intervals.", "Low-confidence tasks may be reported but only tasks marked contributesToThresholds affect pass/fail.");
|
|
647
|
+
}
|
|
648
|
+
if (taskSource.kind === "automatic-self-evaluation")
|
|
649
|
+
notes.push("Automatic tasks are a self-evaluation and are not independent benchmark ground truth; automatic graph self-consistency runs only at the label-generating smallest tier.");
|
|
650
|
+
if (hasGraph)
|
|
651
|
+
notes.push(graphIndependent
|
|
652
|
+
? "Graph labels are declared independent by the frozen task file."
|
|
653
|
+
: "Graph labels come from the evaluated CodeLens index and measure self-consistency only.");
|
|
654
|
+
if (suites.includes("freshness"))
|
|
655
|
+
notes.push("Freshness mutations run only in a detached temporary worktree and cleanup is verified.");
|
|
656
|
+
return notes;
|
|
657
|
+
}
|
|
658
|
+
function taskSetDigest(tasks) {
|
|
659
|
+
const canonical = tasks.map((task) => ({
|
|
660
|
+
id: task.id,
|
|
661
|
+
suite: task.suite,
|
|
662
|
+
type: task.type,
|
|
663
|
+
query: task.query,
|
|
664
|
+
targetPath: task.targetPath,
|
|
665
|
+
expectedPaths: task.expectedPaths,
|
|
666
|
+
confidence: task.confidence,
|
|
667
|
+
contributesToThresholds: task.contributesToThresholds,
|
|
668
|
+
}));
|
|
669
|
+
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
531
670
|
}
|
|
532
671
|
function isFreshnessCandidate(file) {
|
|
533
672
|
if (!file.language || file.size > 512 * 1024)
|
|
@@ -542,17 +681,13 @@ function freshnessComment(language, token) {
|
|
|
542
681
|
return `\n<!-- ${token} -->\n`;
|
|
543
682
|
return `\n// ${token}\n`;
|
|
544
683
|
}
|
|
545
|
-
function disabledFreshness() {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
function databaseBytes(path) {
|
|
549
|
-
try {
|
|
550
|
-
return statSync(path).size;
|
|
551
|
-
}
|
|
552
|
-
catch {
|
|
553
|
-
return 0;
|
|
554
|
-
}
|
|
684
|
+
function disabledFreshness() { return { enabled: false, attempted: false, passed: true }; }
|
|
685
|
+
function databaseBytes(path) { try {
|
|
686
|
+
return statSync(path).size;
|
|
555
687
|
}
|
|
688
|
+
catch {
|
|
689
|
+
return 0;
|
|
690
|
+
} }
|
|
556
691
|
function canonicalOutputPath(path) {
|
|
557
692
|
let current = resolve(path);
|
|
558
693
|
const missing = [];
|
|
@@ -578,11 +713,6 @@ function formatDuration(elapsedMs) {
|
|
|
578
713
|
const minutes = Math.floor(elapsedMs / 60_000);
|
|
579
714
|
return `${minutes}m ${((elapsedMs % 60_000) / 1000).toFixed(0)}s`;
|
|
580
715
|
}
|
|
581
|
-
function formatAggregateSummary(metric) {
|
|
582
|
-
if (!metric)
|
|
583
|
-
return "full arm produced no metrics";
|
|
584
|
-
return `full recall ${(metric.recallAtK * 100).toFixed(1)}%, MRR ${metric.mrr.toFixed(3)}, success ${(metric.successRate * 100).toFixed(1)}%`;
|
|
585
|
-
}
|
|
586
716
|
function gitStatus(repoRoot) {
|
|
587
717
|
const result = spawnSync("git", ["status", "--porcelain=v1", "-z"], { cwd: repoRoot, encoding: "utf-8" });
|
|
588
718
|
return result.status === 0 ? result.stdout ?? "" : "";
|