@dailephd/my-dev-kit-lab 0.4.9 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +20 -2
  3. package/dist/src/cli/help.js +19 -4
  4. package/dist/src/commands/runExperimentDescribeCommand.js +11 -2
  5. package/dist/src/commands/runExperimentListCommand.js +3 -0
  6. package/dist/src/commands/runExperimentRunCommand.js +27 -7
  7. package/dist/src/evaluation/runMyDevKitRetrieval.js +82 -53
  8. package/dist/src/experiments/defaultRegistry.js +2 -0
  9. package/dist/src/experiments/plugins/index.js +1 -0
  10. package/dist/src/experiments/plugins/warmIndexReuse/config.js +58 -0
  11. package/dist/src/experiments/plugins/warmIndexReuse/execution.js +158 -0
  12. package/dist/src/experiments/plugins/warmIndexReuse/executionArtifact.js +77 -0
  13. package/dist/src/experiments/plugins/warmIndexReuse/fakeAgentEvaluation.js +118 -0
  14. package/dist/src/experiments/plugins/warmIndexReuse/index.js +8 -0
  15. package/dist/src/experiments/plugins/warmIndexReuse/metrics.js +257 -0
  16. package/dist/src/experiments/plugins/warmIndexReuse/plugin.js +212 -0
  17. package/dist/src/experiments/plugins/warmIndexReuse/selection.js +85 -0
  18. package/dist/src/experiments/plugins/warmIndexReuse/warmIndexSession.js +43 -0
  19. package/dist/src/plots/buildWarmIndexPlotData.js +112 -0
  20. package/dist/src/plots/index.js +1 -0
  21. package/dist/src/plots/writePlotArtifacts.js +8 -1
  22. package/dist/src/report/experiments/buildPluginExperimentReport.js +18 -2
  23. package/dist/src/report/experiments/buildWarmIndexReuseReport.js +91 -0
  24. package/dist/src/report/experiments/index.js +3 -0
  25. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +3 -0
  26. package/dist/src/report/experiments/renderPluginExperimentReportText.js +73 -0
  27. package/dist/src/report/experiments/renderWarmIndexReuseHtml.js +150 -0
  28. package/dist/src/report/experiments/warmIndexReuseReportModel.js +1 -0
  29. package/docs/METRICS.md +64 -0
  30. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ All notable changes to my-dev-kit-lab are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.0] - 2026-09-21
8
+
9
+ Warm-index reuse experiment support.
10
+
11
+ - Added the experimental `warm-index-reuse` experiment plugin, registered alongside `context-strategy-comparison` and reachable through `experiment list`, `experiment describe`, and `experiment run`.
12
+ - Built exactly one my-dev-kit index per benchmark-project group and reused it across every task in that group, with a matched `raw-full-file` baseline for each task; cases that disagree on target or source roots fail structurally instead of being indexed.
13
+ - Separated index construction from retrieval: one measured `index` command per project, and search/lookup/slice/source against the prepared index for each task, without re-indexing.
14
+ - Added deterministic fake-agent evaluation of each task side with context evidence, reusing the existing prompt generator, answer parser, run classifier, and correctness scorer.
15
+ - Reported index build duration, raw context-construction and warm retrieval durations, raw and retrieved context size, estimated context tokens, amortized index build duration (build duration / task ordinal), cumulative component durations, cumulative estimated context tokens, fake-agent correctness, fake-agent total tokens, and cumulative fake-agent total tokens, each with explicit `available`/`unavailable`/`not-applicable` availability and strict-prefix cumulative rules.
16
+ - Added a bounded warm-index section to the plugin `report.json`, `report.txt`, and `report.html`, explaining cold-start versus warm-reuse cost, with a neutral interpretation and explicit limitations. The bounded `warm-index-execution.json` records execution evidence without context text.
17
+ - Added four warm-index plots through the existing `plots generate` command: amortized index cost, raw versus retrieved context size, fake-agent correctness, and cumulative fake-agent token usage.
18
+ - Added the warm-index-only `experiment run --kit-command <command>` option (default `npx @dailephd/my-dev-kit@latest`), grouped `experiment run --help` options by plugin, and corrected `plots generate` help to describe supported inputs.
19
+ - Extended the exact packed-package acceptance gate with installed warm-index execution, reports, and plots while preserving installed-package and target immutability.
20
+ - Limitations: agent evidence comes from the deterministic fake agent only; fake-agent correctness and token totals are simulated harness evidence, not real-model results or provider billing telemetry; estimated context tokens remain a separate context-size estimate; cumulative durations are component sums, not wall-clock latency; no token-savings percentage, break-even task, winner, or ranking is calculated; the bundled cases provide one task per benchmark project; real-agent warm-index campaigns, warm-index screenshots, and gallery integration are not included.
21
+
7
22
  ## [0.4.9] - 2026-09-20
8
23
 
9
24
  Semantic native-select option selection for declarative browser tutorials.
package/README.md CHANGED
@@ -6,13 +6,16 @@ my-dev-kit provides local repository indexing and graph-guided retrieval. my-dev
6
6
 
7
7
  Whole-ecosystem workflows are centralized in [my-dev-kit/docs/ECOSYSTEM_DEVELOPMENT_WORKFLOWS.md](https://github.com/dailephd/my-dev-kit/blob/main/docs/ECOSYSTEM_DEVELOPMENT_WORKFLOWS.md). Its [command-surface compatibility map](https://github.com/dailephd/my-dev-kit/blob/main/docs/ECOSYSTEM_DEVELOPMENT_WORKFLOWS.md#915-command-surface-compatibility-map) documents which Lab outputs can safely seed my-dev-kit investigation, Observer reference work, or Orchestrator evidence, and which look-alike artifacts are not direct inputs.
8
8
 
9
- The latest release is v0.4.9 (semantic native-select option selection for declarative browser tutorials, adding the value-only `select-option` action backed by Playwright `Locator.selectOption({ value })`). The previous release, v0.4.8, added locator-anchored pointer gestures (`pointer-click` and `pointer-drag`) using normalized locator-relative fraction positions while existing `drag` remains element-to-element. The v0.4.5 context-integrity evaluation remains intentionally frozen against the published `@dailephd/my-dev-kit@1.10.4` and `@dailephd/my-dev-kit-orchestrator@1.2.3` contracts; those versions are historical validation baselines, not a statement that they are the ecosystem's current releases. See [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md).
9
+ The latest release is v0.5.0 (warm-index reuse experiment support). The previous release, v0.4.9, added semantic native-select option selection for declarative browser tutorials through the value-only `select-option` action backed by Playwright `Locator.selectOption({ value })`. The v0.4.5 context-integrity evaluation remains intentionally frozen against the published `@dailephd/my-dev-kit@1.10.4` and `@dailephd/my-dev-kit-orchestrator@1.2.3` contracts; those versions are historical validation baselines, not a statement that they are the ecosystem's current releases. See [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md).
10
+
11
+ The current release includes v0.5.0 warm-index reuse. The package and installed CLI are version 0.5.0; v0.4.9 remains the previous release. See [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md) for the validated release state.
10
12
 
11
13
  v0.4.6 adds a supported `my-dev-kit-lab` installed CLI router (`--help`, `--version`, `security validate`, `audit`, the `experiment` family, `report render`, `plots generate`, `gallery build`, `demo final`, and the historical direct final-demo invocation form), a writable lab workspace model kept separate from the installed package and the inspected target, and a permanent packed-tarball installation/execution acceptance gate (`npm run verify:packed-package`). See [docs/ROADMAP.md](docs/ROADMAP.md) and [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md) for status detail and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the runtime path model. The "Installed CLI" section below documents the shipped command surface; the source-checkout `npm run` workflow in Quickstart remains available for contributors.
12
14
 
13
15
  ## Current capabilities
14
16
 
15
17
  - **Run context-strategy experiments:** compare `raw-full-file` with `my-dev-kit-guided` using deterministic fixtures or locally configured Codex and Claude CLIs.
18
+ - **Measure warm-index reuse (v0.5.0):** the `warm-index-reuse` experiment plugin builds one my-dev-kit index per benchmark project, reuses it across that project's tasks, and compares every task with a matched `raw-full-file` baseline. Reports separate one-time index-build cost from per-task retrieval cost, show amortized index cost and cumulative measurements, and include deterministic fake-agent correctness and token evidence; `plots generate` renders four warm-index SVG charts. Estimated context tokens (a character-based context-size estimate) and fake-agent total tokens (simulated harness telemetry) are reported separately, and neither is provider billing telemetry. See [docs/WORKFLOWS.md](docs/WORKFLOWS.md#warm-index-reuse-experiment) and [docs/METRICS.md](docs/METRICS.md#warm-index-reuse-metrics).
16
19
  - **Audit repository health:** run conservative code-rot detectors for TypeScript/JavaScript, Python, Java, and Kotlin, or adapt security findings into the common audit report.
17
20
  - **Validate CLI/package security:** inspect dependencies, package contents, path and subprocess boundaries, malformed inputs, optional static scanners, and bounded fuzz targets.
18
21
  - **Validate Android projects:** run nineteen static checks by default, with Gradle operations, external tools, and network access available only through explicit opt-in flags.
@@ -153,6 +156,19 @@ npm run experiment:run -- `
153
156
  --no-screenshot
154
157
  ```
155
158
 
159
+ Run the warm-index reuse plugin deterministically with the fake my-dev-kit fixture (omit `--kit-command` to use the default `npx @dailephd/my-dev-kit@latest`):
160
+
161
+ ```bash
162
+ npm run experiment:run -- \
163
+ --experiment warm-index-reuse \
164
+ --cases tests/fixtures/warm-index-reuse/multi-task-cases.json \
165
+ --kit-command "node tests/fixtures/fake-my-dev-kit-cli.js" \
166
+ --out lab-output/warm-index-reuse
167
+ npm run generate-experiment-plots -- --experiment lab-output/warm-index-reuse --out lab-output/warm-index-plots
168
+ ```
169
+
170
+ The bundled `examples/token-savings-cases.json` currently has one task per benchmark project, so multi-task reuse needs a cases file with several tasks per project, such as the test fixture above; an expanded bundled warm-index benchmark suite is planned.
171
+
156
172
  When `--target` is omitted, the experiment runs in self mode against my-dev-kit-lab. When `--target <path>` is provided, the lab remains the tool root and the target project is inspected separately. Generated experiment outputs stay under lab-controlled output directories by default, not inside the target project.
157
173
 
158
174
  ---
@@ -170,6 +186,7 @@ When `--target` is omitted, the experiment runs in self mode against my-dev-kit-
170
186
  | Plugin experiment report JSON | `lab-output/experiments/<plugin>/<target>/<run>/report.json` |
171
187
  | Plugin experiment report HTML | `lab-output/experiments/<plugin>/<target>/<run>/report.html` |
172
188
  | Plugin experiment report text | `lab-output/experiments/<plugin>/<target>/<run>/report.txt` |
189
+ | Warm-index execution evidence | `<experiment out>/warm-index-execution.json` (plus `indexes/`, `commands/`, and `agents/` beneath the same output root) |
173
190
  | Plot data | `lab-output/<plots>/plot-data.json` |
174
191
  | SVG charts | `lab-output/<plots>/charts/*.svg` |
175
192
  | Gallery manifest | `lab-output/<gallery>/gallery-manifest.json` |
@@ -203,7 +220,8 @@ See [docs/METRICS.md](docs/METRICS.md) for full metric definitions.
203
220
  - Claude does not expose token totals; token savings comparisons are unavailable for Claude runs
204
221
  - Codex may expose token totals but can produce timeouts or invalid-output runs
205
222
  - Small projects may make raw-full-file cheaper than my-dev-kit-guided; larger localized tasks are where my-dev-kit is expected to become more useful
206
- - The generic experiment-plugin framework currently ships one plugin, `context-strategy-comparison`; future plugins such as warm-index reuse, incremental-change, and context-window scaling are not implemented yet
223
+ - The generic experiment-plugin framework has two plugins: `context-strategy-comparison` and the current-release v0.5.0 `warm-index-reuse`. Later plugins such as incremental-change and context-window scaling are future roadmap work
224
+ - `warm-index-reuse` uses the deterministic fake agent only; selectable Codex/Claude warm-index campaigns remain planned for v0.5.2, the expanded benchmark suite remains planned for v0.5.1, and warm-index screenshots/gallery integration are not implemented. Its component-duration sums are not wall-clock latency, and it calculates no token-savings percentage, break-even task, winner, or ranking
207
225
  - The current release does not guarantee token savings; it produces auditable evidence for specific cases, targets, agents, and strategies
208
226
  - Provider telemetry dashboards, semantic LLM judging, and cloud API billing integration are not yet implemented
209
227
  - The six new stage-context strategies have no CLI flags yet, are configured programmatically, and do not yet include plots, screenshots, or gallery integration
@@ -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);
@@ -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,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";
@@ -0,0 +1,58 @@
1
+ import { invalidExperimentConfig, isPlainObject, mergeConfig, validExperimentConfig } from "../../config.js";
2
+ export const defaultWarmIndexReuseConfig = {
3
+ casesPath: "examples/token-savings-cases.json",
4
+ projectProfilesPath: "benchmarks/contracts/benchmark-project-profiles.json",
5
+ outDir: "lab-output/warm-index-reuse",
6
+ kitCommand: "npx @dailephd/my-dev-kit@latest",
7
+ };
8
+ const SUPPORTED_FIELDS = [
9
+ "casesPath",
10
+ "projectProfilesPath",
11
+ "outDir",
12
+ "kitCommand",
13
+ "caseIds",
14
+ "benchmarkProjects",
15
+ ];
16
+ export const warmIndexReuseConfigDefinition = {
17
+ fields: [
18
+ { name: "casesPath", type: "string", required: true, description: "Evaluation cases file." },
19
+ { name: "outDir", type: "string", required: true, description: "Experiment output root." },
20
+ { name: "projectProfilesPath", type: "string", description: "Benchmark project profiles file." },
21
+ { name: "kitCommand", type: "string", description: "my-dev-kit command used for index and retrieval." },
22
+ { name: "caseIds", type: "array", description: "Evaluation case IDs to include." },
23
+ { name: "benchmarkProjects", type: "array", description: "Benchmark project IDs to include." },
24
+ ],
25
+ };
26
+ /**
27
+ * Closed validation: fields outside the warm-index contract (for example agent-matrix settings)
28
+ * are rejected instead of being silently ignored.
29
+ */
30
+ export function validateWarmIndexReuseConfig(config) {
31
+ if (config !== undefined && !isPlainObject(config)) {
32
+ return invalidExperimentConfig(["warm index reuse config must be an object."]);
33
+ }
34
+ const errors = [];
35
+ const unsupported = Object.keys(config ?? {}).filter((key) => !SUPPORTED_FIELDS.includes(key));
36
+ if (unsupported.length > 0) {
37
+ errors.push(`Unsupported warm-index-reuse config field(s): ${unsupported.sort().join(", ")}.`);
38
+ }
39
+ const normalized = mergeConfig(defaultWarmIndexReuseConfig, config);
40
+ for (const field of ["casesPath", "projectProfilesPath", "outDir", "kitCommand"]) {
41
+ const value = normalized[field];
42
+ if (typeof value !== "string" || !value.trim()) {
43
+ errors.push(`${field} must be a non-empty string.`);
44
+ }
45
+ }
46
+ for (const field of ["caseIds", "benchmarkProjects"]) {
47
+ const value = normalized[field];
48
+ if (value === undefined)
49
+ continue;
50
+ if (!Array.isArray(value) || value.length === 0 || !value.every((item) => typeof item === "string" && item.trim())) {
51
+ errors.push(`${field} must be a non-empty array of non-empty strings when provided.`);
52
+ }
53
+ }
54
+ if (errors.length > 0) {
55
+ return invalidExperimentConfig(errors);
56
+ }
57
+ return validExperimentConfig(normalized);
58
+ }