@fodx/codelens 2.4.1 → 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 +139 -0
- package/adapters/pi/codelens.extension.ts +17 -21
- package/build/src/cli.js +199 -4
- package/build/src/cli.js.map +1 -1
- package/build/src/eval/baseline.js +90 -0
- package/build/src/eval/baseline.js.map +1 -0
- package/build/src/eval/evaluator.js +720 -0
- package/build/src/eval/evaluator.js.map +1 -0
- package/build/src/eval/report.js +268 -0
- package/build/src/eval/report.js.map +1 -0
- 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 +203 -0
- package/build/src/eval/tasks.js.map +1 -0
- package/build/src/eval/types.js +2 -0
- package/build/src/eval/types.js.map +1 -0
- package/build/src/installer/agents.js +25 -18
- package/build/src/installer/agents.js.map +1 -1
- package/build/src/server.js +1 -1
- package/build/src/server.js.map +1 -1
- package/build/src/tools/search.js +1 -1
- package/build/src/tools/search.js.map +1 -1
- package/docs/agent-guide.md +12 -5
- package/docs/routing.md +27 -24
- package/package.json +1 -1
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { closeSync, constants as fsConstants, existsSync, lstatSync, mkdtempSync, openSync, rmSync, statSync, writeSync } from "node:fs";
|
|
4
|
+
import { homedir, tmpdir } from "node:os";
|
|
5
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
6
|
+
import { openDb } from "../db/db.js";
|
|
7
|
+
import { detectScope } from "../git/scope.js";
|
|
8
|
+
import { indexFile } from "../index/fts.js";
|
|
9
|
+
import { getOrCreateIndex } from "../index/manager.js";
|
|
10
|
+
import { ensureFreshIndex } from "../index/reindex.js";
|
|
11
|
+
import { scanFiles } from "../index/scanner.js";
|
|
12
|
+
import { setPendingPaths } from "../index/staleness.js";
|
|
13
|
+
import { ctxImpact } from "../tools/impact.js";
|
|
14
|
+
import { DEFAULT_WEIGHTS } from "../search/rank.js";
|
|
15
|
+
import { ctxSearch } from "../tools/search.js";
|
|
16
|
+
import { isInsideRepo, resolveReal } from "../util/paths.js";
|
|
17
|
+
import { runRgBaseline } from "./baseline.js";
|
|
18
|
+
import { aggregateRuns, artifactPaths, evaluateObservation, graphAggregates, retrievalAggregates, writeEvalArtifacts, } from "./report.js";
|
|
19
|
+
import { loadEvalTaskFile, requiredTaskPaths } from "./task-file.js";
|
|
20
|
+
import { generateEvalTasks, seededShuffle } from "./tasks.js";
|
|
21
|
+
const RETRIEVAL_ARMS = ["full", "lexical", "fts", "rg"];
|
|
22
|
+
const NON_GRAPH_WEIGHT = 1 - DEFAULT_WEIGHTS.graph;
|
|
23
|
+
const LEXICAL_WEIGHTS = {
|
|
24
|
+
fts: DEFAULT_WEIGHTS.fts / NON_GRAPH_WEIGHT,
|
|
25
|
+
symbol: DEFAULT_WEIGHTS.symbol / NON_GRAPH_WEIGHT,
|
|
26
|
+
graph: 0,
|
|
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,
|
|
31
|
+
};
|
|
32
|
+
const FTS_ONLY_WEIGHTS = {
|
|
33
|
+
fts: 1,
|
|
34
|
+
symbol: 0,
|
|
35
|
+
graph: 0,
|
|
36
|
+
code: 0,
|
|
37
|
+
pathHit: 0,
|
|
38
|
+
exact: 0,
|
|
39
|
+
recency: 0,
|
|
40
|
+
};
|
|
41
|
+
class EvalProgressTracker {
|
|
42
|
+
callback;
|
|
43
|
+
started;
|
|
44
|
+
activePhase = "initialization";
|
|
45
|
+
constructor(callback, started) {
|
|
46
|
+
this.callback = callback;
|
|
47
|
+
this.started = started;
|
|
48
|
+
}
|
|
49
|
+
start(phase, message, counts = {}) {
|
|
50
|
+
this.activePhase = phase;
|
|
51
|
+
this.emit({ phase, status: "start", message, ...counts });
|
|
52
|
+
}
|
|
53
|
+
update(phase, message, counts = {}) {
|
|
54
|
+
this.activePhase = phase;
|
|
55
|
+
this.emit({ phase, status: "progress", message, ...counts });
|
|
56
|
+
}
|
|
57
|
+
complete(phase, message, counts = {}) {
|
|
58
|
+
this.emit({ phase, status: "complete", message, ...counts });
|
|
59
|
+
}
|
|
60
|
+
skipped(phase, message, counts = {}) {
|
|
61
|
+
this.emit({ phase, status: "skipped", message, ...counts });
|
|
62
|
+
}
|
|
63
|
+
error(phase, message, counts = {}) {
|
|
64
|
+
this.activePhase = phase;
|
|
65
|
+
this.emit({ phase, status: "error", message, ...counts });
|
|
66
|
+
}
|
|
67
|
+
failure(error) {
|
|
68
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
69
|
+
this.error(this.activePhase, message);
|
|
70
|
+
return new Error(`evaluation failed during ${this.activePhase}: ${message}`, { cause: error });
|
|
71
|
+
}
|
|
72
|
+
emit(event) {
|
|
73
|
+
try {
|
|
74
|
+
this.callback?.({ ...event, elapsedMs: performance.now() - this.started });
|
|
75
|
+
}
|
|
76
|
+
catch { /* progress is best-effort */ }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function defaultEvalOptions(repoRoot) {
|
|
80
|
+
return {
|
|
81
|
+
repoRoot,
|
|
82
|
+
taskLimit: 100,
|
|
83
|
+
seed: 42,
|
|
84
|
+
repeats: 1,
|
|
85
|
+
resultLimit: 10,
|
|
86
|
+
scales: [500, 2000, "all"],
|
|
87
|
+
suites: ["retrieval", "graph", "freshness"],
|
|
88
|
+
quick: false,
|
|
89
|
+
thresholds: {
|
|
90
|
+
minRecallAtK: 0.6,
|
|
91
|
+
minMrr: 0.5,
|
|
92
|
+
minSuccessRate: 0.7,
|
|
93
|
+
minGraphPrecision: 0.5,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export function quickEvalOptions(repoRoot) {
|
|
98
|
+
return {
|
|
99
|
+
...defaultEvalOptions(repoRoot),
|
|
100
|
+
taskLimit: 20,
|
|
101
|
+
scales: [500],
|
|
102
|
+
suites: ["retrieval", "graph"],
|
|
103
|
+
quick: true,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export function runRepositoryEval(input) {
|
|
107
|
+
const started = performance.now();
|
|
108
|
+
const startedAt = new Date().toISOString();
|
|
109
|
+
const progress = new EvalProgressTracker(input.onProgress, started);
|
|
110
|
+
let temp;
|
|
111
|
+
try {
|
|
112
|
+
const suites = normalizeSuites(input.suites);
|
|
113
|
+
progress.start("repository", `Validating repository ${input.repoRoot}`);
|
|
114
|
+
const requestedRoot = resolveReal(input.repoRoot);
|
|
115
|
+
const scope = detectScope(requestedRoot);
|
|
116
|
+
if (!scope)
|
|
117
|
+
throw new Error(`not a Git repository: ${requestedRoot}`);
|
|
118
|
+
const repoRoot = scope.repoRoot;
|
|
119
|
+
const initialStatus = gitStatus(repoRoot);
|
|
120
|
+
progress.complete("repository", `Using ${repoRoot} at ${scope.headSha.slice(0, 12)} (${scope.dirtyFiles.length} dirty files)`);
|
|
121
|
+
progress.start("scan", "Scanning indexable repository files");
|
|
122
|
+
const scanned = scanFiles(repoRoot);
|
|
123
|
+
if (scanned.length === 0)
|
|
124
|
+
throw new Error(`no indexable files found: ${repoRoot}`);
|
|
125
|
+
progress.complete("scan", `Found ${scanned.length} indexable files`, { current: scanned.length, total: scanned.length });
|
|
126
|
+
progress.start("setup", "Preparing frozen tasks, nested scales, and output paths");
|
|
127
|
+
const outputDirectory = input.outputDir ? canonicalOutputPath(input.outputDir) : defaultOutputDirectory(repoRoot);
|
|
128
|
+
if (isInsideRepo(repoRoot, outputDirectory))
|
|
129
|
+
throw new Error("evaluation output must be outside the target repository");
|
|
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);
|
|
144
|
+
temp = mkdtempSync(join(tmpdir(), "codelens-eval-"));
|
|
145
|
+
const skipped = [];
|
|
146
|
+
const scales = [];
|
|
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"}`);
|
|
148
|
+
for (let scaleIndex = 0; scaleIndex < scaleSpecs.length; scaleIndex++) {
|
|
149
|
+
const spec = scaleSpecs[scaleIndex];
|
|
150
|
+
const scaleCounts = { scale: spec.label, current: scaleIndex + 1, total: scaleSpecs.length };
|
|
151
|
+
progress.start("scale", `Starting scale ${spec.label} with ${spec.count} files`, scaleCounts);
|
|
152
|
+
const selected = orderedFiles.slice(0, spec.count).sort((a, b) => a.path.localeCompare(b.path));
|
|
153
|
+
const inventory = selected.map((file) => file.path);
|
|
154
|
+
const dbPath = join(temp, `eval-${scaleIndex}.db`);
|
|
155
|
+
const db = openDb(dbPath);
|
|
156
|
+
try {
|
|
157
|
+
progress.start("index", `Indexing ${selected.length} files for scale ${spec.label}`, { scale: spec.label, current: 0, total: selected.length });
|
|
158
|
+
const indexStart = performance.now();
|
|
159
|
+
const build = buildSubsetIndex(db, scope, selected, (current, total) => {
|
|
160
|
+
progress.update("index", `Indexed ${current}/${total} files for scale ${spec.label}`, { scale: spec.label, current, total });
|
|
161
|
+
});
|
|
162
|
+
const indexMs = performance.now() - indexStart;
|
|
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 });
|
|
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 });
|
|
168
|
+
}
|
|
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 });
|
|
187
|
+
}
|
|
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.");
|
|
198
|
+
scales.push({
|
|
199
|
+
label: spec.label,
|
|
200
|
+
requestedFiles: spec.requested,
|
|
201
|
+
indexedFiles: build.indexedFiles,
|
|
202
|
+
totalChunks: build.totalChunks,
|
|
203
|
+
skippedFiles: build.skipped,
|
|
204
|
+
indexMs,
|
|
205
|
+
dbBytes: databaseBytes(dbPath),
|
|
206
|
+
tasks,
|
|
207
|
+
runs,
|
|
208
|
+
retrieval: retrievalAggregates(tasks, runs, input.seed + scaleIndex * 1009),
|
|
209
|
+
graph: graphAggregates(tasks, runs, input.seed + scaleIndex * 2003),
|
|
210
|
+
});
|
|
211
|
+
progress.complete("scale", `Finished scale ${spec.label}: ${retrievalTasks.length} retrieval and ${graphTasks.length} graph tasks`, scaleCounts);
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
db.close();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
let freshness;
|
|
218
|
+
if (suites.includes("freshness")) {
|
|
219
|
+
progress.start("freshness", "Starting detached-worktree edit/delete freshness probe");
|
|
220
|
+
freshness = runFreshnessEval(repoRoot, input.seed, progress);
|
|
221
|
+
const message = freshness.attempted
|
|
222
|
+
? `Freshness probe ${freshness.passed ? "passed" : "failed"} in ${formatDuration(freshness.elapsedMs ?? 0)}`
|
|
223
|
+
: `Freshness probe skipped: ${freshness.skippedReason ?? "not attempted"}`;
|
|
224
|
+
if (freshness.attempted && freshness.passed)
|
|
225
|
+
progress.complete("freshness", message);
|
|
226
|
+
else if (freshness.attempted)
|
|
227
|
+
progress.error("freshness", message);
|
|
228
|
+
else
|
|
229
|
+
progress.skipped("freshness", message);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
freshness = disabledFreshness();
|
|
233
|
+
progress.skipped("freshness", "Freshness suite disabled");
|
|
234
|
+
}
|
|
235
|
+
if (freshness.skippedReason)
|
|
236
|
+
skipped.push(`freshness: ${freshness.skippedReason}`);
|
|
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
|
+
}
|
|
246
|
+
const thresholdFailures = scales.flatMap((scale) => thresholdFailuresFor(scale, input));
|
|
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)
|
|
255
|
+
thresholdFailures.push("Freshness edit/delete probe failed.");
|
|
256
|
+
if (initialStatus !== gitStatus(repoRoot))
|
|
257
|
+
thresholdFailures.push("Target repository worktree changed during evaluation.");
|
|
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;
|
|
263
|
+
const result = {
|
|
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
|
+
},
|
|
274
|
+
repository: {
|
|
275
|
+
root: repoRoot,
|
|
276
|
+
branch: scope.branch,
|
|
277
|
+
headSha: scope.headSha,
|
|
278
|
+
dirtyFiles: scope.dirtyFiles.length,
|
|
279
|
+
totalFiles: scanned.length,
|
|
280
|
+
},
|
|
281
|
+
options: {
|
|
282
|
+
taskLimit: input.taskLimit,
|
|
283
|
+
seed: input.seed,
|
|
284
|
+
repeats: input.repeats,
|
|
285
|
+
resultLimit: input.resultLimit,
|
|
286
|
+
scales: input.scales,
|
|
287
|
+
suites,
|
|
288
|
+
quick: input.quick,
|
|
289
|
+
thresholds: input.thresholds,
|
|
290
|
+
},
|
|
291
|
+
startedAt,
|
|
292
|
+
completedAt: new Date().toISOString(),
|
|
293
|
+
durationMs: performance.now() - started,
|
|
294
|
+
pass: hasTaskEvidence && hasFreshnessEvidence && thresholdFailures.length === 0,
|
|
295
|
+
thresholdFailures,
|
|
296
|
+
skipped: [...new Set(skipped)],
|
|
297
|
+
scales,
|
|
298
|
+
freshness,
|
|
299
|
+
artifacts,
|
|
300
|
+
};
|
|
301
|
+
progress.start("reports", `Writing evaluation artifacts to ${artifacts.directory}`);
|
|
302
|
+
writeEvalArtifacts(result);
|
|
303
|
+
progress.complete("reports", `Wrote results.json, report.md, and tasks.json to ${artifacts.directory}`);
|
|
304
|
+
progress.start("cleanup", "Removing temporary evaluation databases");
|
|
305
|
+
rmSync(temp, { recursive: true, force: true });
|
|
306
|
+
temp = undefined;
|
|
307
|
+
progress.complete("cleanup", "Removed temporary evaluation databases");
|
|
308
|
+
progress.complete("evaluation", `Evaluation ${result.pass ? "passed" : "failed thresholds"} in ${formatDuration(performance.now() - started)}`);
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
throw progress.failure(error);
|
|
313
|
+
}
|
|
314
|
+
finally {
|
|
315
|
+
if (temp) {
|
|
316
|
+
progress.start("cleanup", "Removing temporary evaluation databases");
|
|
317
|
+
try {
|
|
318
|
+
rmSync(temp, { recursive: true, force: true });
|
|
319
|
+
progress.complete("cleanup", "Removed temporary evaluation databases");
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
progress.error("cleanup", error instanceof Error ? error.message : String(error));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function buildSubsetIndex(db, scope, files, onProgress) {
|
|
328
|
+
const indexId = getOrCreateIndex(db, scope).id;
|
|
329
|
+
const knownFiles = new Set(files.map((file) => file.path));
|
|
330
|
+
let indexedFiles = 0;
|
|
331
|
+
let totalChunks = 0;
|
|
332
|
+
let skipped = 0;
|
|
333
|
+
const progressInterval = Math.max(1, Math.ceil(files.length / 20));
|
|
334
|
+
for (let index = 0; index < files.length; index++) {
|
|
335
|
+
const file = files[index];
|
|
336
|
+
try {
|
|
337
|
+
const indexed = indexFile(db, indexId, scope.repoRoot, file, knownFiles);
|
|
338
|
+
indexedFiles++;
|
|
339
|
+
totalChunks += indexed.chunkCount;
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
skipped++;
|
|
343
|
+
}
|
|
344
|
+
const current = index + 1;
|
|
345
|
+
if (current === files.length || current % progressInterval === 0)
|
|
346
|
+
onProgress?.(current, files.length);
|
|
347
|
+
}
|
|
348
|
+
setPendingPaths(indexId, []);
|
|
349
|
+
return { indexId, indexedFiles, totalChunks, skipped };
|
|
350
|
+
}
|
|
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);
|
|
382
|
+
const runs = [];
|
|
383
|
+
for (let repeat = 0; repeat < Math.max(1, repeats); repeat++) {
|
|
384
|
+
for (const task of tasks) {
|
|
385
|
+
const observation = runGraphArm(db, task, limit);
|
|
386
|
+
runs.push(evaluateObservation(task, "graph", observation, limit, repeat, 0));
|
|
387
|
+
onProgress?.(runs.length);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return runs;
|
|
391
|
+
}
|
|
392
|
+
function runRetrievalArm(db, repoRoot, inventory, task, limit, arm) {
|
|
393
|
+
if (arm === "rg")
|
|
394
|
+
return runRgBaseline(repoRoot, task, limit, inventory);
|
|
395
|
+
const start = performance.now();
|
|
396
|
+
try {
|
|
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;
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
foundPaths,
|
|
423
|
+
toolCalls,
|
|
424
|
+
bytesServed,
|
|
425
|
+
elapsedMs: performance.now() - start,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
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);
|
|
439
|
+
return {
|
|
440
|
+
foundPaths: [...new Set(paths)].slice(0, limit),
|
|
441
|
+
toolCalls: 1,
|
|
442
|
+
bytesServed: Buffer.byteLength(JSON.stringify(result)),
|
|
443
|
+
elapsedMs: performance.now() - start,
|
|
444
|
+
};
|
|
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
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function runFreshnessEval(repoRoot, seed, progress) {
|
|
460
|
+
const start = performance.now();
|
|
461
|
+
const temp = resolveReal(mkdtempSync(join(tmpdir(), "codelens-eval-worktree-")));
|
|
462
|
+
const worktree = join(temp, "repo");
|
|
463
|
+
let db = null;
|
|
464
|
+
let worktreeAdded = false;
|
|
465
|
+
try {
|
|
466
|
+
progress.start("freshness-worktree", "Creating detached temporary worktree");
|
|
467
|
+
const added = spawnSync("git", ["worktree", "add", "--detach", "--quiet", worktree, "HEAD"], { cwd: repoRoot, encoding: "utf-8" });
|
|
468
|
+
if (added.status !== 0) {
|
|
469
|
+
const skippedReason = (added.stderr ?? "").trim() || "could not create temporary worktree";
|
|
470
|
+
progress.skipped("freshness-worktree", skippedReason);
|
|
471
|
+
return { enabled: true, attempted: false, passed: false, skippedReason };
|
|
472
|
+
}
|
|
473
|
+
worktreeAdded = true;
|
|
474
|
+
progress.complete("freshness-worktree", `Created detached worktree at ${worktree}`);
|
|
475
|
+
const scope = detectScope(worktree);
|
|
476
|
+
if (!scope)
|
|
477
|
+
return { enabled: true, attempted: false, passed: false, skippedReason: "temporary worktree scope detection failed" };
|
|
478
|
+
progress.start("freshness-scan", "Scanning temporary worktree for a safe probe file");
|
|
479
|
+
const scanned = scanFiles(worktree);
|
|
480
|
+
const candidates = seededShuffle(scanned.filter(isFreshnessCandidate), seed);
|
|
481
|
+
const selected = candidates.map((file) => ({ file, path: safeFreshnessPath(worktree, file) })).find((item) => item.path !== null);
|
|
482
|
+
if (!selected?.path)
|
|
483
|
+
return { enabled: true, attempted: false, passed: false, skippedReason: "no safe regular text file for freshness probe" };
|
|
484
|
+
progress.complete("freshness-scan", `Selected ${selected.file.path} from ${scanned.length} files`);
|
|
485
|
+
db = openDb(join(temp, "freshness.db"));
|
|
486
|
+
progress.start("freshness-index", `Building freshness index for ${scanned.length} files`, { current: 0, total: scanned.length });
|
|
487
|
+
const build = buildSubsetIndex(db, scope, scanned, (current, total) => {
|
|
488
|
+
progress.update("freshness-index", `Indexed ${current}/${total} freshness files`, { current, total });
|
|
489
|
+
});
|
|
490
|
+
progress.complete("freshness-index", `Built freshness index with ${build.indexedFiles} files and ${build.totalChunks} chunks`, { current: scanned.length, total: scanned.length });
|
|
491
|
+
const token = `codelensfreshnessprobe${createHash("sha256").update(`${repoRoot}\0${seed}`).digest("hex").slice(0, 12)}`;
|
|
492
|
+
progress.start("freshness-edit", `Appending probe token to ${selected.file.path}`);
|
|
493
|
+
appendFreshnessProbe(selected.path, freshnessComment(selected.file.language, token));
|
|
494
|
+
ensureFreshIndex(db, scope, { budgetMs: 30000 });
|
|
495
|
+
const modifiedVisible = ctxSearch(db, token, { snippet: "none" }).results.some((result) => result.path === selected.file.path);
|
|
496
|
+
progress.complete("freshness-edit", `Modified content ${modifiedVisible ? "became visible" : "was not found"}`);
|
|
497
|
+
progress.start("freshness-delete", `Deleting probe file ${selected.file.path} in the temporary worktree`);
|
|
498
|
+
rmSync(selected.path, { force: true });
|
|
499
|
+
ensureFreshIndex(db, scope, { budgetMs: 30000 });
|
|
500
|
+
const deletedRemoved = !ctxSearch(db, token, { snippet: "none" }).results.some((result) => result.path === selected.file.path);
|
|
501
|
+
progress.complete("freshness-delete", `Deleted content ${deletedRemoved ? "was removed" : "remained searchable"}`);
|
|
502
|
+
return { enabled: true, attempted: true, passed: modifiedVisible && deletedRemoved, modifiedVisible, deletedRemoved, elapsedMs: performance.now() - start };
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
506
|
+
progress.error("freshness", message);
|
|
507
|
+
return { enabled: true, attempted: true, passed: false, elapsedMs: performance.now() - start, error: message };
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
progress.start("freshness-cleanup", "Removing temporary worktree and Git metadata");
|
|
511
|
+
try {
|
|
512
|
+
db?.close();
|
|
513
|
+
}
|
|
514
|
+
catch { /* cleanup verification below catches remaining state */ }
|
|
515
|
+
cleanupTemporaryWorktree(repoRoot, temp, worktree, worktreeAdded);
|
|
516
|
+
progress.complete("freshness-cleanup", "Removed temporary worktree and Git metadata");
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function appendFreshnessProbe(path, content) {
|
|
520
|
+
const fd = openSync(path, fsConstants.O_WRONLY | fsConstants.O_APPEND | fsConstants.O_NOFOLLOW);
|
|
521
|
+
try {
|
|
522
|
+
writeSync(fd, content);
|
|
523
|
+
}
|
|
524
|
+
finally {
|
|
525
|
+
closeSync(fd);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function safeFreshnessPath(worktree, file) {
|
|
529
|
+
const path = join(worktree, file.path);
|
|
530
|
+
try {
|
|
531
|
+
const stat = lstatSync(path);
|
|
532
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
533
|
+
return null;
|
|
534
|
+
const resolved = resolveReal(path);
|
|
535
|
+
return isInsideRepo(worktree, resolved) ? resolved : null;
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function cleanupTemporaryWorktree(repoRoot, temp, worktree, worktreeAdded) {
|
|
542
|
+
let removalFailure;
|
|
543
|
+
if (worktreeAdded) {
|
|
544
|
+
const removed = spawnSync("git", ["worktree", "remove", "--force", worktree], { cwd: repoRoot, encoding: "utf-8" });
|
|
545
|
+
if (removed.error || removed.status !== 0)
|
|
546
|
+
removalFailure = removed.error?.message ?? ((removed.stderr ?? "").trim() || `exit ${removed.status}`);
|
|
547
|
+
}
|
|
548
|
+
let tempFailure;
|
|
549
|
+
try {
|
|
550
|
+
rmSync(temp, { recursive: true, force: true });
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
tempFailure = error instanceof Error ? error.message : String(error);
|
|
554
|
+
}
|
|
555
|
+
if (!worktreeAdded) {
|
|
556
|
+
if (tempFailure)
|
|
557
|
+
throw new Error(`failed to remove evaluation temp directory: ${tempFailure}`);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const pruned = spawnSync("git", ["worktree", "prune", "--expire", "now"], { cwd: repoRoot, encoding: "utf-8" });
|
|
561
|
+
if (pruned.error || pruned.status !== 0)
|
|
562
|
+
throw new Error(`failed to prune temporary worktree metadata: ${pruned.error?.message ?? (pruned.stderr ?? "").trim()}`);
|
|
563
|
+
const listed = spawnSync("git", ["worktree", "list", "--porcelain"], { cwd: repoRoot, encoding: "utf-8" });
|
|
564
|
+
if (listed.error || listed.status !== 0)
|
|
565
|
+
throw new Error(`failed to verify temporary worktree cleanup: ${listed.error?.message ?? "git worktree list failed"}`);
|
|
566
|
+
if ((listed.stdout ?? "").split(/\r?\n/).includes(`worktree ${worktree}`)) {
|
|
567
|
+
throw new Error(`temporary worktree metadata remains registered${removalFailure ? ` after removal failed (${removalFailure})` : ""}: ${worktree}`);
|
|
568
|
+
}
|
|
569
|
+
if (tempFailure)
|
|
570
|
+
throw new Error(`failed to remove evaluation temp directory: ${tempFailure}`);
|
|
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
|
+
}
|
|
582
|
+
function resolveScaleSpecs(scales, total) {
|
|
583
|
+
const byCount = new Map();
|
|
584
|
+
for (const scale of scales.length > 0 ? scales : ["all"]) {
|
|
585
|
+
const count = scale === "all" ? total : Math.max(1, Math.min(total, Math.floor(scale)));
|
|
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
|
+
}));
|
|
594
|
+
}
|
|
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)];
|
|
602
|
+
}
|
|
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)` : ""}`);
|
|
608
|
+
}
|
|
609
|
+
function thresholdFailuresFor(scale, options) {
|
|
610
|
+
const failures = [];
|
|
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
|
+
}
|
|
637
|
+
return failures;
|
|
638
|
+
}
|
|
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");
|
|
670
|
+
}
|
|
671
|
+
function isFreshnessCandidate(file) {
|
|
672
|
+
if (!file.language || file.size > 512 * 1024)
|
|
673
|
+
return false;
|
|
674
|
+
const base = basename(file.path).toLowerCase();
|
|
675
|
+
return !base.includes("lock") && !/(^|\/)(test|tests|__tests__)(\/|$)/i.test(file.path) && !/\.(test|spec)\./i.test(base);
|
|
676
|
+
}
|
|
677
|
+
function freshnessComment(language, token) {
|
|
678
|
+
if (language === "python" || language === "ruby" || language === "bash")
|
|
679
|
+
return `\n# ${token}\n`;
|
|
680
|
+
if (language === "markdown")
|
|
681
|
+
return `\n<!-- ${token} -->\n`;
|
|
682
|
+
return `\n// ${token}\n`;
|
|
683
|
+
}
|
|
684
|
+
function disabledFreshness() { return { enabled: false, attempted: false, passed: true }; }
|
|
685
|
+
function databaseBytes(path) { try {
|
|
686
|
+
return statSync(path).size;
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
return 0;
|
|
690
|
+
} }
|
|
691
|
+
function canonicalOutputPath(path) {
|
|
692
|
+
let current = resolve(path);
|
|
693
|
+
const missing = [];
|
|
694
|
+
while (!existsSync(current)) {
|
|
695
|
+
const parent = dirname(current);
|
|
696
|
+
if (parent === current)
|
|
697
|
+
break;
|
|
698
|
+
missing.unshift(basename(current));
|
|
699
|
+
current = parent;
|
|
700
|
+
}
|
|
701
|
+
return join(resolveReal(current), ...missing);
|
|
702
|
+
}
|
|
703
|
+
function defaultOutputDirectory(repoRoot) {
|
|
704
|
+
const id = createHash("sha256").update(repoRoot).digest("hex").slice(0, 12);
|
|
705
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
706
|
+
return join(homedir(), ".codelens", "evals", id, stamp);
|
|
707
|
+
}
|
|
708
|
+
function formatDuration(elapsedMs) {
|
|
709
|
+
if (elapsedMs < 1000)
|
|
710
|
+
return `${elapsedMs.toFixed(0)}ms`;
|
|
711
|
+
if (elapsedMs < 60_000)
|
|
712
|
+
return `${(elapsedMs / 1000).toFixed(1)}s`;
|
|
713
|
+
const minutes = Math.floor(elapsedMs / 60_000);
|
|
714
|
+
return `${minutes}m ${((elapsedMs % 60_000) / 1000).toFixed(0)}s`;
|
|
715
|
+
}
|
|
716
|
+
function gitStatus(repoRoot) {
|
|
717
|
+
const result = spawnSync("git", ["status", "--porcelain=v1", "-z"], { cwd: repoRoot, encoding: "utf-8" });
|
|
718
|
+
return result.status === 0 ? result.stdout ?? "" : "";
|
|
719
|
+
}
|
|
720
|
+
//# sourceMappingURL=evaluator.js.map
|