@dailephd/my-dev-kit-lab 0.4.9 → 0.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +20 -2
  3. package/benchmarks/contracts/benchmark-project-profiles.json +14 -14
  4. package/benchmarks/contracts/warm-index-benchmark-cases.json +764 -0
  5. package/dist/src/cli/help.js +19 -4
  6. package/dist/src/commands/runExperimentDescribeCommand.js +11 -2
  7. package/dist/src/commands/runExperimentListCommand.js +3 -0
  8. package/dist/src/commands/runExperimentRunCommand.js +27 -7
  9. package/dist/src/evaluation/benchmarkMetadata.js +191 -1
  10. package/dist/src/evaluation/projectComplexity.js +1 -1
  11. package/dist/src/evaluation/readEvaluationCases.js +7 -1
  12. package/dist/src/evaluation/runMyDevKitRetrieval.js +82 -53
  13. package/dist/src/evaluation/types.js +1 -1
  14. package/dist/src/experiments/defaultRegistry.js +2 -0
  15. package/dist/src/experiments/plugins/index.js +1 -0
  16. package/dist/src/experiments/plugins/warmIndexReuse/config.js +58 -0
  17. package/dist/src/experiments/plugins/warmIndexReuse/execution.js +158 -0
  18. package/dist/src/experiments/plugins/warmIndexReuse/executionArtifact.js +77 -0
  19. package/dist/src/experiments/plugins/warmIndexReuse/fakeAgentEvaluation.js +118 -0
  20. package/dist/src/experiments/plugins/warmIndexReuse/index.js +8 -0
  21. package/dist/src/experiments/plugins/warmIndexReuse/metrics.js +257 -0
  22. package/dist/src/experiments/plugins/warmIndexReuse/plugin.js +212 -0
  23. package/dist/src/experiments/plugins/warmIndexReuse/selection.js +85 -0
  24. package/dist/src/experiments/plugins/warmIndexReuse/warmIndexSession.js +43 -0
  25. package/dist/src/plots/buildWarmIndexPlotData.js +112 -0
  26. package/dist/src/plots/index.js +1 -0
  27. package/dist/src/plots/writePlotArtifacts.js +8 -1
  28. package/dist/src/report/experiments/buildPluginExperimentReport.js +18 -2
  29. package/dist/src/report/experiments/buildWarmIndexReuseReport.js +91 -0
  30. package/dist/src/report/experiments/index.js +3 -0
  31. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +3 -0
  32. package/dist/src/report/experiments/renderPluginExperimentReportText.js +73 -0
  33. package/dist/src/report/experiments/renderWarmIndexReuseHtml.js +150 -0
  34. package/dist/src/report/experiments/warmIndexReuseReportModel.js +1 -0
  35. package/docs/METRICS.md +75 -0
  36. package/package.json +1 -1
@@ -142,7 +142,7 @@ export function renderExperimentRunHelp() {
142
142
  "Required:",
143
143
  " --experiment <id> Registered experiment plugin id (see \"my-dev-kit-lab experiment list\")",
144
144
  "",
145
- "Options:",
145
+ "Common options (all plugins):",
146
146
  " --target <path> Local target project path (default: self)",
147
147
  " --out <dir> Output directory for experiment artifacts",
148
148
  " (default: beneath the workspace when installed)",
@@ -152,6 +152,15 @@ export function renderExperimentRunHelp() {
152
152
  " (default: the bundled package resource)",
153
153
  " --case <ids> Comma-separated case ids to run",
154
154
  " --benchmark-project <ids> Comma-separated benchmark project ids to run",
155
+ "",
156
+ "warm-index-reuse only:",
157
+ " --kit-command <command> my-dev-kit command used to build one index per benchmark",
158
+ " project and retrieve per task",
159
+ " (default: npx @dailephd/my-dev-kit@latest)",
160
+ " Agent evidence uses the deterministic fake agent only;",
161
+ " agent options below are not accepted by this plugin.",
162
+ "",
163
+ "context-strategy-comparison only:",
155
164
  " --agents <list> Comma-separated agent ids (default: fake-agent)",
156
165
  " --strategies <list> Comma-separated strategies: raw-full-file,my-dev-kit-guided",
157
166
  " --complexities <list> Comma-separated complexity levels: short,medium,long,multi-step",
@@ -238,19 +247,25 @@ export function renderPlotsHelp() {
238
247
  " my-dev-kit-lab plots --help",
239
248
  "",
240
249
  "Commands:",
241
- " generate Generate experiment plot artifacts from controlled-experiment artifacts.",
250
+ " generate Generate experiment plot artifacts from controlled-experiment or supported",
251
+ " plugin experiment artifacts (warm-index-reuse).",
242
252
  " Run \"my-dev-kit-lab plots generate --help\" for details."
243
253
  ].join("\n");
244
254
  }
245
255
  export function renderPlotsGenerateHelp() {
246
256
  return [
247
- "my-dev-kit-lab plots generate - generate chart/plot artifacts from controlled-experiment artifacts",
257
+ "my-dev-kit-lab plots generate - generate chart/plot artifacts from experiment artifacts",
248
258
  "",
249
259
  "Usage:",
250
260
  " my-dev-kit-lab plots generate --experiment <dir> --out <dir>",
251
261
  "",
262
+ "Supported inputs:",
263
+ " - legacy controlled-experiment output directories",
264
+ " - warm-index-reuse plugin output directories (containing its report.json)",
265
+ " Other plugin outputs are not plotted by this command.",
266
+ "",
252
267
  "Required:",
253
- " --experiment <dir> Path to a controlled-experiment output directory",
268
+ " --experiment <dir> Path to a supported experiment output directory",
254
269
  " --out <dir> Output directory for plot artifacts"
255
270
  ].join("\n");
256
271
  }
@@ -106,6 +106,9 @@ function readConfigFields(plugin, required) {
106
106
  }));
107
107
  }
108
108
  function readSupportedVariants(plugin) {
109
+ if (plugin.supportedVariants) {
110
+ return [...plugin.supportedVariants];
111
+ }
109
112
  const strategies = readArrayField(plugin.defaultConfig, "strategies");
110
113
  return strategies.filter((value) => typeof value === "string");
111
114
  }
@@ -139,9 +142,15 @@ function describeExpectedReports(plugin) {
139
142
  return `Writes supported outputs: ${plugin.metadata.supportedOutputs.join(", ")}.`;
140
143
  }
141
144
  function describeExamples(plugin) {
145
+ // Only advertise agent-matrix options for plugins that declare them.
146
+ const agentOptions = hasConfigField(plugin, "agents") ? " --agents fake-agent --complexities short" : "";
147
+ const screenshotOption = hasConfigField(plugin, "agents") ? " --no-screenshot" : "";
142
148
  return [
143
149
  `my-dev-kit-lab experiment describe --experiment ${plugin.metadata.id}`,
144
- `my-dev-kit-lab experiment run --experiment ${plugin.metadata.id} --agents fake-agent --complexities short`,
145
- `my-dev-kit-lab experiment run --experiment ${plugin.metadata.id} --target "Z:\\Users\\newuser\\Projects\\my-dev-kit-v1" --agents fake-agent --complexities short --no-screenshot`
150
+ `my-dev-kit-lab experiment run --experiment ${plugin.metadata.id}${agentOptions}`,
151
+ `my-dev-kit-lab experiment run --experiment ${plugin.metadata.id} --target "Z:\\Users\\newuser\\Projects\\my-dev-kit-v1"${agentOptions}${screenshotOption}`
146
152
  ];
147
153
  }
154
+ function hasConfigField(plugin, name) {
155
+ return (plugin.configDefinition?.fields ?? []).some((field) => field.name === name);
156
+ }
@@ -29,6 +29,9 @@ function toListedExperiment(metadata, plugin) {
29
29
  };
30
30
  }
31
31
  function readSupportedVariants(plugin) {
32
+ if (plugin.supportedVariants) {
33
+ return [...plugin.supportedVariants];
34
+ }
32
35
  const strategies = readArrayField(plugin.defaultConfig, "strategies");
33
36
  return strategies.filter((value) => typeof value === "string");
34
37
  }
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
  import { parseAgentCommandTemplate } from "../agents/index.js";
3
3
  import { parseAgentId } from "../agents/agentRegistry.js";
4
4
  import { readBenchmarkProjectProfiles, readEvaluationCases } from "../evaluation/index.js";
5
- import { contextStrategyComparisonPlugin, createDefaultExperimentPluginRegistry, resolveExperimentTarget, runExperiment } from "../experiments/index.js";
5
+ import { contextStrategyComparisonPlugin, createDefaultExperimentPluginRegistry, resolveExperimentTarget, runExperiment, warmIndexReusePlugin } from "../experiments/index.js";
6
6
  import { buildDefaultExperimentOutputRoot } from "../experiments/outputPaths.js";
7
7
  import { parsePromptComplexityLevel, parsePromptStrategy } from "../prompts/index.js";
8
8
  import { writePluginExperimentReports } from "../report/index.js";
@@ -100,6 +100,7 @@ export function parseRunExperimentArgs(argv) {
100
100
  let continueOnFailure;
101
101
  let requireAgents;
102
102
  let includeRealAgents;
103
+ let kitCommand;
103
104
  const commandTemplates = {};
104
105
  for (let index = 0; index < argv.length; index += 1) {
105
106
  const arg = argv[index];
@@ -157,6 +158,9 @@ export function parseRunExperimentArgs(argv) {
157
158
  else if (arg === "--command-template-claude") {
158
159
  commandTemplates.claude = parseAgentCommandTemplate(readRequiredValue(argv, ++index, "--command-template-claude"));
159
160
  }
161
+ else if (arg === "--kit-command") {
162
+ kitCommand = readRequiredValue(argv, ++index, "--kit-command");
163
+ }
160
164
  else if (arg === "--no-screenshot") {
161
165
  // The plugin-aware report path does not capture screenshots yet; accept this
162
166
  // flag so smoke commands can share the legacy demo option set.
@@ -168,6 +172,9 @@ export function parseRunExperimentArgs(argv) {
168
172
  if (!experimentId) {
169
173
  throw new Error("Usage: --experiment <id> [--target <path>] [--out <directory>]");
170
174
  }
175
+ if (kitCommand !== undefined && experimentId !== warmIndexReusePlugin.metadata.id) {
176
+ throw new Error(`--kit-command is only supported for --experiment ${warmIndexReusePlugin.metadata.id}.`);
177
+ }
171
178
  return {
172
179
  experimentId,
173
180
  targetPath,
@@ -185,7 +192,8 @@ export function parseRunExperimentArgs(argv) {
185
192
  continueOnFailure,
186
193
  requireAgents,
187
194
  includeRealAgents,
188
- commandTemplates: Object.keys(commandTemplates).length > 0 ? commandTemplates : undefined
195
+ commandTemplates: Object.keys(commandTemplates).length > 0 ? commandTemplates : undefined,
196
+ kitCommand
189
197
  })
190
198
  };
191
199
  }
@@ -212,13 +220,25 @@ function generateExperimentRunId(pluginId) {
212
220
  return `${pluginId}-${timestamp}`;
213
221
  }
214
222
  async function loadPluginInputs(args, toolRoot, context) {
215
- if (args.experimentId !== contextStrategyComparisonPlugin.metadata.id) {
216
- return undefined;
223
+ if (args.experimentId === contextStrategyComparisonPlugin.metadata.id) {
224
+ const validation = contextStrategyComparisonPlugin.validateConfig(args.config);
225
+ if (!validation.valid || !validation.config) {
226
+ throw new Error(`Invalid context strategy comparison config: ${validation.errors.join("; ")}`);
227
+ }
228
+ return loadCasesAndProjectProfiles(args, toolRoot, context);
217
229
  }
218
- const validation = contextStrategyComparisonPlugin.validateConfig(args.config);
219
- if (!validation.valid || !validation.config) {
220
- throw new Error(`Invalid context strategy comparison config: ${validation.errors.join("; ")}`);
230
+ if (args.experimentId === warmIndexReusePlugin.metadata.id) {
231
+ const validation = warmIndexReusePlugin.validateConfig(args.config);
232
+ if (!validation.valid || !validation.config) {
233
+ throw new Error(`Invalid warm index reuse config: ${validation.errors.join("; ")}`);
234
+ }
235
+ return loadCasesAndProjectProfiles(args, toolRoot, context);
221
236
  }
237
+ return undefined;
238
+ }
239
+ // Shared by both registered plugins: explicit paths resolve against toolRoot, defaults come from
240
+ // bundled package resources so they do not depend on the invocation cwd.
241
+ async function loadCasesAndProjectProfiles(args, toolRoot, context) {
222
242
  const projectProfilesPath = args.config.projectProfilesPath
223
243
  ? path.resolve(toolRoot, args.config.projectProfilesPath)
224
244
  : resolvePackageResource(context, DEFAULT_PROJECT_PROFILES_RESOURCE);
@@ -1,7 +1,8 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { PROJECT_COMPLEXITY_FORMULA, calculateProjectComplexityScore } from "./projectComplexity.js";
4
+ import { PROJECT_COMPLEXITY_FORMULA, calculateProjectComplexityScore, roundToTwo } from "./projectComplexity.js";
5
+ import { TASK_LOCALITIES } from "./types.js";
5
6
  export const REQUIRED_BENCHMARK_PROJECT_IDS = [
6
7
  "todo-ts",
7
8
  "todo-python",
@@ -11,6 +12,8 @@ export const REQUIRED_BENCHMARK_PROJECT_IDS = [
11
12
  "task-analytics-large-mixed"
12
13
  ];
13
14
  export const VALID_COMPLEXITY_LEVELS = new Set(["small", "medium", "large", "mixed-language"]);
15
+ export const REQUIRED_WARM_INDEX_BENCHMARK_PROJECT_IDS = ["task-workflow-medium-ts", "task-analytics-large-mixed"];
16
+ export const MIN_WARM_INDEX_TASKS_PER_PROJECT = 5;
14
17
  export async function readBenchmarkProjectProfiles(profilesPath, repoRoot = process.cwd()) {
15
18
  let parsed;
16
19
  try {
@@ -95,6 +98,193 @@ export function validateAnswerKey(answerKey, label) {
95
98
  }
96
99
  return errors;
97
100
  }
101
+ export function validateTaskLocality(value, label) {
102
+ if (typeof value === "string" && TASK_LOCALITIES.includes(value)) {
103
+ return [];
104
+ }
105
+ return [`${label}: taskLocality must be one of ${TASK_LOCALITIES.join(", ")} (received ${JSON.stringify(value)}).`];
106
+ }
107
+ export function deriveWarmIndexTaskStats(cases, projectId) {
108
+ const projectCases = cases.filter((benchmarkCase) => benchmarkCase?.benchmarkProject === projectId);
109
+ if (projectCases.length === 0) {
110
+ return { taskCount: 0, expectedRelevantFilesAverage: 0, expectedRelevantSymbolsAverage: 0 };
111
+ }
112
+ const fileCounts = projectCases.map((benchmarkCase) => (Array.isArray(benchmarkCase.expectedFiles) ? benchmarkCase.expectedFiles.length : 0));
113
+ const symbolCounts = projectCases.map((benchmarkCase) => Array.isArray(benchmarkCase.expectedSymbols) ? benchmarkCase.expectedSymbols.length : 0);
114
+ return {
115
+ taskCount: projectCases.length,
116
+ expectedRelevantFilesAverage: roundToTwo(fileCounts.reduce((total, count) => total + count, 0) / projectCases.length),
117
+ expectedRelevantSymbolsAverage: roundToTwo(symbolCounts.reduce((total, count) => total + count, 0) / projectCases.length)
118
+ };
119
+ }
120
+ export function validateWarmIndexBenchmarkCases(cases, profiles, repoRoot = process.cwd()) {
121
+ const errors = [];
122
+ const profilesById = new Map(profiles.map((profile) => [profile.projectId, profile]));
123
+ const ids = new Set();
124
+ const sourceRootsByProject = new Map();
125
+ cases.forEach((benchmarkCase, index) => {
126
+ if (!benchmarkCase || typeof benchmarkCase !== "object") {
127
+ errors.push(`warm-index case at index ${index}: must be an object.`);
128
+ return;
129
+ }
130
+ const id = typeof benchmarkCase.id === "string" && benchmarkCase.id.length > 0 ? benchmarkCase.id : undefined;
131
+ const label = id === undefined ? `warm-index case at index ${index}` : `warm-index case ${id}`;
132
+ if (id === undefined) {
133
+ errors.push(`${label}: id must be a nonempty string.`);
134
+ }
135
+ else if (ids.has(id)) {
136
+ errors.push(`${label}: duplicate case id.`);
137
+ }
138
+ else {
139
+ ids.add(id);
140
+ }
141
+ const projectId = typeof benchmarkCase.benchmarkProject === "string" && benchmarkCase.benchmarkProject.length > 0
142
+ ? benchmarkCase.benchmarkProject
143
+ : undefined;
144
+ if (projectId === undefined) {
145
+ errors.push(`${label}: benchmarkProject must be a nonempty string.`);
146
+ }
147
+ if (typeof benchmarkCase.projectProfileRef !== "string" || benchmarkCase.projectProfileRef.length === 0) {
148
+ errors.push(`${label}: missing projectProfileRef.`);
149
+ }
150
+ else if (benchmarkCase.projectProfileRef !== projectId) {
151
+ errors.push(`${label}: projectProfileRef ${benchmarkCase.projectProfileRef} must equal benchmarkProject ${projectId ?? "<missing>"}.`);
152
+ }
153
+ const profile = projectId === undefined ? undefined : profilesById.get(projectId);
154
+ if (projectId !== undefined && profile === undefined) {
155
+ errors.push(`${label}: unknown benchmark project profile ${projectId}.`);
156
+ }
157
+ if (benchmarkCase.taskLocality === undefined) {
158
+ errors.push(`${label}: missing taskLocality; expected one of ${TASK_LOCALITIES.join(", ")}.`);
159
+ }
160
+ else {
161
+ errors.push(...validateTaskLocality(benchmarkCase.taskLocality, label));
162
+ }
163
+ const targetRoot = typeof benchmarkCase.targetRoot === "string" && benchmarkCase.targetRoot.length > 0 ? benchmarkCase.targetRoot : undefined;
164
+ if (targetRoot === undefined) {
165
+ errors.push(`${label}: targetRoot must be a nonempty string.`);
166
+ }
167
+ else if (profile !== undefined && normalizeRelativeRoot(targetRoot) !== normalizeRelativeRoot(profile.rootPath ?? "")) {
168
+ errors.push(`${label}: targetRoot ${targetRoot} does not match profile ${profile.projectId} rootPath ${profile.rootPath}.`);
169
+ }
170
+ if (!Array.isArray(benchmarkCase.sourceRoots) || benchmarkCase.sourceRoots.length === 0) {
171
+ errors.push(`${label}: sourceRoots must be a nonempty array.`);
172
+ }
173
+ else {
174
+ if (profile !== undefined) {
175
+ errors.push(...validateWarmIndexSourceRoots(benchmarkCase.sourceRoots, profile, label));
176
+ }
177
+ if (projectId !== undefined) {
178
+ const reference = sourceRootsByProject.get(projectId);
179
+ if (reference === undefined) {
180
+ sourceRootsByProject.set(projectId, { caseLabel: label, sourceRoots: benchmarkCase.sourceRoots });
181
+ }
182
+ else if (!sameOrderedValues(reference.sourceRoots, benchmarkCase.sourceRoots)) {
183
+ errors.push(`${label}: sourceRoots ${JSON.stringify(benchmarkCase.sourceRoots)} must match the ordered sourceRoots ${JSON.stringify(reference.sourceRoots)} of ${reference.caseLabel} for project ${projectId}.`);
184
+ }
185
+ }
186
+ }
187
+ const expectedFiles = Array.isArray(benchmarkCase.expectedFiles) ? benchmarkCase.expectedFiles : undefined;
188
+ const expectedSymbols = Array.isArray(benchmarkCase.expectedSymbols) ? benchmarkCase.expectedSymbols : undefined;
189
+ if (expectedFiles === undefined || expectedFiles.length === 0) {
190
+ errors.push(`${label}: expectedFiles must be a nonempty array.`);
191
+ }
192
+ if (expectedSymbols === undefined || expectedSymbols.length === 0) {
193
+ errors.push(`${label}: expectedSymbols must be a nonempty array.`);
194
+ }
195
+ if (benchmarkCase.answerKey === undefined) {
196
+ errors.push(`${label}: missing answerKey.`);
197
+ }
198
+ else {
199
+ errors.push(...validateAnswerKey(benchmarkCase.answerKey, label));
200
+ const answerKey = benchmarkCase.answerKey;
201
+ if (answerKey && typeof answerKey === "object") {
202
+ if (expectedFiles !== undefined && Array.isArray(answerKey.expectedFiles) && !sameOrderedValues(expectedFiles, answerKey.expectedFiles)) {
203
+ errors.push(`${label}: expectedFiles must exactly match answerKey.expectedFiles (same values in the same order).`);
204
+ }
205
+ if (expectedSymbols !== undefined &&
206
+ Array.isArray(answerKey.expectedSymbols) &&
207
+ !sameOrderedValues(expectedSymbols, answerKey.expectedSymbols)) {
208
+ errors.push(`${label}: expectedSymbols must exactly match answerKey.expectedSymbols (same values in the same order).`);
209
+ }
210
+ }
211
+ }
212
+ if (expectedFiles !== undefined && targetRoot !== undefined) {
213
+ for (const expectedFile of expectedFiles) {
214
+ if (typeof expectedFile !== "string" || expectedFile.length === 0 || path.isAbsolute(expectedFile) || expectedFile.includes("..")) {
215
+ errors.push(`${label}: expected file must be a safe relative path: ${String(expectedFile)}.`);
216
+ }
217
+ else if (!existsSync(path.resolve(repoRoot, targetRoot, expectedFile))) {
218
+ errors.push(`${label}: expected file does not exist in ${projectId ?? targetRoot}: ${expectedFile}.`);
219
+ }
220
+ }
221
+ }
222
+ });
223
+ const representedProjectIds = [
224
+ ...new Set(cases
225
+ .map((benchmarkCase) => benchmarkCase?.benchmarkProject)
226
+ .filter((projectId) => typeof projectId === "string" && projectId.length > 0))
227
+ ].sort();
228
+ for (const projectId of representedProjectIds) {
229
+ const profile = profilesById.get(projectId);
230
+ if (profile === undefined || !profile.complexityMetrics || typeof profile.complexityMetrics !== "object") {
231
+ continue;
232
+ }
233
+ const derived = deriveWarmIndexTaskStats(cases, projectId);
234
+ for (const field of ["taskCount", "expectedRelevantFilesAverage", "expectedRelevantSymbolsAverage"]) {
235
+ if (profile.complexityMetrics[field] !== derived[field]) {
236
+ errors.push(`profile ${projectId}: complexityMetrics.${field} ${profile.complexityMetrics[field]} does not match warm-index corpus value ${derived[field]}.`);
237
+ }
238
+ }
239
+ }
240
+ return errors;
241
+ }
242
+ export function validateWarmIndexBenchmarkSuiteCoverage(cases) {
243
+ const errors = [];
244
+ for (const projectId of REQUIRED_WARM_INDEX_BENCHMARK_PROJECT_IDS) {
245
+ const projectCases = cases.filter((benchmarkCase) => benchmarkCase?.benchmarkProject === projectId);
246
+ if (projectCases.length === 0) {
247
+ errors.push(`warm-index suite: missing required benchmark project ${projectId}.`);
248
+ continue;
249
+ }
250
+ if (projectCases.length < MIN_WARM_INDEX_TASKS_PER_PROJECT) {
251
+ errors.push(`warm-index suite: project ${projectId} has ${projectCases.length} cases; at least ${MIN_WARM_INDEX_TASKS_PER_PROJECT} are required.`);
252
+ }
253
+ for (const locality of TASK_LOCALITIES) {
254
+ if (!projectCases.some((benchmarkCase) => benchmarkCase.taskLocality === locality)) {
255
+ errors.push(`warm-index suite: project ${projectId} has no ${locality} case.`);
256
+ }
257
+ }
258
+ }
259
+ return errors;
260
+ }
261
+ function validateWarmIndexSourceRoots(sourceRoots, profile, label) {
262
+ const errors = [];
263
+ const allowedRoots = [...(profile.sourceRoots ?? []), ...(profile.testRoots ?? [])];
264
+ const seen = new Set();
265
+ for (const sourceRoot of sourceRoots) {
266
+ if (seen.has(sourceRoot)) {
267
+ errors.push(`${label}: duplicate source root ${JSON.stringify(sourceRoot)}.`);
268
+ continue;
269
+ }
270
+ seen.add(sourceRoot);
271
+ if (typeof sourceRoot !== "string" || !allowedRoots.includes(sourceRoot)) {
272
+ errors.push(`${label}: unknown source root ${JSON.stringify(sourceRoot)} for profile ${profile.projectId}; allowed roots: ${allowedRoots.join(", ")}.`);
273
+ }
274
+ }
275
+ for (const allowedRoot of allowedRoots) {
276
+ if (!seen.has(allowedRoot)) {
277
+ errors.push(`${label}: missing profile source/test root ${allowedRoot} for profile ${profile.projectId}.`);
278
+ }
279
+ }
280
+ return errors;
281
+ }
282
+ function normalizeRelativeRoot(value) {
283
+ return path.posix.normalize(value.replace(/\\/g, "/")).replace(/\/+$/, "");
284
+ }
285
+ function sameOrderedValues(left, right) {
286
+ return left.length === right.length && left.every((value, index) => value === right[index]);
287
+ }
98
288
  export function validateBenchmarkProjectProfiles(profiles, repoRoot = process.cwd()) {
99
289
  const errors = [];
100
290
  const ids = new Set(profiles.map((profile) => profile.projectId));
@@ -73,7 +73,7 @@ export function computeProjectComplexityMetrics(projectRoot, fileTree, taskStats
73
73
  function normalize(value, cap) {
74
74
  return Math.min(value / cap, 1);
75
75
  }
76
- function roundToTwo(value) {
76
+ export function roundToTwo(value) {
77
77
  return Math.round(value * 100) / 100;
78
78
  }
79
79
  function sum(values) {
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { resolveWithinRoot } from "../core/pathSafety.js";
4
- import { validateAnswerKey } from "./benchmarkMetadata.js";
4
+ import { validateAnswerKey, validateTaskLocality } from "./benchmarkMetadata.js";
5
5
  export async function readEvaluationCases(casesPath, repoRoot = process.cwd(), options = {}) {
6
6
  let parsed;
7
7
  try {
@@ -36,6 +36,12 @@ export async function readEvaluationCases(casesPath, repoRoot = process.cwd(), o
36
36
  throw new Error(`Duplicate evaluation case id: ${input.id}`);
37
37
  }
38
38
  ids.add(input.id);
39
+ if (candidate.taskLocality !== undefined) {
40
+ const localityErrors = validateTaskLocality(candidate.taskLocality, `evaluation case ${input.id}`);
41
+ if (localityErrors.length > 0) {
42
+ throw new Error(localityErrors.join("\n"));
43
+ }
44
+ }
39
45
  if (candidate.answerKey !== undefined) {
40
46
  const answerKeyErrors = validateAnswerKey(candidate.answerKey, `evaluation case ${input.id}`);
41
47
  if (answerKeyErrors.length > 0) {
@@ -39,52 +39,71 @@ function pickCandidateFields(candidate) {
39
39
  symbol: readString("symbol", "name", "label")
40
40
  };
41
41
  }
42
- export async function runMyDevKitRetrieval(options) {
43
- const started = Date.now();
42
+ function skippedRetrieval(caseId, warnings, commands, durationMs) {
43
+ return {
44
+ caseId,
45
+ skipped: true,
46
+ warnings,
47
+ totalChars: 0,
48
+ totalEstimatedTokens: 0,
49
+ tokenCountMethod,
50
+ contextText: "",
51
+ filesRead: [],
52
+ commands,
53
+ durationMs
54
+ };
55
+ }
56
+ /**
57
+ * Runs exactly one my-dev-kit `index` command for the target and writes the index only to
58
+ * `indexDir`. Performs no search/lookup/slice/source work. With `requireKit` a failed index
59
+ * throws; otherwise the failure is returned as `ok: false` with a warning.
60
+ */
61
+ export async function buildMyDevKitIndex(options) {
44
62
  const warnings = [];
45
- const commandsDir = path.join(options.outputDir, "commands", options.evaluationCase.id);
46
- const indexesDir = path.join(options.outputDir, "indexes", options.evaluationCase.id);
47
- const commands = [];
48
- const indexCommand = await runMeasuredCommand({
63
+ const command = await runMeasuredCommand({
49
64
  commandId: "index",
50
65
  commandString: options.kitCommand,
51
66
  cwd: process.cwd(),
52
- outDir: commandsDir,
67
+ outDir: options.commandsDir,
53
68
  extraArgs: [
54
69
  "index",
55
70
  "--root",
56
- options.evaluationCase.absoluteTargetRoot,
57
- ...options.evaluationCase.sourceRoots.flatMap((sourceRoot) => ["--src", sourceRoot]),
71
+ options.target.absoluteTargetRoot,
72
+ ...options.target.sourceRoots.flatMap((sourceRoot) => ["--src", sourceRoot]),
58
73
  "--out",
59
- indexesDir,
74
+ options.indexDir,
60
75
  "--json"
61
76
  ]
62
77
  });
63
- commands.push(indexCommand);
64
- if (!indexCommand.ok) {
78
+ if (!command.ok) {
65
79
  if (options.requireKit) {
66
- throw new Error(indexCommand.error || `my-dev-kit index failed with exit code ${indexCommand.exitCode}`);
80
+ throw new Error(command.error || `my-dev-kit index failed with exit code ${command.exitCode}`);
67
81
  }
68
82
  warnings.push("my-dev-kit index command was unavailable or failed.");
69
- return {
70
- caseId: options.evaluationCase.id,
71
- skipped: true,
72
- warnings,
73
- totalChars: 0,
74
- totalEstimatedTokens: 0,
75
- tokenCountMethod,
76
- contextText: "",
77
- filesRead: [],
78
- commands,
79
- durationMs: Date.now() - started
80
- };
81
83
  }
84
+ return {
85
+ ok: command.ok,
86
+ indexDir: options.indexDir,
87
+ durationMs: command.durationMs,
88
+ warnings,
89
+ command
90
+ };
91
+ }
92
+ /**
93
+ * Runs search -> lookup -> slice -> source against an already prepared index. Never invokes
94
+ * `index`; `durationMs` covers retrieval only.
95
+ */
96
+ export async function runMyDevKitRetrievalFromIndex(options) {
97
+ const started = Date.now();
98
+ const warnings = [];
99
+ const commands = [];
100
+ const { commandsDir, indexDir } = options;
82
101
  const searchCommand = await runMeasuredCommand({
83
102
  commandId: "search",
84
103
  commandString: options.kitCommand,
85
104
  cwd: process.cwd(),
86
105
  outDir: commandsDir,
87
- extraArgs: ["search", "--index", indexesDir, "--query", options.evaluationCase.query, "--json"]
106
+ extraArgs: ["search", "--index", indexDir, "--query", options.evaluationCase.query, "--json"]
88
107
  });
89
108
  commands.push(searchCommand);
90
109
  if (!searchCommand.ok) {
@@ -92,36 +111,14 @@ export async function runMyDevKitRetrieval(options) {
92
111
  throw new Error(searchCommand.error || `my-dev-kit search failed with exit code ${searchCommand.exitCode}`);
93
112
  }
94
113
  warnings.push("my-dev-kit search command failed.");
95
- return {
96
- caseId: options.evaluationCase.id,
97
- skipped: true,
98
- warnings,
99
- totalChars: 0,
100
- totalEstimatedTokens: 0,
101
- tokenCountMethod,
102
- contextText: "",
103
- filesRead: [],
104
- commands,
105
- durationMs: Date.now() - started
106
- };
114
+ return skippedRetrieval(options.evaluationCase.id, warnings, commands, Date.now() - started);
107
115
  }
108
116
  const searchPayload = parseJsonIfPossible(searchCommand.stdout);
109
117
  const candidates = readSearchResults(searchPayload);
110
118
  const selected = candidates[0];
111
119
  if (!selected) {
112
120
  warnings.push("No my-dev-kit search candidate was found.");
113
- return {
114
- caseId: options.evaluationCase.id,
115
- skipped: true,
116
- warnings,
117
- totalChars: 0,
118
- totalEstimatedTokens: 0,
119
- tokenCountMethod,
120
- contextText: "",
121
- filesRead: [],
122
- commands,
123
- durationMs: Date.now() - started
124
- };
121
+ return skippedRetrieval(options.evaluationCase.id, warnings, commands, Date.now() - started);
125
122
  }
126
123
  const candidate = pickCandidateFields(selected);
127
124
  const selectedNodeId = candidate.nodeId;
@@ -136,7 +133,7 @@ export async function runMyDevKitRetrieval(options) {
136
133
  commandString: options.kitCommand,
137
134
  cwd: process.cwd(),
138
135
  outDir: commandsDir,
139
- extraArgs: ["lookup", "--index", indexesDir, "--node", selectedNodeId, "--json"]
136
+ extraArgs: ["lookup", "--index", indexDir, "--node", selectedNodeId, "--json"]
140
137
  });
141
138
  commands.push(lookupCommand);
142
139
  if (lookupCommand.ok) {
@@ -150,7 +147,7 @@ export async function runMyDevKitRetrieval(options) {
150
147
  commandString: options.kitCommand,
151
148
  cwd: process.cwd(),
152
149
  outDir: commandsDir,
153
- extraArgs: ["slice", "--index", indexesDir, "--node", selectedNodeId, "--json"]
150
+ extraArgs: ["slice", "--index", indexDir, "--node", selectedNodeId, "--json"]
154
151
  });
155
152
  commands.push(sliceCommand);
156
153
  if (sliceCommand.ok) {
@@ -164,7 +161,7 @@ export async function runMyDevKitRetrieval(options) {
164
161
  commandString: options.kitCommand,
165
162
  cwd: process.cwd(),
166
163
  outDir: commandsDir,
167
- extraArgs: ["source", "--index", indexesDir, "--node", selectedNodeId, "--max-lines", "160", "--format", "numbered"]
164
+ extraArgs: ["source", "--index", indexDir, "--node", selectedNodeId, "--max-lines", "160", "--format", "numbered"]
168
165
  });
169
166
  commands.push(sourceCommand);
170
167
  if (sourceCommand.ok) {
@@ -195,3 +192,35 @@ export async function runMyDevKitRetrieval(options) {
195
192
  durationMs: Date.now() - started
196
193
  };
197
194
  }
195
+ /**
196
+ * Legacy per-case lifecycle: builds a case-specific index and retrieves from it. `commands`
197
+ * starts with the index command and `durationMs` covers the whole lifecycle including indexing.
198
+ */
199
+ export async function runMyDevKitRetrieval(options) {
200
+ const started = Date.now();
201
+ const commandsDir = path.join(options.outputDir, "commands", options.evaluationCase.id);
202
+ const indexDir = path.join(options.outputDir, "indexes", options.evaluationCase.id);
203
+ const index = await buildMyDevKitIndex({
204
+ target: options.evaluationCase,
205
+ kitCommand: options.kitCommand,
206
+ indexDir,
207
+ commandsDir,
208
+ requireKit: options.requireKit
209
+ });
210
+ if (!index.ok) {
211
+ return skippedRetrieval(options.evaluationCase.id, [...index.warnings], [index.command], Date.now() - started);
212
+ }
213
+ const retrieval = await runMyDevKitRetrievalFromIndex({
214
+ evaluationCase: options.evaluationCase,
215
+ kitCommand: options.kitCommand,
216
+ indexDir,
217
+ commandsDir,
218
+ requireKit: options.requireKit
219
+ });
220
+ return {
221
+ ...retrieval,
222
+ warnings: [...index.warnings, ...retrieval.warnings],
223
+ commands: [index.command, ...retrieval.commands],
224
+ durationMs: Date.now() - started
225
+ };
226
+ }
@@ -1 +1 @@
1
- export {};
1
+ export const TASK_LOCALITIES = ["localized", "cross-module", "broad-change"];
@@ -1,7 +1,9 @@
1
1
  import { ExperimentPluginRegistry } from "./registry.js";
2
2
  import { contextStrategyComparisonPlugin } from "./plugins/contextStrategyComparison/index.js";
3
+ import { warmIndexReusePlugin } from "./plugins/warmIndexReuse/index.js";
3
4
  export function createDefaultExperimentPluginRegistry() {
4
5
  const registry = new ExperimentPluginRegistry();
5
6
  registry.register(contextStrategyComparisonPlugin);
7
+ registry.register(warmIndexReusePlugin);
6
8
  return registry;
7
9
  }
@@ -1 +1,2 @@
1
1
  export * from "./contextStrategyComparison/index.js";
2
+ export * from "./warmIndexReuse/index.js";