@dailephd/my-dev-kit-lab 0.4.8 → 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 (36) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +24 -3
  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/dist/src/tutorial/scenarioValidation.js +11 -0
  30. package/dist/src/tutorial/tutorialActions.js +14 -0
  31. package/dist/src/tutorial/tutorialSession.js +3 -0
  32. package/dist/src/tutorial/types.js +1 -0
  33. package/docs/METRICS.md +64 -0
  34. package/examples/tutorial-browser/index.html +5 -0
  35. package/examples/tutorial-browser/scenario.json +1 -0
  36. package/package.json +1 -1
@@ -256,6 +256,77 @@ function renderV043Section(lines, section) {
256
256
  renderStrategy(lines, section.strategies[index], index);
257
257
  }
258
258
  }
259
+ function formatWarmMetric(metric) {
260
+ const method = metric.tokenCountMethod ? `, method ${metric.tokenCountMethod}` : "";
261
+ if (metric.availability === "available") {
262
+ return `${metric.value} ${metric.unit} (${metric.source}${method})`;
263
+ }
264
+ return `${metric.availability} (${metric.reason ?? ""})`;
265
+ }
266
+ function formatWarmAgent(agent) {
267
+ if (agent === null)
268
+ return "not run (no context evidence for this side)";
269
+ const passed = agent.passed === null ? "unavailable" : String(agent.passed);
270
+ const issues = [...agent.errors, ...agent.warnings].slice(0, 3).join("; ");
271
+ return `status ${agent.status}, passed ${passed}, token source ${agent.tokenUsageSource}, reliability ${agent.tokenUsageReliability}${issues ? `, notes: ${issues}` : ""}`;
272
+ }
273
+ function renderWarmIndexReuseSection(lines, section) {
274
+ if (section === null) {
275
+ lines.push("Not applicable to this plugin.");
276
+ return;
277
+ }
278
+ lines.push(fieldLine("Project Count", section.summary.projectCount));
279
+ lines.push(fieldLine("Task Count", section.summary.taskCount));
280
+ lines.push(fieldLine("Prepared Session Project Count", section.summary.preparedSessionProjectCount));
281
+ lines.push(fieldLine("Incomplete Or Failed Project Count", section.summary.incompleteProjectCount));
282
+ lines.push(fieldLine("Fake-Agent Correctness Available (task sides)", `${section.summary.agentCorrectnessAvailableCount} of ${section.summary.agentSideCount}`));
283
+ lines.push(fieldLine("Fake-Agent Token Totals Available (task sides)", `${section.summary.agentTotalTokensAvailableCount} of ${section.summary.agentSideCount}`));
284
+ lines.push("Cold Start And Warm Reuse:");
285
+ pushDashList(lines, section.costModel);
286
+ lines.push("Limitations:");
287
+ pushDashList(lines, section.limitations);
288
+ if (section.projects.length === 0) {
289
+ lines.push("No warm-index project evidence was recorded.");
290
+ return;
291
+ }
292
+ section.projects.forEach((project, index) => {
293
+ pushSection(lines, `Warm Index Project ${index + 1}: ${sanitizeScalar(project.benchmarkProject)}`);
294
+ lines.push(fieldLine("Session Key", project.sessionKey));
295
+ lines.push(fieldLine("Status", project.status));
296
+ lines.push(fieldLine("Session Prepared", project.sessionPrepared));
297
+ lines.push(fieldLine("Index Build Duration", formatWarmMetric(project.indexBuildDurationMs)));
298
+ lines.push(fieldLine("Task Count", project.taskCount));
299
+ for (const task of project.tasks) {
300
+ pushSection(lines, `Task ${task.taskOrdinal}: ${sanitizeScalar(task.caseId)}`);
301
+ const rows = [
302
+ ["Raw Status", task.rawStatus],
303
+ ["Warm Status", task.warmStatus],
304
+ ["Raw Context Characters", formatWarmMetric(task.raw.contextCharacters)],
305
+ ["Warm Context Characters", formatWarmMetric(task.warm.contextCharacters)],
306
+ ["Raw Estimated Context Tokens (estimate)", formatWarmMetric(task.raw.contextEstimatedTokens)],
307
+ ["Warm Estimated Context Tokens (estimate)", formatWarmMetric(task.warm.contextEstimatedTokens)],
308
+ ["Raw Operation Duration", formatWarmMetric(task.raw.operationDurationMs)],
309
+ ["Warm Retrieval Duration", formatWarmMetric(task.warm.retrievalDurationMs)],
310
+ ["Amortized Index Build Duration", formatWarmMetric(task.warm.amortizedIndexBuildDurationMs)],
311
+ ["Cumulative Raw Duration", formatWarmMetric(task.raw.cumulativeDurationMs)],
312
+ ["Cumulative Warm Component Duration", formatWarmMetric(task.warm.cumulativeComponentDurationMs)],
313
+ ["Cumulative Raw Estimated Context Tokens (estimate)", formatWarmMetric(task.raw.cumulativeEstimatedContextTokens)],
314
+ ["Cumulative Warm Estimated Context Tokens (estimate)", formatWarmMetric(task.warm.cumulativeEstimatedContextTokens)],
315
+ ["Raw Fake-Agent Evaluation", formatWarmAgent(task.rawAgent)],
316
+ ["Warm Fake-Agent Evaluation", formatWarmAgent(task.warmAgent)],
317
+ ["Raw Agent Correctness (fake agent)", formatWarmMetric(task.raw.agentCorrectness)],
318
+ ["Warm Agent Correctness (fake agent)", formatWarmMetric(task.warm.agentCorrectness)],
319
+ ["Raw Agent Total Tokens (fake-agent simulated)", formatWarmMetric(task.raw.agentTotalTokens)],
320
+ ["Warm Agent Total Tokens (fake-agent simulated)", formatWarmMetric(task.warm.agentTotalTokens)],
321
+ ["Cumulative Raw Agent Total Tokens (fake-agent simulated)", formatWarmMetric(task.raw.cumulativeAgentTotalTokens)],
322
+ ["Cumulative Warm Agent Total Tokens (fake-agent simulated)", formatWarmMetric(task.warm.cumulativeAgentTotalTokens)],
323
+ ];
324
+ for (const [label, value] of rows) {
325
+ lines.push(fieldLine(label, value));
326
+ }
327
+ }
328
+ });
329
+ }
259
330
  export function renderPluginExperimentReportText(report) {
260
331
  const lines = [];
261
332
  pushSection(lines, "Plugin Experiment Report");
@@ -284,6 +355,8 @@ export function renderPluginExperimentReportText(report) {
284
355
  pushDashList(lines, report.metrics.map((metric) => `${metric.name} (${metric.id}) value=${metric.value ?? "unavailable"} unit=${metric.unit ?? ""}`));
285
356
  pushSection(lines, "V0.4.3 Stage-Context Evidence");
286
357
  renderV043Section(lines, report.contextStrategyComparisonV043);
358
+ pushSection(lines, "Warm Index Reuse Evidence");
359
+ renderWarmIndexReuseSection(lines, report.warmIndexReuse);
287
360
  pushSection(lines, "Warnings, Skips, And Failures");
288
361
  pushDashList(lines, report.findings.map((finding) => `${finding.severity} ${finding.code} ${finding.message} variant=${finding.variantId ?? ""} case=${finding.caseId ?? ""}`));
289
362
  pushSection(lines, "Artifacts");
@@ -0,0 +1,150 @@
1
+ const UNIT_LABELS = {
2
+ ms: "ms",
3
+ characters: "chars",
4
+ "estimated-tokens": "est. tokens",
5
+ tokens: "tokens",
6
+ score: "score",
7
+ };
8
+ export function renderWarmIndexReuseHtml(section) {
9
+ if (section === null) {
10
+ return `<section>
11
+ <h2>Warm Index Reuse Evidence</h2>
12
+ <p>Not applicable to this plugin.</p>
13
+ </section>`;
14
+ }
15
+ return `<section>
16
+ <h2>Warm Index Reuse Evidence</h2>
17
+ ${table(["Field", "Value"], [
18
+ ["Projects", String(section.summary.projectCount)],
19
+ ["Tasks", String(section.summary.taskCount)],
20
+ ["Projects with a prepared index session", String(section.summary.preparedSessionProjectCount)],
21
+ ["Incomplete or failed projects", String(section.summary.incompleteProjectCount)],
22
+ ["Fake-agent correctness available (task sides)", `${section.summary.agentCorrectnessAvailableCount} of ${section.summary.agentSideCount}`],
23
+ ["Fake-agent token totals available (task sides)", `${section.summary.agentTotalTokensAvailableCount} of ${section.summary.agentSideCount}`],
24
+ ])}
25
+ <h3>Cold Start And Warm Reuse</h3>
26
+ ${list(section.costModel)}
27
+ <h3>Limitations</h3>
28
+ ${list(section.limitations)}
29
+ ${section.projects.length === 0 ? "<p>No warm-index project evidence was recorded.</p>" : section.projects.map(renderProject).join("\n")}
30
+ </section>`;
31
+ }
32
+ function renderProject(project, index) {
33
+ const unavailableRows = project.tasks.flatMap((task) => [
34
+ ["Raw context characters", task.raw.contextCharacters],
35
+ ["Warm context characters", task.warm.contextCharacters],
36
+ ["Raw estimated context tokens", task.raw.contextEstimatedTokens],
37
+ ["Warm estimated context tokens", task.warm.contextEstimatedTokens],
38
+ ["Raw operation duration", task.raw.operationDurationMs],
39
+ ["Warm retrieval duration", task.warm.retrievalDurationMs],
40
+ ["Amortized index build duration", task.warm.amortizedIndexBuildDurationMs],
41
+ ["Cumulative raw duration", task.raw.cumulativeDurationMs],
42
+ ["Cumulative warm component duration", task.warm.cumulativeComponentDurationMs],
43
+ ["Cumulative raw estimated context tokens", task.raw.cumulativeEstimatedContextTokens],
44
+ ["Cumulative warm estimated context tokens", task.warm.cumulativeEstimatedContextTokens],
45
+ ["Raw fake-agent correctness", task.raw.agentCorrectness],
46
+ ["Warm fake-agent correctness", task.warm.agentCorrectness],
47
+ ["Raw fake-agent total tokens", task.raw.agentTotalTokens],
48
+ ["Warm fake-agent total tokens", task.warm.agentTotalTokens],
49
+ ["Cumulative raw fake-agent total tokens", task.raw.cumulativeAgentTotalTokens],
50
+ ["Cumulative warm fake-agent total tokens", task.warm.cumulativeAgentTotalTokens],
51
+ ]
52
+ .filter(([, metric]) => metric.availability !== "available")
53
+ .map(([label, metric]) => [String(task.taskOrdinal), task.caseId, label, metric.availability, metric.reason ?? ""]));
54
+ return `<h3>Project ${index + 1}: ${escapeHtml(project.benchmarkProject)}</h3>
55
+ ${table(["Field", "Value"], [
56
+ ["Session key", project.sessionKey],
57
+ ["Project status", project.status],
58
+ ["Session prepared", String(project.sessionPrepared)],
59
+ ["Index build duration", formatMetric(project.indexBuildDurationMs)],
60
+ ["Index build duration availability", withReason(project.indexBuildDurationMs)],
61
+ ["Tasks", String(project.taskCount)],
62
+ ])}
63
+ ${table([
64
+ "Task",
65
+ "Raw status",
66
+ "Warm status",
67
+ "Raw chars",
68
+ "Warm chars",
69
+ "Raw est. tokens",
70
+ "Warm est. tokens",
71
+ "Raw duration",
72
+ "Warm retrieval duration",
73
+ "Amortized index cost",
74
+ "Cumulative raw duration",
75
+ "Cumulative warm duration",
76
+ "Cumulative raw est. tokens",
77
+ "Cumulative warm est. tokens",
78
+ ], project.tasks.map((task) => [
79
+ `${task.taskOrdinal}. ${task.caseId}`,
80
+ task.rawStatus,
81
+ task.warmStatus,
82
+ formatMetric(task.raw.contextCharacters),
83
+ formatMetric(task.warm.contextCharacters),
84
+ formatMetric(task.raw.contextEstimatedTokens),
85
+ formatMetric(task.warm.contextEstimatedTokens),
86
+ formatMetric(task.raw.operationDurationMs),
87
+ formatMetric(task.warm.retrievalDurationMs),
88
+ formatMetric(task.warm.amortizedIndexBuildDurationMs),
89
+ formatMetric(task.raw.cumulativeDurationMs),
90
+ formatMetric(task.warm.cumulativeComponentDurationMs),
91
+ formatMetric(task.raw.cumulativeEstimatedContextTokens),
92
+ formatMetric(task.warm.cumulativeEstimatedContextTokens),
93
+ ]))}
94
+ <p class="muted">Estimated tokens are character-based context-size estimates, not provider token usage.</p>
95
+ <p class="muted">Agent columns are deterministic fake-agent evidence; token totals are simulated harness telemetry, not provider billing telemetry.</p>
96
+ ${table([
97
+ "Task",
98
+ "Raw fake-agent status",
99
+ "Warm fake-agent status",
100
+ "Raw correctness",
101
+ "Warm correctness",
102
+ "Raw agent tokens",
103
+ "Warm agent tokens",
104
+ "Cumulative raw agent tokens",
105
+ "Cumulative warm agent tokens",
106
+ "Token source / reliability",
107
+ ], project.tasks.map((task) => [
108
+ `${task.taskOrdinal}. ${task.caseId}`,
109
+ formatAgent(task.rawAgent),
110
+ formatAgent(task.warmAgent),
111
+ formatMetric(task.raw.agentCorrectness),
112
+ formatMetric(task.warm.agentCorrectness),
113
+ formatMetric(task.raw.agentTotalTokens),
114
+ formatMetric(task.warm.agentTotalTokens),
115
+ formatMetric(task.raw.cumulativeAgentTotalTokens),
116
+ formatMetric(task.warm.cumulativeAgentTotalTokens),
117
+ [task.rawAgent, task.warmAgent]
118
+ .map((agent) => (agent ? `${agent.tokenUsageSource} / ${agent.tokenUsageReliability}` : "not run"))
119
+ .join("; "),
120
+ ]))}
121
+ ${unavailableRows.length === 0 ? "" : table(["Task", "Case", "Metric", "Availability", "Reason"], unavailableRows)}`;
122
+ }
123
+ function formatAgent(agent) {
124
+ if (agent === null)
125
+ return "not run";
126
+ const passed = agent.passed === null ? "" : agent.passed ? ", passed" : ", not passed";
127
+ return `${agent.status}${passed}${agent.errors[0] ? ` (${agent.errors[0]})` : ""}`;
128
+ }
129
+ function formatMetric(metric) {
130
+ return metric.availability === "available" ? `${metric.value} ${UNIT_LABELS[metric.unit]}` : metric.availability;
131
+ }
132
+ function withReason(metric) {
133
+ return metric.availability === "available" ? formatMetric(metric) : `${metric.availability}: ${metric.reason ?? ""}`;
134
+ }
135
+ function list(items) {
136
+ return `<ul>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`;
137
+ }
138
+ function table(headers, rows) {
139
+ return `<table><thead><tr>${headers.map((header) => `<th>${escapeHtml(header)}</th>`).join("")}</tr></thead><tbody>${rows
140
+ .map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join("")}</tr>`)
141
+ .join("")}</tbody></table>`;
142
+ }
143
+ function escapeHtml(value) {
144
+ return String(value ?? "")
145
+ .replaceAll("&", "&amp;")
146
+ .replaceAll("<", "&lt;")
147
+ .replaceAll(">", "&gt;")
148
+ .replaceAll('"', "&quot;")
149
+ .replaceAll("'", "&#39;");
150
+ }
@@ -0,0 +1 @@
1
+ export const WARM_INDEX_REUSE_REPORT_SCHEMA_VERSION = "my-dev-kit-lab-warm-index-report-v1";
@@ -266,6 +266,17 @@ export function validateAction(errors, location, value) {
266
266
  validateLocator(errors, `${location}.target`, value.target);
267
267
  validateOptionalTimeout(errors, `${location}.timeoutMs`, value.timeoutMs);
268
268
  break;
269
+ case "select-option":
270
+ // Closed to `value` alone: `label`, `index` and `values` are unknown
271
+ // fields here, so a scenario cannot reach a selection mode the runtime
272
+ // does not implement by guessing at Playwright's wider API.
273
+ rejectUnknownKeys(errors, location, value, ["type", "locator", "value", "timeoutMs"]);
274
+ validateLocator(errors, `${location}.locator`, value.locator);
275
+ if (!isNonEmptyString(value.value)) {
276
+ errors.add(`${location}.value`, `expected a non-empty option value string after trimming, received ${describeValue(value.value)}.`);
277
+ }
278
+ validateOptionalTimeout(errors, `${location}.timeoutMs`, value.timeoutMs);
279
+ break;
269
280
  case "pointer-click":
270
281
  rejectUnknownKeys(errors, location, value, ["type", "locator", "position", "coordinateSpace", "timeoutMs"]);
271
282
  validateLocator(errors, `${location}.locator`, value.locator);
@@ -99,6 +99,18 @@ async function performAction(page, action, context) {
99
99
  await runWithMouseDown(page.mouse, () => page.mouse.move(end.x, end.y, { steps: POINTER_DRAG_MOVE_STEPS }));
100
100
  return;
101
101
  }
102
+ case "select-option": {
103
+ const locator = resolveTutorialLocator(page, action.locator);
104
+ const selected = await locator.selectOption({ value: action.value }, { timeout: timeoutFor(action.timeoutMs) });
105
+ // Playwright resolves to the values it actually selected. A mismatched or
106
+ // empty result means the intended option was not chosen, so it is a
107
+ // failure rather than a silently-accepted no-op; more than one value
108
+ // would mean a multi-select, which this contract does not express.
109
+ if (selected.length !== 1 || selected[0] !== action.value) {
110
+ throw new Error(`expected exactly one selected value ${JSON.stringify(action.value)}, but the select reported ${JSON.stringify(selected)}`);
111
+ }
112
+ return;
113
+ }
102
114
  case "wait-for":
103
115
  await resolveTutorialLocator(page, action.locator).waitFor({
104
116
  ...(action.state !== undefined ? { state: action.state } : {}),
@@ -159,6 +171,8 @@ function describeActionError(action, error) {
159
171
  return `pointer-click on ${describeTutorialLocator(action.locator)} at fraction ${describeFractionPoint(action.position)} failed: ${message}`;
160
172
  case "pointer-drag":
161
173
  return `pointer-drag on ${describeTutorialLocator(action.locator)} from fraction ${describeFractionPoint(action.from)} to ${describeFractionPoint(action.to)} failed: ${message}`;
174
+ case "select-option":
175
+ return `select-option on ${describeTutorialLocator(action.locator)} for value ${JSON.stringify(action.value)} failed: ${message}`;
162
176
  case "click":
163
177
  case "fill":
164
178
  case "press":
@@ -239,6 +239,9 @@ export function cursorLocatorForAction(action) {
239
239
  case "fill":
240
240
  case "press":
241
241
  case "hover":
242
+ // The cursor rests on the select itself; the native popup is OS-drawn and
243
+ // is deliberately not animated through.
244
+ case "select-option":
242
245
  case "pointer-click":
243
246
  case "pointer-drag":
244
247
  return action.locator;
@@ -31,6 +31,7 @@ export const TUTORIAL_ACTION_TYPES = [
31
31
  "press",
32
32
  "hover",
33
33
  "drag",
34
+ "select-option",
34
35
  "wait-for",
35
36
  "pointer-click",
36
37
  "pointer-drag"
package/docs/METRICS.md CHANGED
@@ -441,3 +441,67 @@ Implemented in `src/evaluation/stageContextMetrics` and composed once per run by
441
441
  - Determinism and fixture self-immutability
442
442
  Meaning: reuses `calculateStageContextDeterminism`/`canonicalizeStageContextRun` (`v0.4.3`) to confirm repeated evaluations of the same fixture produce identical canonicalized results, and re-runs fixture hash verification to confirm the frozen bytes were not mutated by evaluation.
443
443
  Caveat: this is fixture-level self-immutability, not the `v0.4.3` live-target-mutation check — no live target repository is exercised by this evaluation path.
444
+
445
+ ## Warm-index reuse metrics
446
+
447
+ Implemented in the current release v0.5.0. The `warm-index-reuse` plugin calculates these metrics once, in `src/experiments/plugins/warmIndexReuse/metrics.ts`, from bounded execution summaries and bounded fake-agent evidence. The report section (`report.warmIndexReuse` in `report.json`, and the text and HTML "Warm Index Reuse Evidence" sections) and the four warm-index plots render these precomputed values; they never recalculate a formula.
448
+
449
+ **Availability contract.** Every warm-index metric carries `availability`, `value`, `unit`, `source`, `reason`, and `tokenCountMethod`:
450
+
451
+ - `available`: `value` is a finite number and `reason` is `null`. Zero is a valid available value.
452
+ - `unavailable`: `value` is `null` and `reason` explains the missing or invalid evidence. Missing evidence is never reported as zero, and a non-finite upstream number becomes `unavailable` with an invalid-input reason.
453
+ - `not-applicable`: `value` is `null` and `reason` explains why the metric does not apply. The contract supports it; the current warm-index metrics do not emit it.
454
+
455
+ Units are `ms`, `characters`, `estimated-tokens`, `tokens`, and `score`. Sources are `measured`, `derived`, `estimated-chars-div-4`, and `agent`. `tokenCountMethod` is set only for estimated-token metrics (currently `estimated_chars_div_4`).
456
+
457
+ **Task ordinal.** Tasks keep their filtered source order inside each benchmark project and are numbered `1, 2, 3, …` per project. Ordinals, amortization, and every cumulative sum restart for each benchmark project.
458
+
459
+ **Direct measurements (per task):**
460
+
461
+ - Raw context characters / warm retrieved context characters (`characters`, `measured`)
462
+ Meaning: characters in the raw full-file context and in the retrieved my-dev-kit context.
463
+ Caveat: unavailable when that side produced no evidence (for example, a raw baseline failure, or no warm retrieval because the project index was not prepared).
464
+ - Raw estimated context tokens / warm estimated context tokens (`estimated-tokens`, `estimated-chars-div-4`)
465
+ Meaning: the character-based context-size estimate, `ceil(characters / 4)`.
466
+ Caveat: a context-size estimate, not provider token usage and not fake-agent token usage.
467
+ - Raw context-construction duration (`ms`, `measured`)
468
+ Meaning: time to build the raw full-file context for the task. It is not agent duration.
469
+ - Warm retrieval duration (`ms`, `measured`)
470
+ Meaning: search, lookup, slice, and source against the already prepared index. It never includes the index build.
471
+ Caveat: a skipped retrieval (no usable context) still reports its measured duration and sizes; its outcome remains `skipped`.
472
+ - Index build duration (per project, `ms`, `measured`)
473
+ Meaning: the measured duration of the project's single my-dev-kit `index` command.
474
+ Caveat: available whenever a build was attempted and measured, even if the build failed. Unavailable, never zero, when the project group was structurally inconsistent and no index setup was attempted.
475
+
476
+ **Derived measurements (per task):**
477
+
478
+ - Amortized index build duration (`ms`, `derived`)
479
+ Formula: `amortizedIndexBuildDurationMs(N) = indexBuildDurationMs / N` for task ordinal `N`, without rounding.
480
+ Caveat: available only when the project prepared a valid index session. A failed or never-attempted index is not amortized.
481
+ - Cumulative raw component duration (`ms`, `measured`)
482
+ Formula: sum of raw context-construction durations for tasks `1..N`.
483
+ - Cumulative warm component duration (`ms`, `derived`)
484
+ Formula: `indexBuildDurationMs` (counted once) plus the warm retrieval durations for tasks `1..N`. At `N = 1` this is the cold-start warm cost: index build plus the first retrieval.
485
+ Caveat: requires a prepared index session. It is a sum of measured components, not wall-clock or end-to-end latency, and it is not a speedup claim.
486
+ - Cumulative raw / warm estimated context tokens (`estimated-tokens`, `estimated-chars-div-4`)
487
+ Formula: sum of that side's estimated context tokens for tasks `1..N`. No index-token cost is added; indexing is a local repository operation, not model context.
488
+ Caveat: if different token-count methods appear within one prefix, the cumulative value is unavailable rather than summed.
489
+
490
+ **Strict-prefix rule.** A cumulative metric at ordinal `N` is available only when every task `1..N` has the required input. After the first missing or invalid input, that cumulative metric stays unavailable for every later task in the project, with a reason naming the first incomplete task. The later tasks' direct metrics remain available.
491
+
492
+ **Fake-agent evidence (per task side):**
493
+
494
+ The plugin evaluates each task side that has context evidence once with the deterministic fake agent, through the existing prompt generator, answer parser, run classifier, and correctness scorer. A side with no context evidence is not evaluated, and its agent metrics are unavailable with that reason.
495
+
496
+ - Fake-agent correctness score (`score`, `agent`)
497
+ Meaning: `correctnessScore` from the existing benchmark answer-key scorer (see "Experiment and run metrics" above).
498
+ Caveat: available only when the fake-agent answer was scoreable (classified `completed` or `invalid-output`); a failed agent run reports `unavailable` with its status and error. It is deterministic benchmark-harness evidence, not real-model correctness, and retrieval success is never used as correctness.
499
+ - Fake-agent total tokens (`tokens`, `agent`)
500
+ Meaning: `tokenUsage.totalTokens` exactly as reported by the fake agent (its prompt estimate plus a fixed simulated output count), with the reported token-usage source and reliability kept in the agent evidence.
501
+ Caveat: simulated harness telemetry, not provider billing telemetry. When the fake agent reports no total (for example `FAKE_AGENT_MODE=missing-token-usage`), the value is unavailable; estimated context tokens are never substituted.
502
+ - Cumulative fake-agent total tokens (`tokens`, `agent`)
503
+ Formula: sum of fake-agent total tokens for tasks `1..N` on that side, under the strict-prefix rule.
504
+
505
+ **Generic outcome metrics.** Available values also appear as `ExperimentMetric` entries on each outcome, with `variantId`, `caseId`, `unit`, and `description`. Both the `raw-full-file` and `warm-index-reuse` outcomes can carry `context-character-count`, `context-estimated-token-count`, `operation-duration-ms` (raw context construction or warm retrieval), `cumulative-component-duration-ms`, `cumulative-context-estimated-token-count`, `agent-correctness-score`, `agent-total-tokens`, and `cumulative-agent-total-tokens`. The warm outcome also carries `amortized-index-build-duration-ms`. Unavailable metrics are omitted rather than emitted as zero. Run-level metrics are limited to `warm-index-project-count`, `warm-index-task-count`, and `warm-index-session-prepared-project-count`.
506
+
507
+ **Interpretation.** The report separates the one-time index cost from per-task retrieval cost and shows the fixed cost falling per task as more tasks reuse the index. It calculates no token-savings percentage, duration-reduction percentage, break-even task, composite score, winner, or ranking. Warm-index plots map the same precomputed values: amortized index build duration, raw versus retrieved estimated context tokens, fake-agent correctness, and cumulative fake-agent total tokens. Unavailable values become skipped plot points with their reason.
@@ -12,6 +12,7 @@
12
12
  button { font: inherit; padding: 8px 16px; border-radius: 6px; border: 1px solid #5a6270; background: #fff; cursor: pointer; }
13
13
  button:hover { background: #eef1f5; }
14
14
  input { font: inherit; padding: 8px; border: 1px solid #b8bec8; border-radius: 6px; width: 100%; box-sizing: border-box; }
15
+ select { font: inherit; padding: 8px; border: 1px solid #b8bec8; border-radius: 6px; }
15
16
  .dropzone { min-height: 72px; border: 2px dashed #9aa3af; border-radius: 8px; padding: 12px; }
16
17
  .card { background: #dde7ff; border: 1px solid #5f7fd6; border-radius: 6px; padding: 10px; width: 140px; }
17
18
  #pointer-surface { display: block; width: 400px; height: 240px; background: #eef4ff; box-shadow: inset 0 0 0 2px #7890c2; touch-action: none; user-select: none; }
@@ -26,6 +27,7 @@
26
27
  <div class="panel"><p id="intro-text">Welcome to the generic tutorial fixture.</p><button id="primary-action" data-testid="primary-action" data-state="idle">Activate</button> <span id="activation-count" data-testid="activation-count">0</span></div>
27
28
  <div class="panel"><label for="name-field">Your name</label><input id="name-field" data-testid="name-field" aria-label="Your name" /><p id="greeting" data-testid="greeting"></p></div>
28
29
  <div class="panel"><button id="hover-target" data-testid="hover-target">Hover me</button><p id="hover-readout" data-testid="hover-readout">not hovered</p></div>
30
+ <div class="panel"><label for="operation-select">Operation</label> <select id="operation-select" data-testid="operation-select" aria-label="Operation"><option value="inspect">Inspect</option><option value="move">Move</option><option value="resize">Resize</option><option value="remove">Remove</option><option value="preserve">Preserve</option></select> <span id="selected-operation" data-testid="selected-operation" data-selected-value="inspect">inspect</span></div>
29
31
  <div class="panel"><div id="status-banner" data-testid="status-banner" class="hidden">Delayed banner is now visible</div></div>
30
32
  <script>
31
33
  (() => {
@@ -37,6 +39,9 @@
37
39
  nameField.addEventListener("input", () => { greeting.textContent = nameField.value ? `Hello, ${nameField.value}` : ""; });
38
40
  nameField.addEventListener("keydown", event => { if (event.key === "Enter") greeting.textContent = `Submitted: ${nameField.value}`; });
39
41
  document.getElementById("hover-target").addEventListener("mouseenter", () => { document.getElementById("hover-readout").textContent = "hovered"; });
42
+ const operationSelect = document.getElementById("operation-select");
43
+ const selectedOperation = document.getElementById("selected-operation");
44
+ operationSelect.addEventListener("change", () => { selectedOperation.textContent = operationSelect.value; selectedOperation.setAttribute("data-selected-value", operationSelect.value); });
40
45
  const dragCard = document.getElementById("drag-card");
41
46
  const dropZone = document.getElementById("drop-zone");
42
47
  dragCard.addEventListener("dragstart", event => event.dataTransfer.setData("text/plain", "drag-card"));
@@ -14,6 +14,7 @@
14
14
  { "id": "drag-card", "narration": "Drag the card onto the drop target.", "action": { "type": "drag", "source": { "kind": "test-id", "testId": "drag-card" }, "target": { "kind": "test-id", "testId": "drop-zone" } }, "highlight": { "kind": "test-id", "testId": "drop-zone" }, "screenshot": { "id": "dropped" }, "assertions": [ { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "drop-zone" }, "name": "data-dropped", "expected": "true" } ] },
15
15
  { "id": "pointer-click-surface", "narration": "Click a deliberate position inside the pointer gesture surface.", "action": { "type": "pointer-click", "locator": { "kind": "test-id", "testId": "pointer-surface" }, "position": { "x": 0.25, "y": 0.25 }, "coordinateSpace": "fraction" }, "highlight": { "kind": "test-id", "testId": "pointer-surface" }, "screenshot": { "id": "pointer-clicked" }, "assertions": [ { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "pointer-surface" }, "name": "data-pointer-click-count", "expected": "1" }, { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "pointer-surface" }, "name": "data-last-pointer-click-zone", "expected": "upper-left" } ] },
16
16
  { "id": "pointer-drag-surface", "narration": "Drag across two distinct positions inside the same pointer gesture surface.", "action": { "type": "pointer-drag", "locator": { "kind": "test-id", "testId": "pointer-surface" }, "from": { "x": 0.20, "y": 0.25 }, "to": { "x": 0.80, "y": 0.75 }, "coordinateSpace": "fraction" }, "screenshot": { "id": "pointer-dragged" }, "assertions": [ { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "pointer-surface" }, "name": "data-pointer-drag-count", "expected": "1" }, { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "pointer-surface" }, "name": "data-pointer-drag-start-zone", "expected": "upper-left" }, { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "pointer-surface" }, "name": "data-pointer-drag-end-zone", "expected": "lower-right" }, { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "pointer-surface" }, "name": "data-pointer-drag-intermediate", "expected": "true" } ] },
17
+ { "id": "select-operation", "narration": "Select the preserve operation from the native selector.", "action": { "type": "select-option", "locator": { "kind": "role", "role": "combobox", "name": "Operation" }, "value": "preserve" }, "highlight": { "kind": "role", "role": "combobox", "name": "Operation" }, "screenshot": { "id": "operation-selected" }, "assertions": [ { "type": "text-equals", "locator": { "kind": "test-id", "testId": "selected-operation" }, "expected": "preserve" }, { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "selected-operation" }, "name": "data-selected-value", "expected": "preserve" } ] },
17
18
  { "id": "wait-banner", "narration": "Wait for the delayed status banner to appear.", "action": { "type": "wait-for", "locator": { "kind": "text", "text": "Delayed banner is now visible" }, "state": "visible", "timeoutMs": 5000 }, "callout": { "text": "The banner appears after a short delay." }, "screenshot": { "id": "banner-visible" }, "assertions": [ { "type": "element-visible", "locator": { "kind": "test-id", "testId": "status-banner" } } ] }
18
19
  ]
19
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dailephd/my-dev-kit-lab",
3
- "version": "0.4.8",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Evidence, benchmark, and evaluation companion for my-dev-kit.",
6
6
  "bin": {