@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
@@ -0,0 +1,112 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { WARM_INDEX_REUSE_REPORT_SCHEMA_VERSION, } from "../report/experiments/warmIndexReuseReportModel.js";
5
+ // ---------------------------------------------------------------------------
6
+ // Warm-index plot data. Maps metrics already calculated by the warm-index
7
+ // metric owner (as carried by the plugin report) to plot points; unavailable
8
+ // metrics become skipped points with their reason. No formula is recalculated.
9
+ // ---------------------------------------------------------------------------
10
+ export const WARM_INDEX_PLOT_IDS = [
11
+ "warm-index-amortized-index-cost",
12
+ "warm-index-context-size",
13
+ "warm-index-correctness",
14
+ "warm-index-cumulative-token-usage",
15
+ ];
16
+ const WARM_INDEX_PLUGIN_ID = "warm-index-reuse";
17
+ const VARIANTS = ["raw-full-file", "warm-index-reuse"];
18
+ /**
19
+ * Returns the warm-index report section when `<experimentDir>/report.json` belongs to the
20
+ * warm-index-reuse plugin, or null for any other directory. A warm report that is malformed
21
+ * fails instead of falling back to legacy controlled-experiment parsing.
22
+ */
23
+ export async function readWarmIndexPlotSource(experimentDir) {
24
+ const reportPath = path.join(experimentDir, "report.json");
25
+ if (!existsSync(reportPath))
26
+ return null;
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(await readFile(reportPath, "utf8"));
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ const report = parsed?.report;
35
+ if (report?.plugin?.id !== WARM_INDEX_PLUGIN_ID)
36
+ return null;
37
+ const section = report.warmIndexReuse;
38
+ const invalid = (detail) => new Error(`Invalid warm-index-reuse report for plots (${reportPath}): ${detail}`);
39
+ if (!section || typeof section !== "object")
40
+ throw invalid("missing warmIndexReuse section.");
41
+ if (section.schemaVersion !== WARM_INDEX_REUSE_REPORT_SCHEMA_VERSION) {
42
+ throw invalid(`unsupported schema version ${String(section.schemaVersion)}.`);
43
+ }
44
+ if (!Array.isArray(section.projects))
45
+ throw invalid("projects must be an array.");
46
+ for (const project of section.projects) {
47
+ if (!project || typeof project.benchmarkProject !== "string" || !Array.isArray(project.tasks)) {
48
+ throw invalid("each project needs a benchmarkProject and a tasks array.");
49
+ }
50
+ for (const task of project.tasks) {
51
+ if (!task || typeof task.taskOrdinal !== "number" || !task.raw || !task.warm) {
52
+ throw invalid("each task needs a taskOrdinal and raw/warm metrics.");
53
+ }
54
+ }
55
+ }
56
+ return section;
57
+ }
58
+ export function buildWarmIndexPlotData(args) {
59
+ const skippedPoints = [];
60
+ const series = (id, title, yLabel) => ({
61
+ id,
62
+ title,
63
+ xLabel: "Task ordinal",
64
+ yLabel,
65
+ kind: "scatter",
66
+ points: [],
67
+ warnings: [],
68
+ });
69
+ const amortized = series("warm-index-amortized-index-cost", "Amortized index build cost", "Amortized index build duration (ms)");
70
+ const contextSize = series("warm-index-context-size", "Raw vs retrieved context size", "Estimated context tokens");
71
+ const correctness = series("warm-index-correctness", "Fake-agent correctness by strategy", "Fake-agent correctness score");
72
+ const cumulativeTokens = series("warm-index-cumulative-token-usage", "Cumulative fake-agent token usage", "Cumulative fake-agent total tokens (simulated)");
73
+ const add = (plot, metric, group, label, task) => {
74
+ if (metric.availability === "available" && metric.value !== null && Number.isFinite(metric.value)) {
75
+ const point = { x: task.taskOrdinal, y: metric.value, group, label, metadata: { caseId: task.caseId } };
76
+ plot.points.push(point);
77
+ }
78
+ else {
79
+ skippedPoints.push({ plotId: plot.id, label, reason: metric.reason ?? `Metric is ${metric.availability}.` });
80
+ }
81
+ };
82
+ for (const project of args.section.projects) {
83
+ for (const task of project.tasks) {
84
+ const taskLabel = `${project.benchmarkProject} task ${task.taskOrdinal} (${task.caseId})`;
85
+ add(amortized, task.warm.amortizedIndexBuildDurationMs, project.benchmarkProject, taskLabel, task);
86
+ for (const variantId of VARIANTS) {
87
+ const side = variantId === "raw-full-file" ? task.raw : task.warm;
88
+ const group = `${project.benchmarkProject} / ${variantId}`;
89
+ const label = `${taskLabel} ${variantId}`;
90
+ add(contextSize, side.contextEstimatedTokens, group, label, task);
91
+ add(correctness, side.agentCorrectness, group, label, task);
92
+ // Agent token totals only; estimated context tokens are never used as a fallback.
93
+ add(cumulativeTokens, side.cumulativeAgentTotalTokens, group, label, task);
94
+ }
95
+ }
96
+ }
97
+ const plots = [amortized, contextSize, correctness, cumulativeTokens];
98
+ const warnings = [];
99
+ for (const plot of plots) {
100
+ if (plot.points.length === 0) {
101
+ plot.warnings.push("No comparable data available.");
102
+ warnings.push(`${plot.title}: no comparable data available.`);
103
+ }
104
+ }
105
+ return {
106
+ generatedAt: args.generatedAt ?? new Date().toISOString(),
107
+ sourceExperimentDir: args.experimentDir,
108
+ plots,
109
+ skippedPoints,
110
+ warnings,
111
+ };
112
+ }
@@ -1,4 +1,5 @@
1
1
  export * from "./types.js";
2
2
  export * from "./buildExperimentPlotData.js";
3
+ export * from "./buildWarmIndexPlotData.js";
3
4
  export * from "./renderSvgChart.js";
4
5
  export * from "./writePlotArtifacts.js";
@@ -2,6 +2,7 @@ import { mkdir, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { resolveWithinRoot } from "../core/pathSafety.js";
4
4
  import { buildExperimentPlotData } from "./buildExperimentPlotData.js";
5
+ import { buildWarmIndexPlotData, readWarmIndexPlotSource } from "./buildWarmIndexPlotData.js";
5
6
  import { renderSvgChart } from "./renderSvgChart.js";
6
7
  const chartFiles = {
7
8
  "token-savings-vs-prompt-length": "token-savings-vs-prompt-length.svg",
@@ -12,7 +13,13 @@ const chartFiles = {
12
13
  "run-outcomes-by-agent": "run-outcomes-by-agent.svg"
13
14
  };
14
15
  export async function writePlotArtifacts(options) {
15
- const data = await buildExperimentPlotData({ experimentDir: options.experimentDir, repoRoot: options.repoRoot });
16
+ // A warm-index-reuse plugin output directory is detected by its plugin report; every other
17
+ // directory keeps the legacy controlled-experiment plot path unchanged.
18
+ const experimentDir = path.resolve(options.repoRoot ?? process.cwd(), options.experimentDir);
19
+ const warmSection = await readWarmIndexPlotSource(experimentDir);
20
+ const data = warmSection
21
+ ? buildWarmIndexPlotData({ section: warmSection, experimentDir })
22
+ : await buildExperimentPlotData({ experimentDir: options.experimentDir, repoRoot: options.repoRoot });
16
23
  return writePlotArtifactsFromData({ data, outDir: options.outDir });
17
24
  }
18
25
  export async function writePlotArtifactsFromData(options) {
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { buildContextStrategyComparisonV043Report } from "./buildContextStrategyComparisonV043Report.js";
3
+ import { buildWarmIndexReuseReport } from "./buildWarmIndexReuseReport.js";
3
4
  const V043_BULK_ARRAY_KEYS = [
4
5
  "v043StageContextExecutions",
5
6
  "v043StageContextEvaluations",
@@ -9,6 +10,7 @@ export function buildPluginExperimentReport(args) {
9
10
  const outputRoot = args.outputRoot ?? readString(args.run.metadata?.outputRoot) ?? null;
10
11
  const allOutcomes = args.run.cases.flatMap((experimentCase) => experimentCase.outcomes);
11
12
  const contextStrategyComparisonV043 = buildContextStrategyComparisonV043Report(args.run);
13
+ const warmIndexReuse = buildWarmIndexReuseReport(args.run);
12
14
  const rawRun = { ...args.run, artifacts: relativizeArtifacts(args.run.artifacts, outputRoot) };
13
15
  for (const key of V043_BULK_ARRAY_KEYS) {
14
16
  delete rawRun[key];
@@ -36,8 +38,9 @@ export function buildPluginExperimentReport(args) {
36
38
  failures: args.run.failures,
37
39
  skippedOutcomes: allOutcomes.filter((outcome) => outcome.status === "skipped"),
38
40
  findings: buildFindings(args.run),
41
+ warmIndexReuse,
39
42
  contextStrategyComparisonV043,
40
- interpretation: buildInterpretation(args.run, contextStrategyComparisonV043),
43
+ interpretation: buildInterpretation(args.run, contextStrategyComparisonV043, warmIndexReuse),
41
44
  rawRun,
42
45
  };
43
46
  }
@@ -94,7 +97,20 @@ function buildFindings(run) {
94
97
  ...skipped,
95
98
  ];
96
99
  }
97
- function buildInterpretation(run, contextStrategyComparisonV043) {
100
+ function buildInterpretation(run, contextStrategyComparisonV043, warmIndexReuse) {
101
+ if (run.pluginId === "warm-index-reuse" && warmIndexReuse) {
102
+ const summary = warmIndexReuse.summary;
103
+ return {
104
+ summary: `The warm-index run prepared ${summary.preparedSessionProjectCount} of ${summary.projectCount} project indexes and evaluated ${summary.taskCount} tasks. ` +
105
+ "The report separates one-time index-build duration from per-task retrieval duration and shows how the fixed build cost is amortized across repeated tasks. " +
106
+ `Deterministic fake-agent correctness is available for ${summary.agentCorrectnessAvailableCount} of ${summary.agentSideCount} task sides and fake-agent token totals for ${summary.agentTotalTokensAvailableCount}; ` +
107
+ "these are simulated harness evidence, not real-model or provider measurements. " +
108
+ "Estimated context-token values are context-size estimates, not provider token usage.",
109
+ recommendedNextStep: run.status === "completed"
110
+ ? "Review per-task measurements and cumulative component costs for each project; the variants are reported side by side and are not ranked."
111
+ : "Inspect unavailable metrics, warnings, and failures before drawing conclusions from this run.",
112
+ };
113
+ }
98
114
  if (run.pluginId === "context-strategy-comparison" && (contextStrategyComparisonV043?.summary.strategyCount ?? 0) > 0) {
99
115
  return {
100
116
  summary: `Stage-context evidence was recorded for ${contextStrategyComparisonV043.summary.strategyCount} strategy executions. Review each strategy independently; this report does not calculate a composite ranking or winning strategy.`,
@@ -0,0 +1,91 @@
1
+ import { WARM_INDEX_REUSE_REPORT_SCHEMA_VERSION, } from "./warmIndexReuseReportModel.js";
2
+ const WARM_INDEX_REUSE_PLUGIN_ID = "warm-index-reuse";
3
+ const COST_MODEL = [
4
+ "Index construction is a one-time cost per benchmark project; task retrieval is the repeated warm cost.",
5
+ "Cold start: for the first task in a project, the cumulative warm component duration is the one-time index build plus the first retrieval.",
6
+ "Warm reuse: later tasks do not rebuild the index; their warm operation duration is retrieval only.",
7
+ "Amortized index build duration after N tasks is the index build duration divided by N.",
8
+ "Raw cumulative duration sums one raw-context construction per task; warm cumulative component duration includes one index build plus each warm retrieval once.",
9
+ "Cumulative durations are sums of measured components, not wall-clock or end-to-end latency, and are not presented as a speedup.",
10
+ ];
11
+ const LIMITATIONS = [
12
+ "Context estimated-token counts use the existing character-based estimate and are not provider billing telemetry.",
13
+ "Agent evidence comes from the deterministic fake agent only; real agents such as Codex or Claude are not run by warm-index-reuse in v0.5.0.",
14
+ "Fake-agent correctness is deterministic benchmark-harness evidence from the existing answer-key scorer, not a measure of real-model correctness.",
15
+ "Fake-agent token totals are simulated harness telemetry, not provider billing telemetry; estimated context tokens are a separate character-based context-size estimate.",
16
+ "Component-duration sums are not equivalent to provider end-to-end latency.",
17
+ "Failed or missing prefix measurements make cumulative metrics unavailable; missing values are never treated as zero.",
18
+ "No composite score, winner, ranking, or break-even task is calculated.",
19
+ ];
20
+ /**
21
+ * Presents the warm-index metrics already calculated on the run. It performs no amortization,
22
+ * cumulative, or availability calculation of its own.
23
+ */
24
+ export function buildWarmIndexReuseReport(run) {
25
+ if (run.pluginId !== WARM_INDEX_REUSE_PLUGIN_ID)
26
+ return null;
27
+ const candidate = run;
28
+ const executions = candidate.projectExecutions ?? [];
29
+ const metricProjects = candidate.warmIndexMetrics?.projects ?? [];
30
+ const agentProjects = candidate.agentEvidence;
31
+ if (executions.length !== metricProjects.length) {
32
+ throw new Error("Invalid warm-index report source: project executions and metrics are inconsistent.");
33
+ }
34
+ const projects = executions.map((execution, projectIndex) => {
35
+ const metrics = metricProjects[projectIndex];
36
+ if (metrics.benchmarkProject !== execution.benchmarkProject ||
37
+ metrics.tasks.length !== execution.tasks.length ||
38
+ metrics.tasks.some((task, taskIndex) => task.caseId !== execution.tasks[taskIndex].caseId)) {
39
+ throw new Error("Invalid warm-index report source: project executions and metrics are inconsistent.");
40
+ }
41
+ return {
42
+ benchmarkProject: execution.benchmarkProject,
43
+ sessionKey: execution.sessionKey,
44
+ status: execution.status,
45
+ sessionPrepared: metrics.sessionPrepared,
46
+ indexBuildDurationMs: metrics.indexBuildDurationMs,
47
+ taskCount: metrics.tasks.length,
48
+ tasks: metrics.tasks.map((task, taskIndex) => {
49
+ const agents = agentProjects?.[projectIndex]?.tasks[taskIndex];
50
+ return {
51
+ taskOrdinal: task.taskOrdinal,
52
+ caseId: task.caseId,
53
+ rawStatus: execution.tasks[taskIndex].rawStatus,
54
+ warmStatus: execution.tasks[taskIndex].warmStatus,
55
+ raw: task.raw,
56
+ warm: task.warm,
57
+ rawAgent: toReportAgent(agents?.raw ?? null),
58
+ warmAgent: toReportAgent(agents?.warm ?? null),
59
+ };
60
+ }),
61
+ };
62
+ });
63
+ const sides = projects.flatMap((project) => project.tasks.flatMap((task) => [task.raw, task.warm]));
64
+ return {
65
+ schemaVersion: WARM_INDEX_REUSE_REPORT_SCHEMA_VERSION,
66
+ summary: {
67
+ projectCount: projects.length,
68
+ taskCount: projects.reduce((sum, project) => sum + project.taskCount, 0),
69
+ preparedSessionProjectCount: projects.filter((project) => project.sessionPrepared).length,
70
+ incompleteProjectCount: projects.filter((project) => project.status !== "completed").length,
71
+ agentSideCount: sides.length,
72
+ agentCorrectnessAvailableCount: sides.filter((side) => side.agentCorrectness.availability === "available").length,
73
+ agentTotalTokensAvailableCount: sides.filter((side) => side.agentTotalTokens.availability === "available").length,
74
+ },
75
+ costModel: [...COST_MODEL],
76
+ limitations: [...LIMITATIONS],
77
+ projects,
78
+ };
79
+ }
80
+ function toReportAgent(agent) {
81
+ if (!agent)
82
+ return null;
83
+ return {
84
+ status: agent.status,
85
+ passed: agent.correctness.passed,
86
+ tokenUsageSource: agent.tokenUsage.source,
87
+ tokenUsageReliability: agent.tokenUsage.reliability,
88
+ warnings: [...agent.warnings],
89
+ errors: [...agent.errors],
90
+ };
91
+ }
@@ -6,6 +6,9 @@ export * from "./contextStrategyComparisonV043ReportModel.js";
6
6
  export { buildContextStrategyComparisonV043Report } from "./buildContextStrategyComparisonV043Report.js";
7
7
  export { renderContextStrategyComparisonV043Html } from "./renderContextStrategyComparisonV043Html.js";
8
8
  export { renderPluginExperimentReportText } from "./renderPluginExperimentReportText.js";
9
+ export * from "./warmIndexReuseReportModel.js";
10
+ export { buildWarmIndexReuseReport } from "./buildWarmIndexReuseReport.js";
11
+ export { renderWarmIndexReuseHtml } from "./renderWarmIndexReuseHtml.js";
9
12
  export * from "./contextIntegrityReportModel.js";
10
13
  export { buildContextIntegrityReport } from "./buildContextIntegrityReport.js";
11
14
  export { renderContextIntegrityJsonReport } from "./renderContextIntegrityJsonReport.js";
@@ -1,4 +1,5 @@
1
1
  import { renderContextStrategyComparisonV043Html } from "./renderContextStrategyComparisonV043Html.js";
2
+ import { renderWarmIndexReuseHtml } from "./renderWarmIndexReuseHtml.js";
2
3
  export function renderPluginExperimentReportHtml(report) {
3
4
  return `<!DOCTYPE html>
4
5
  <html lang="en">
@@ -96,6 +97,8 @@ export function renderPluginExperimentReportHtml(report) {
96
97
 
97
98
  ${renderContextStrategyComparisonV043Html(report.contextStrategyComparisonV043)}
98
99
 
100
+ ${renderWarmIndexReuseHtml(report.warmIndexReuse)}
101
+
99
102
  <section>
100
103
  <h2>Warnings, Skips, And Failures</h2>
101
104
  ${report.findings.length ? table(["Severity", "Code", "Message", "Variant", "Case"], report.findings.map((finding) => [
@@ -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";
package/docs/METRICS.md CHANGED
@@ -137,6 +137,15 @@ Each entry below uses the same fields: **Meaning**, **Appears in** (the artifact
137
137
  Interpretation: human-readable size category.
138
138
  Caveat: coarse label; use the score and metrics for detail.
139
139
 
140
+ In the released v0.5.1 benchmark suite, the task statistics (`taskCount`, `expectedRelevantFilesAverage`, `expectedRelevantSymbolsAverage`) of the two warm-index benchmark projects are derived from the dedicated corpus `benchmarks/contracts/warm-index-benchmark-cases.json`: the averages are the mean `expectedFiles` and `expectedSymbols` counts of that project's cases, rounded to two decimals. `npm run verify:benchmarks` fails if a profile disagrees with the corpus. The `benchmark-project-complexity-v1` formula, its weights, and its caps are unchanged; the current values are:
141
+
142
+ | Project | `taskCount` | `expectedRelevantFilesAverage` | `expectedRelevantSymbolsAverage` | `complexityScore` |
143
+ |---|---|---|---|---|
144
+ | `task-workflow-medium-ts` | 6 | 3.83 | 4.33 | 31 |
145
+ | `task-analytics-large-mixed` | 6 | 4.5 | 4.67 | 47 |
146
+
147
+ Task locality (`localized`, `cross-module`, `broad-change`) is benchmark metadata on each corpus case. It is not a metric, score, or ranking, and it does not enter the complexity formula.
148
+
140
149
  ## Prompt complexity metrics
141
150
 
142
151
  - `promptChars`
@@ -441,3 +450,69 @@ Implemented in `src/evaluation/stageContextMetrics` and composed once per run by
441
450
  - Determinism and fixture self-immutability
442
451
  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
452
  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.
453
+
454
+ ## Warm-index reuse metrics
455
+
456
+ Introduced in 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.
457
+
458
+ The released v0.5.1 expanded benchmark suite adds no warm-index metric and changes no formula: over the dedicated 12-case corpus the same metrics run through task ordinal 6 in each project, and amortization and cumulative sums restart for each project. No token-savings percentage, break-even task, winner, speedup, ranking, or provider billing metric is calculated.
459
+
460
+ **Availability contract.** Every warm-index metric carries `availability`, `value`, `unit`, `source`, `reason`, and `tokenCountMethod`:
461
+
462
+ - `available`: `value` is a finite number and `reason` is `null`. Zero is a valid available value.
463
+ - `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.
464
+ - `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.
465
+
466
+ 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`).
467
+
468
+ **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.
469
+
470
+ **Direct measurements (per task):**
471
+
472
+ - Raw context characters / warm retrieved context characters (`characters`, `measured`)
473
+ Meaning: characters in the raw full-file context and in the retrieved my-dev-kit context.
474
+ 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).
475
+ - Raw estimated context tokens / warm estimated context tokens (`estimated-tokens`, `estimated-chars-div-4`)
476
+ Meaning: the character-based context-size estimate, `ceil(characters / 4)`.
477
+ Caveat: a context-size estimate, not provider token usage and not fake-agent token usage.
478
+ - Raw context-construction duration (`ms`, `measured`)
479
+ Meaning: time to build the raw full-file context for the task. It is not agent duration.
480
+ - Warm retrieval duration (`ms`, `measured`)
481
+ Meaning: search, lookup, slice, and source against the already prepared index. It never includes the index build.
482
+ Caveat: a skipped retrieval (no usable context) still reports its measured duration and sizes; its outcome remains `skipped`.
483
+ - Index build duration (per project, `ms`, `measured`)
484
+ Meaning: the measured duration of the project's single my-dev-kit `index` command.
485
+ 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.
486
+
487
+ **Derived measurements (per task):**
488
+
489
+ - Amortized index build duration (`ms`, `derived`)
490
+ Formula: `amortizedIndexBuildDurationMs(N) = indexBuildDurationMs / N` for task ordinal `N`, without rounding.
491
+ Caveat: available only when the project prepared a valid index session. A failed or never-attempted index is not amortized.
492
+ - Cumulative raw component duration (`ms`, `measured`)
493
+ Formula: sum of raw context-construction durations for tasks `1..N`.
494
+ - Cumulative warm component duration (`ms`, `derived`)
495
+ 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.
496
+ 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.
497
+ - Cumulative raw / warm estimated context tokens (`estimated-tokens`, `estimated-chars-div-4`)
498
+ 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.
499
+ Caveat: if different token-count methods appear within one prefix, the cumulative value is unavailable rather than summed.
500
+
501
+ **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.
502
+
503
+ **Fake-agent evidence (per task side):**
504
+
505
+ 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.
506
+
507
+ - Fake-agent correctness score (`score`, `agent`)
508
+ Meaning: `correctnessScore` from the existing benchmark answer-key scorer (see "Experiment and run metrics" above).
509
+ 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.
510
+ - Fake-agent total tokens (`tokens`, `agent`)
511
+ 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.
512
+ 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.
513
+ - Cumulative fake-agent total tokens (`tokens`, `agent`)
514
+ Formula: sum of fake-agent total tokens for tasks `1..N` on that side, under the strict-prefix rule.
515
+
516
+ **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`.
517
+
518
+ **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.