@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.
- package/CHANGELOG.md +28 -0
- package/README.md +20 -2
- package/benchmarks/contracts/benchmark-project-profiles.json +14 -14
- package/benchmarks/contracts/warm-index-benchmark-cases.json +764 -0
- package/dist/src/cli/help.js +19 -4
- package/dist/src/commands/runExperimentDescribeCommand.js +11 -2
- package/dist/src/commands/runExperimentListCommand.js +3 -0
- package/dist/src/commands/runExperimentRunCommand.js +27 -7
- package/dist/src/evaluation/benchmarkMetadata.js +191 -1
- package/dist/src/evaluation/projectComplexity.js +1 -1
- package/dist/src/evaluation/readEvaluationCases.js +7 -1
- package/dist/src/evaluation/runMyDevKitRetrieval.js +82 -53
- package/dist/src/evaluation/types.js +1 -1
- package/dist/src/experiments/defaultRegistry.js +2 -0
- package/dist/src/experiments/plugins/index.js +1 -0
- package/dist/src/experiments/plugins/warmIndexReuse/config.js +58 -0
- package/dist/src/experiments/plugins/warmIndexReuse/execution.js +158 -0
- package/dist/src/experiments/plugins/warmIndexReuse/executionArtifact.js +77 -0
- package/dist/src/experiments/plugins/warmIndexReuse/fakeAgentEvaluation.js +118 -0
- package/dist/src/experiments/plugins/warmIndexReuse/index.js +8 -0
- package/dist/src/experiments/plugins/warmIndexReuse/metrics.js +257 -0
- package/dist/src/experiments/plugins/warmIndexReuse/plugin.js +212 -0
- package/dist/src/experiments/plugins/warmIndexReuse/selection.js +85 -0
- package/dist/src/experiments/plugins/warmIndexReuse/warmIndexSession.js +43 -0
- package/dist/src/plots/buildWarmIndexPlotData.js +112 -0
- package/dist/src/plots/index.js +1 -0
- package/dist/src/plots/writePlotArtifacts.js +8 -1
- package/dist/src/report/experiments/buildPluginExperimentReport.js +18 -2
- package/dist/src/report/experiments/buildWarmIndexReuseReport.js +91 -0
- package/dist/src/report/experiments/index.js +3 -0
- package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +3 -0
- package/dist/src/report/experiments/renderPluginExperimentReportText.js +73 -0
- package/dist/src/report/experiments/renderWarmIndexReuseHtml.js +150 -0
- package/dist/src/report/experiments/warmIndexReuseReportModel.js +1 -0
- package/docs/METRICS.md +75 -0
- package/package.json +1 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Warm-index metric owner. Every warm-index formula (amortization, cumulative
|
|
3
|
+
// prefix sums, availability) is calculated here exactly once, as a pure
|
|
4
|
+
// transform over the bounded Prompt 2 execution summaries. Reports render
|
|
5
|
+
// these results; they never recalculate them.
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
export const WARM_INDEX_METRICS_SCHEMA_VERSION = "my-dev-kit-lab-warm-index-metrics-v1";
|
|
8
|
+
const NO_AGENT_EVIDENCE_REASON = "No fake-agent evaluation evidence was supplied for this task.";
|
|
9
|
+
const AGENT_NOT_RUN_REASON = "The fake agent was not run because this side produced no context evidence.";
|
|
10
|
+
export function availableMetric(value, unit, source, tokenCountMethod = null) {
|
|
11
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
12
|
+
return unavailableMetric(unit, source, `Invalid input: expected a finite number but received ${String(value)}.`, tokenCountMethod);
|
|
13
|
+
}
|
|
14
|
+
return { availability: "available", value, unit, source, reason: null, tokenCountMethod };
|
|
15
|
+
}
|
|
16
|
+
export function unavailableMetric(unit, source, reason, tokenCountMethod = null) {
|
|
17
|
+
return { availability: "unavailable", value: null, unit, source, reason: requireReason(reason), tokenCountMethod };
|
|
18
|
+
}
|
|
19
|
+
export function notApplicableMetric(unit, source, reason) {
|
|
20
|
+
return { availability: "not-applicable", value: null, unit, source, reason: requireReason(reason), tokenCountMethod: null };
|
|
21
|
+
}
|
|
22
|
+
function requireReason(reason) {
|
|
23
|
+
if (!reason.trim()) {
|
|
24
|
+
throw new Error("Unavailable and not-applicable warm-index metrics require a reason.");
|
|
25
|
+
}
|
|
26
|
+
return reason;
|
|
27
|
+
}
|
|
28
|
+
/** Strict-prefix running sum: once one ordinal lacks evidence, every later ordinal stays unavailable. */
|
|
29
|
+
class PrefixSum {
|
|
30
|
+
unit;
|
|
31
|
+
source;
|
|
32
|
+
total;
|
|
33
|
+
brokenReason;
|
|
34
|
+
method = null;
|
|
35
|
+
constructor(unit, source, start) {
|
|
36
|
+
this.unit = unit;
|
|
37
|
+
this.source = source;
|
|
38
|
+
this.total = "value" in start ? start.value : 0;
|
|
39
|
+
this.brokenReason = "unavailableReason" in start ? start.unavailableReason : null;
|
|
40
|
+
}
|
|
41
|
+
add(direct, label) {
|
|
42
|
+
if (this.brokenReason === null) {
|
|
43
|
+
if (direct.availability !== "available" || direct.value === null) {
|
|
44
|
+
this.brokenReason = `${label} evidence is missing or invalid; the cumulative prefix is incomplete from this task onward.`;
|
|
45
|
+
}
|
|
46
|
+
else if (this.method !== null && direct.tokenCountMethod !== this.method) {
|
|
47
|
+
this.brokenReason = `${label} uses token-count method ${String(direct.tokenCountMethod)} but earlier tasks used ${this.method}; mixed methods are not summed.`;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
this.method = direct.tokenCountMethod;
|
|
51
|
+
this.total += direct.value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return this.brokenReason === null
|
|
55
|
+
? availableMetric(this.total, this.unit, this.source, this.method)
|
|
56
|
+
: unavailableMetric(this.unit, this.source, this.brokenReason, this.method);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function indexBuildMetric(project) {
|
|
60
|
+
if (project.buildDurationMs === null) {
|
|
61
|
+
return unavailableMetric("ms", "measured", project.errors.length > 0
|
|
62
|
+
? "No index setup was attempted because the project group was structurally inconsistent."
|
|
63
|
+
: "No index build evidence was recorded for this project.");
|
|
64
|
+
}
|
|
65
|
+
// A failed build is still a measured command duration; it is only excluded from warm costs below.
|
|
66
|
+
return availableMetric(project.buildDurationMs, "ms", "measured");
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Fake-agent correctness and total tokens come only from bounded fake-agent evidence. Missing
|
|
70
|
+
* token telemetry stays unavailable; estimated context tokens are never substituted.
|
|
71
|
+
*/
|
|
72
|
+
function agentSideMetrics(evidence) {
|
|
73
|
+
if (evidence === undefined || evidence === null) {
|
|
74
|
+
const reason = evidence === undefined ? NO_AGENT_EVIDENCE_REASON : AGENT_NOT_RUN_REASON;
|
|
75
|
+
return {
|
|
76
|
+
agentCorrectness: unavailableMetric("score", "agent", reason),
|
|
77
|
+
agentTotalTokens: unavailableMetric("tokens", "agent", reason),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const correctnessScore = evidence.correctness.score;
|
|
81
|
+
const agentCorrectness = evidence.correctness.available && correctnessScore !== null
|
|
82
|
+
? availableMetric(correctnessScore, "score", "agent")
|
|
83
|
+
: unavailableMetric("score", "agent", `Fake-agent output was not scoreable (status ${evidence.status})${evidence.errors[0] ? `: ${evidence.errors[0]}` : "."}`);
|
|
84
|
+
const agentTotalTokens = evidence.tokenUsage.totalTokens !== null
|
|
85
|
+
? availableMetric(evidence.tokenUsage.totalTokens, "tokens", "agent")
|
|
86
|
+
: unavailableMetric("tokens", "agent", `The fake agent did not report total tokens (token usage source ${evidence.tokenUsage.source}).`);
|
|
87
|
+
return { agentCorrectness, agentTotalTokens };
|
|
88
|
+
}
|
|
89
|
+
function assertAgentEvidenceAligned(project, evidence) {
|
|
90
|
+
if (!evidence)
|
|
91
|
+
return;
|
|
92
|
+
const aligned = evidence.benchmarkProject === project.benchmarkProject &&
|
|
93
|
+
evidence.tasks.length === project.tasks.length &&
|
|
94
|
+
evidence.tasks.every((task, index) => task.caseId === project.tasks[index].caseId);
|
|
95
|
+
if (!aligned) {
|
|
96
|
+
throw new Error(`Fake-agent evidence is not aligned with the execution summary for ${project.benchmarkProject}.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export function calculateWarmIndexProjectMetrics(project, agentEvidence) {
|
|
100
|
+
assertAgentEvidenceAligned(project, agentEvidence);
|
|
101
|
+
const indexBuildDurationMs = indexBuildMetric(project);
|
|
102
|
+
const buildUsable = project.sessionPrepared && indexBuildDurationMs.availability === "available";
|
|
103
|
+
const noSessionReason = project.sessionPrepared
|
|
104
|
+
? "Index build duration is unavailable, so warm index cost cannot be calculated."
|
|
105
|
+
: "No valid warm index session was prepared for this project, so the index build cost is not charged to warm tasks.";
|
|
106
|
+
const rawDuration = new PrefixSum("ms", "measured", { value: 0 });
|
|
107
|
+
const rawTokens = new PrefixSum("estimated-tokens", "estimated-chars-div-4", { value: 0 });
|
|
108
|
+
// The one-time build is charged exactly once, as the starting value of the warm prefix.
|
|
109
|
+
const warmDuration = new PrefixSum("ms", "derived", buildUsable ? { value: indexBuildDurationMs.value } : { unavailableReason: noSessionReason });
|
|
110
|
+
const warmTokens = new PrefixSum("estimated-tokens", "estimated-chars-div-4", { value: 0 });
|
|
111
|
+
// Fake-agent token totals accumulate separately from estimated context tokens.
|
|
112
|
+
const rawAgentTokens = new PrefixSum("tokens", "agent", { value: 0 });
|
|
113
|
+
const warmAgentTokens = new PrefixSum("tokens", "agent", { value: 0 });
|
|
114
|
+
const tasks = project.tasks.map((task, index) => {
|
|
115
|
+
const taskOrdinal = index + 1;
|
|
116
|
+
const label = (side) => `Task ${taskOrdinal} (${task.caseId}) ${side}`;
|
|
117
|
+
const raw = rawDirect(task);
|
|
118
|
+
const warm = warmDirect(task);
|
|
119
|
+
const taskAgentEvidence = agentEvidence?.tasks[index];
|
|
120
|
+
const rawAgent = agentSideMetrics(agentEvidence ? taskAgentEvidence?.raw ?? null : undefined);
|
|
121
|
+
const warmAgent = agentSideMetrics(agentEvidence ? taskAgentEvidence?.warm ?? null : undefined);
|
|
122
|
+
return {
|
|
123
|
+
caseId: task.caseId,
|
|
124
|
+
taskOrdinal,
|
|
125
|
+
raw: {
|
|
126
|
+
...raw,
|
|
127
|
+
cumulativeDurationMs: rawDuration.add(raw.operationDurationMs, label("raw duration")),
|
|
128
|
+
cumulativeEstimatedContextTokens: rawTokens.add(raw.contextEstimatedTokens, label("raw estimated context tokens")),
|
|
129
|
+
...rawAgent,
|
|
130
|
+
cumulativeAgentTotalTokens: rawAgentTokens.add(rawAgent.agentTotalTokens, label("raw fake-agent total tokens")),
|
|
131
|
+
},
|
|
132
|
+
warm: {
|
|
133
|
+
...warm,
|
|
134
|
+
// Amortized build cost after N tasks = build duration / N.
|
|
135
|
+
amortizedIndexBuildDurationMs: buildUsable
|
|
136
|
+
? availableMetric(indexBuildDurationMs.value / taskOrdinal, "ms", "derived")
|
|
137
|
+
: unavailableMetric("ms", "derived", noSessionReason),
|
|
138
|
+
cumulativeComponentDurationMs: warmDuration.add(warm.retrievalDurationMs, label("warm retrieval duration")),
|
|
139
|
+
cumulativeEstimatedContextTokens: warmTokens.add(warm.contextEstimatedTokens, label("warm estimated context tokens")),
|
|
140
|
+
...warmAgent,
|
|
141
|
+
cumulativeAgentTotalTokens: warmAgentTokens.add(warmAgent.agentTotalTokens, label("warm fake-agent total tokens")),
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
benchmarkProject: project.benchmarkProject,
|
|
147
|
+
sessionKey: project.sessionKey,
|
|
148
|
+
sessionPrepared: project.sessionPrepared,
|
|
149
|
+
indexBuildDurationMs,
|
|
150
|
+
tasks,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function rawDirect(task) {
|
|
154
|
+
const raw = task.rawBaseline;
|
|
155
|
+
if (!raw) {
|
|
156
|
+
const reason = `No raw baseline evidence was produced (raw status ${task.rawStatus}).`;
|
|
157
|
+
return {
|
|
158
|
+
contextCharacters: unavailableMetric("characters", "measured", reason),
|
|
159
|
+
contextEstimatedTokens: unavailableMetric("estimated-tokens", "estimated-chars-div-4", reason),
|
|
160
|
+
operationDurationMs: unavailableMetric("ms", "measured", reason),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
contextCharacters: availableMetric(raw.totalChars, "characters", "measured"),
|
|
165
|
+
contextEstimatedTokens: availableMetric(raw.totalEstimatedTokens, "estimated-tokens", "estimated-chars-div-4", raw.tokenCountMethod),
|
|
166
|
+
operationDurationMs: availableMetric(raw.durationMs, "ms", "measured"),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function warmDirect(task) {
|
|
170
|
+
const warm = task.warmRetrieval;
|
|
171
|
+
if (!warm) {
|
|
172
|
+
const reason = `No warm retrieval was run for this task (warm status ${task.warmStatus}).`;
|
|
173
|
+
return {
|
|
174
|
+
contextCharacters: unavailableMetric("characters", "measured", reason),
|
|
175
|
+
contextEstimatedTokens: unavailableMetric("estimated-tokens", "estimated-chars-div-4", reason),
|
|
176
|
+
retrievalDurationMs: unavailableMetric("ms", "measured", reason),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// A skipped retrieval still carries measured values; its skipped status stays on the outcome.
|
|
180
|
+
return {
|
|
181
|
+
contextCharacters: availableMetric(warm.totalChars, "characters", "measured"),
|
|
182
|
+
contextEstimatedTokens: availableMetric(warm.totalEstimatedTokens, "estimated-tokens", "estimated-chars-div-4", warm.tokenCountMethod),
|
|
183
|
+
retrievalDurationMs: availableMetric(warm.durationMs, "ms", "measured"),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/** Pure, order-preserving calculation; ordinals and cumulative state restart for every project. */
|
|
187
|
+
export function calculateWarmIndexMetrics(projects, agentEvidence) {
|
|
188
|
+
if (agentEvidence && agentEvidence.length !== projects.length) {
|
|
189
|
+
throw new Error("Fake-agent evidence is not aligned with the execution summaries.");
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
schemaVersion: WARM_INDEX_METRICS_SCHEMA_VERSION,
|
|
193
|
+
projects: projects.map((project, index) => calculateWarmIndexProjectMetrics(project, agentEvidence?.[index])),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/** Generic outcome metrics: only available numeric values are emitted, never placeholder zeros. */
|
|
197
|
+
export function toRawOutcomeMetrics(task, variantId) {
|
|
198
|
+
return toExperimentMetrics(task.caseId, variantId, [
|
|
199
|
+
{ id: "context-character-count", name: "Context characters", description: "Characters in the raw full-file context.", metric: task.raw.contextCharacters },
|
|
200
|
+
{ id: "context-estimated-token-count", name: "Estimated context tokens", description: "Character-based estimate of raw context tokens; not provider token usage.", metric: task.raw.contextEstimatedTokens },
|
|
201
|
+
{ id: "operation-duration-ms", name: "Operation duration", description: "Measured raw full-file context construction duration.", metric: task.raw.operationDurationMs },
|
|
202
|
+
{ id: "cumulative-component-duration-ms", name: "Cumulative component duration", description: "Sum of raw context construction durations for tasks 1..N in this project.", metric: task.raw.cumulativeDurationMs },
|
|
203
|
+
{ id: "cumulative-context-estimated-token-count", name: "Cumulative estimated context tokens", description: "Sum of raw estimated context tokens for tasks 1..N in this project; not provider token usage.", metric: task.raw.cumulativeEstimatedContextTokens },
|
|
204
|
+
...agentSpecs(task.raw),
|
|
205
|
+
]);
|
|
206
|
+
}
|
|
207
|
+
export function toWarmOutcomeMetrics(task, variantId) {
|
|
208
|
+
return toExperimentMetrics(task.caseId, variantId, [
|
|
209
|
+
{ id: "context-character-count", name: "Context characters", description: "Characters in the retrieved my-dev-kit context.", metric: task.warm.contextCharacters },
|
|
210
|
+
{ id: "context-estimated-token-count", name: "Estimated context tokens", description: "Character-based estimate of retrieved context tokens; not provider token usage.", metric: task.warm.contextEstimatedTokens },
|
|
211
|
+
{ id: "operation-duration-ms", name: "Operation duration", description: "Measured retrieval-only duration (search, lookup, slice, source); excludes index build.", metric: task.warm.retrievalDurationMs },
|
|
212
|
+
{ id: "cumulative-component-duration-ms", name: "Cumulative component duration", description: "One index build plus retrieval durations for tasks 1..N in this project; a component sum, not wall-clock latency.", metric: task.warm.cumulativeComponentDurationMs },
|
|
213
|
+
{ id: "cumulative-context-estimated-token-count", name: "Cumulative estimated context tokens", description: "Sum of retrieved estimated context tokens for tasks 1..N in this project; not provider token usage.", metric: task.warm.cumulativeEstimatedContextTokens },
|
|
214
|
+
{ id: "amortized-index-build-duration-ms", name: "Amortized index build duration", description: "Index build duration divided by this task's ordinal within its project.", metric: task.warm.amortizedIndexBuildDurationMs },
|
|
215
|
+
...agentSpecs(task.warm),
|
|
216
|
+
]);
|
|
217
|
+
}
|
|
218
|
+
function agentSpecs(side) {
|
|
219
|
+
return [
|
|
220
|
+
{ id: "agent-correctness-score", name: "Fake-agent correctness score", description: "Deterministic fake-agent correctness from the existing benchmark answer-key scorer; not real-model correctness.", metric: side.agentCorrectness },
|
|
221
|
+
{ id: "agent-total-tokens", name: "Fake-agent total tokens", description: "Simulated fake-agent harness token total; not provider billing telemetry.", metric: side.agentTotalTokens },
|
|
222
|
+
{ id: "cumulative-agent-total-tokens", name: "Cumulative fake-agent total tokens", description: "Sum of simulated fake-agent token totals for tasks 1..N in this project; not provider billing telemetry.", metric: side.cumulativeAgentTotalTokens },
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
function toExperimentMetrics(caseId, variantId, specs) {
|
|
226
|
+
return specs
|
|
227
|
+
.filter((spec) => spec.metric.availability === "available" && spec.metric.value !== null)
|
|
228
|
+
.map((spec) => ({
|
|
229
|
+
id: spec.id,
|
|
230
|
+
name: spec.name,
|
|
231
|
+
value: spec.metric.value,
|
|
232
|
+
unit: spec.metric.unit,
|
|
233
|
+
description: spec.description,
|
|
234
|
+
variantId,
|
|
235
|
+
caseId,
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
/** Run-level metrics are limited to direct counts. */
|
|
239
|
+
export function toRunLevelMetrics(metrics) {
|
|
240
|
+
return [
|
|
241
|
+
{ id: "warm-index-project-count", name: "Warm-index project count", value: metrics.projects.length, unit: "count", description: "Benchmark-project groups in this run." },
|
|
242
|
+
{
|
|
243
|
+
id: "warm-index-task-count",
|
|
244
|
+
name: "Warm-index task count",
|
|
245
|
+
value: metrics.projects.reduce((sum, project) => sum + project.tasks.length, 0),
|
|
246
|
+
unit: "count",
|
|
247
|
+
description: "Tasks evaluated across all benchmark-project groups.",
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: "warm-index-session-prepared-project-count",
|
|
251
|
+
name: "Prepared warm-index session count",
|
|
252
|
+
value: metrics.projects.filter((project) => project.sessionPrepared).length,
|
|
253
|
+
unit: "count",
|
|
254
|
+
description: "Benchmark-project groups for which exactly one warm index session was prepared.",
|
|
255
|
+
},
|
|
256
|
+
];
|
|
257
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveWithinRoot } from "../../../core/pathSafety.js";
|
|
4
|
+
import { summarizeExperimentRun } from "../../results.js";
|
|
5
|
+
import { defaultWarmIndexReuseConfig, validateWarmIndexReuseConfig, warmIndexReuseConfigDefinition, } from "./config.js";
|
|
6
|
+
import { aggregateStatus, executeWarmIndexReuse, } from "./execution.js";
|
|
7
|
+
import { buildWarmIndexExecutionArtifact, WARM_INDEX_EXECUTION_ARTIFACT_FILE, } from "./executionArtifact.js";
|
|
8
|
+
import { calculateWarmIndexMetrics, toRawOutcomeMetrics, toRunLevelMetrics, toWarmOutcomeMetrics, } from "./metrics.js";
|
|
9
|
+
import { selectWarmIndexCases } from "./selection.js";
|
|
10
|
+
import { evaluateWarmIndexFakeAgents, } from "./fakeAgentEvaluation.js";
|
|
11
|
+
export const RAW_FULL_FILE_VARIANT_ID = "raw-full-file";
|
|
12
|
+
export const WARM_INDEX_REUSE_VARIANT_ID = "warm-index-reuse";
|
|
13
|
+
export const warmIndexReuseMetadata = {
|
|
14
|
+
id: "warm-index-reuse",
|
|
15
|
+
name: "Warm Index Reuse",
|
|
16
|
+
description: "Measure repeated task execution where one my-dev-kit index is built once per benchmark project and reused across multiple tasks, with a matched raw-full-file baseline per task.",
|
|
17
|
+
schemaVersion: "1.0.0",
|
|
18
|
+
status: "experimental",
|
|
19
|
+
supportedTargets: ["self", "external-local"],
|
|
20
|
+
supportedOutputs: ["json", "html", "text", "plot", "artifact"],
|
|
21
|
+
};
|
|
22
|
+
const WARM_INDEX_VARIANTS = [
|
|
23
|
+
{
|
|
24
|
+
id: RAW_FULL_FILE_VARIANT_ID,
|
|
25
|
+
name: "Raw full file",
|
|
26
|
+
description: "Every file matched by the case's raw include globs, read once per task.",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: WARM_INDEX_REUSE_VARIANT_ID,
|
|
30
|
+
name: "Warm index reuse",
|
|
31
|
+
description: "my-dev-kit retrieval against one index prepared once per benchmark project.",
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
export const warmIndexReusePlugin = {
|
|
35
|
+
metadata: warmIndexReuseMetadata,
|
|
36
|
+
defaultConfig: defaultWarmIndexReuseConfig,
|
|
37
|
+
configDefinition: warmIndexReuseConfigDefinition,
|
|
38
|
+
supportedVariants: WARM_INDEX_VARIANTS.map((variant) => variant.id),
|
|
39
|
+
validateConfig: validateWarmIndexReuseConfig,
|
|
40
|
+
async run(context) {
|
|
41
|
+
const startedAt = context.startedAt.toISOString();
|
|
42
|
+
const cases = selectWarmIndexCases(readCasesInput(context.inputs), context.config);
|
|
43
|
+
const projectProfiles = readProjectProfilesInput(context.inputs);
|
|
44
|
+
const outDir = context.outputRoot ?? path.resolve(context.toolRoot, context.config.outDir);
|
|
45
|
+
await mkdir(outDir, { recursive: true });
|
|
46
|
+
const projects = await executeWarmIndexReuse({
|
|
47
|
+
cases,
|
|
48
|
+
kitCommand: context.config.kitCommand,
|
|
49
|
+
outputRoot: outDir,
|
|
50
|
+
});
|
|
51
|
+
// Fake-agent evaluation runs after execution evidence exists and never repeats it.
|
|
52
|
+
const agentEvidence = await evaluateWarmIndexFakeAgents({
|
|
53
|
+
projects,
|
|
54
|
+
cases,
|
|
55
|
+
projectProfiles,
|
|
56
|
+
outputRoot: outDir,
|
|
57
|
+
cwd: context.target.targetRoot,
|
|
58
|
+
env: readEnvInput(context.inputs),
|
|
59
|
+
});
|
|
60
|
+
const artifact = buildWarmIndexExecutionArtifact({
|
|
61
|
+
runId: context.runId,
|
|
62
|
+
pluginId: warmIndexReuseMetadata.id,
|
|
63
|
+
projects,
|
|
64
|
+
});
|
|
65
|
+
const artifactPath = resolveWithinRoot(outDir, WARM_INDEX_EXECUTION_ARTIFACT_FILE);
|
|
66
|
+
await writeFile(artifactPath, `${JSON.stringify(artifact, null, 2)}\n`, "utf8");
|
|
67
|
+
const run = mapWarmIndexExecutionToRun({
|
|
68
|
+
runId: context.runId,
|
|
69
|
+
startedAt,
|
|
70
|
+
completedAt: new Date().toISOString(),
|
|
71
|
+
target: context.target,
|
|
72
|
+
cases,
|
|
73
|
+
projects,
|
|
74
|
+
projectSummaries: artifact.projects,
|
|
75
|
+
agentEvidence,
|
|
76
|
+
artifactPath,
|
|
77
|
+
});
|
|
78
|
+
return run;
|
|
79
|
+
},
|
|
80
|
+
summarize(result) {
|
|
81
|
+
return result.summary ?? summarizeExperimentRun(result);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
export function mapWarmIndexExecutionToRun(args) {
|
|
85
|
+
const titles = new Map(args.cases.map((evaluationCase) => [evaluationCase.id, evaluationCase.title]));
|
|
86
|
+
const warmIndexMetrics = calculateWarmIndexMetrics(args.projectSummaries, args.agentEvidence);
|
|
87
|
+
// projectSummaries (and therefore metrics) preserve project and task order of args.projects.
|
|
88
|
+
const experimentCases = args.projects.flatMap((project, projectIndex) => project.tasks.map((task, taskIndex) => {
|
|
89
|
+
const taskMetrics = warmIndexMetrics.projects[projectIndex].tasks[taskIndex];
|
|
90
|
+
const taskAgents = args.agentEvidence[projectIndex].tasks[taskIndex];
|
|
91
|
+
return {
|
|
92
|
+
id: task.caseId,
|
|
93
|
+
name: titles.get(task.caseId) ?? task.caseId,
|
|
94
|
+
outcomes: [
|
|
95
|
+
buildRawOutcome(project, task, taskMetrics, taskAgents),
|
|
96
|
+
buildWarmOutcome(project, task, taskMetrics, taskAgents),
|
|
97
|
+
],
|
|
98
|
+
metadata: { benchmarkProject: task.benchmarkProject, sessionKey: project.projectSegment },
|
|
99
|
+
};
|
|
100
|
+
}));
|
|
101
|
+
const warnings = args.projects.flatMap((project) => project.warnings.map((message) => ({
|
|
102
|
+
code: "warm-index-setup-warning",
|
|
103
|
+
message,
|
|
104
|
+
details: { benchmarkProject: project.benchmarkProject },
|
|
105
|
+
})));
|
|
106
|
+
const run = {
|
|
107
|
+
runId: args.runId,
|
|
108
|
+
pluginId: warmIndexReuseMetadata.id,
|
|
109
|
+
startedAt: args.startedAt,
|
|
110
|
+
completedAt: args.completedAt,
|
|
111
|
+
status: aggregateStatus(experimentCases.flatMap((experimentCase) => experimentCase.outcomes.map((outcome) => outcome.status))),
|
|
112
|
+
target: args.target,
|
|
113
|
+
variants: WARM_INDEX_VARIANTS.map((variant) => ({ ...variant })),
|
|
114
|
+
cases: experimentCases,
|
|
115
|
+
metrics: toRunLevelMetrics(warmIndexMetrics),
|
|
116
|
+
artifacts: [
|
|
117
|
+
{
|
|
118
|
+
id: "warm-index-execution",
|
|
119
|
+
label: "Warm index execution evidence",
|
|
120
|
+
path: args.artifactPath,
|
|
121
|
+
kind: "artifact",
|
|
122
|
+
mimeType: "application/json",
|
|
123
|
+
description: "Per-project index setup and per-task raw/warm execution summaries without context text.",
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
warnings,
|
|
127
|
+
failures: [],
|
|
128
|
+
metadata: { executionArtifactPath: args.artifactPath },
|
|
129
|
+
projectExecutions: args.projectSummaries,
|
|
130
|
+
agentEvidence: args.agentEvidence,
|
|
131
|
+
warmIndexMetrics,
|
|
132
|
+
};
|
|
133
|
+
run.summary = summarizeExperimentRun(run);
|
|
134
|
+
return run;
|
|
135
|
+
}
|
|
136
|
+
function outcomeMetadata(project, task, agent) {
|
|
137
|
+
return {
|
|
138
|
+
benchmarkProject: task.benchmarkProject,
|
|
139
|
+
sessionKey: project.projectSegment,
|
|
140
|
+
warmSessionAvailable: project.session !== undefined,
|
|
141
|
+
taskStatus: task.status,
|
|
142
|
+
// Agent evaluation status is reported separately; it does not change the execution status.
|
|
143
|
+
agentStatus: agent ? agent.status : "not-run",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function agentWarnings(agent, variantId, caseId) {
|
|
147
|
+
if (!agent)
|
|
148
|
+
return [];
|
|
149
|
+
return agent.errors.map((message) => ({
|
|
150
|
+
code: "fake-agent-evaluation-error",
|
|
151
|
+
message,
|
|
152
|
+
variantId,
|
|
153
|
+
caseId,
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
function buildRawOutcome(project, task, taskMetrics, taskAgents) {
|
|
157
|
+
return {
|
|
158
|
+
id: `${task.caseId}:${RAW_FULL_FILE_VARIANT_ID}`,
|
|
159
|
+
caseId: task.caseId,
|
|
160
|
+
variantId: RAW_FULL_FILE_VARIANT_ID,
|
|
161
|
+
status: task.rawStatus,
|
|
162
|
+
metrics: toRawOutcomeMetrics(taskMetrics, RAW_FULL_FILE_VARIANT_ID),
|
|
163
|
+
artifacts: [],
|
|
164
|
+
warnings: agentWarnings(taskAgents.raw, RAW_FULL_FILE_VARIANT_ID, task.caseId),
|
|
165
|
+
failures: toFailures(task, "raw", RAW_FULL_FILE_VARIANT_ID),
|
|
166
|
+
metadata: outcomeMetadata(project, task, taskAgents.raw),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function buildWarmOutcome(project, task, taskMetrics, taskAgents) {
|
|
170
|
+
return {
|
|
171
|
+
id: `${task.caseId}:${WARM_INDEX_REUSE_VARIANT_ID}`,
|
|
172
|
+
caseId: task.caseId,
|
|
173
|
+
variantId: WARM_INDEX_REUSE_VARIANT_ID,
|
|
174
|
+
status: task.warmStatus,
|
|
175
|
+
metrics: toWarmOutcomeMetrics(taskMetrics, WARM_INDEX_REUSE_VARIANT_ID),
|
|
176
|
+
artifacts: [],
|
|
177
|
+
warnings: [
|
|
178
|
+
...task.warnings.map((message) => ({
|
|
179
|
+
code: "warm-retrieval-warning",
|
|
180
|
+
message,
|
|
181
|
+
variantId: WARM_INDEX_REUSE_VARIANT_ID,
|
|
182
|
+
caseId: task.caseId,
|
|
183
|
+
})),
|
|
184
|
+
...agentWarnings(taskAgents.warm, WARM_INDEX_REUSE_VARIANT_ID, task.caseId),
|
|
185
|
+
],
|
|
186
|
+
failures: toFailures(task, "warm", WARM_INDEX_REUSE_VARIANT_ID),
|
|
187
|
+
metadata: outcomeMetadata(project, task, taskAgents.warm),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function toFailures(task, side, variantId) {
|
|
191
|
+
return task.errors
|
|
192
|
+
.filter((error) => error.side === side)
|
|
193
|
+
.map((error) => ({ code: error.code, message: error.message, variantId, caseId: task.caseId, recoverable: true }));
|
|
194
|
+
}
|
|
195
|
+
function readProjectProfilesInput(inputs) {
|
|
196
|
+
const value = inputs?.projectProfiles;
|
|
197
|
+
if (!Array.isArray(value)) {
|
|
198
|
+
throw new Error("Warm index reuse requires projectProfiles input for fake-agent evaluation.");
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
function readEnvInput(inputs) {
|
|
203
|
+
const env = inputs?.env;
|
|
204
|
+
return env && typeof env === "object" && !Array.isArray(env) ? env : undefined;
|
|
205
|
+
}
|
|
206
|
+
function readCasesInput(inputs) {
|
|
207
|
+
const value = inputs?.cases;
|
|
208
|
+
if (!Array.isArray(value)) {
|
|
209
|
+
throw new Error("Warm index reuse requires cases input.");
|
|
210
|
+
}
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { sanitizePathSegment } from "../../outputPaths.js";
|
|
3
|
+
/**
|
|
4
|
+
* Keeps source case order, applies case-ID then benchmark-project filters, and fails clearly for
|
|
5
|
+
* unknown requested IDs or an empty selection.
|
|
6
|
+
*/
|
|
7
|
+
export function selectWarmIndexCases(cases, filters) {
|
|
8
|
+
const knownCaseIds = new Set(cases.map((evaluationCase) => evaluationCase.id));
|
|
9
|
+
const missingCases = (filters.caseIds ?? []).filter((caseId) => !knownCaseIds.has(caseId));
|
|
10
|
+
if (missingCases.length > 0) {
|
|
11
|
+
throw new Error(`Evaluation case not found: ${missingCases.join(", ")}`);
|
|
12
|
+
}
|
|
13
|
+
const knownProjects = new Set(cases.map((evaluationCase) => evaluationCase.benchmarkProject));
|
|
14
|
+
const missingProjects = (filters.benchmarkProjects ?? []).filter((project) => !knownProjects.has(project));
|
|
15
|
+
if (missingProjects.length > 0) {
|
|
16
|
+
throw new Error(`Benchmark project not found: ${missingProjects.join(", ")}`);
|
|
17
|
+
}
|
|
18
|
+
const selected = cases
|
|
19
|
+
.filter((evaluationCase) => !filters.caseIds?.length || filters.caseIds.includes(evaluationCase.id))
|
|
20
|
+
.filter((evaluationCase) => !filters.benchmarkProjects?.length || filters.benchmarkProjects.includes(evaluationCase.benchmarkProject));
|
|
21
|
+
if (selected.length === 0) {
|
|
22
|
+
throw new Error(`No evaluation cases matched the requested filters (cases: ${(filters.caseIds ?? []).join(", ") || "any"}; ` +
|
|
23
|
+
`benchmark projects: ${(filters.benchmarkProjects ?? []).join(", ") || "any"}).`);
|
|
24
|
+
}
|
|
25
|
+
return selected;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Groups cases by benchmarkProject in first-seen order, keeping task order within a group. Cases
|
|
29
|
+
* that share a project but disagree on target root or ordered source roots make the group
|
|
30
|
+
* structurally invalid rather than silently picking one configuration.
|
|
31
|
+
*/
|
|
32
|
+
export function groupWarmIndexCases(cases) {
|
|
33
|
+
const groups = new Map();
|
|
34
|
+
for (const evaluationCase of cases) {
|
|
35
|
+
let group = groups.get(evaluationCase.benchmarkProject);
|
|
36
|
+
if (!group) {
|
|
37
|
+
group = {
|
|
38
|
+
benchmarkProject: evaluationCase.benchmarkProject,
|
|
39
|
+
projectSegment: safeOutputSegment(evaluationCase.benchmarkProject, "benchmark project"),
|
|
40
|
+
targetRoot: evaluationCase.absoluteTargetRoot,
|
|
41
|
+
sourceRoots: Object.freeze([...evaluationCase.sourceRoots]),
|
|
42
|
+
cases: [],
|
|
43
|
+
structuralErrors: [],
|
|
44
|
+
};
|
|
45
|
+
groups.set(evaluationCase.benchmarkProject, group);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
if (path.resolve(group.targetRoot) !== path.resolve(evaluationCase.absoluteTargetRoot)) {
|
|
49
|
+
group.structuralErrors.push(`Case ${evaluationCase.id} target root ${evaluationCase.absoluteTargetRoot} differs from ${group.targetRoot} for benchmark project ${group.benchmarkProject}.`);
|
|
50
|
+
}
|
|
51
|
+
if (evaluationCase.sourceRoots.join("\u0000") !== group.sourceRoots.join("\u0000")) {
|
|
52
|
+
group.structuralErrors.push(`Case ${evaluationCase.id} source roots [${evaluationCase.sourceRoots.join(", ")}] differ from [${group.sourceRoots.join(", ")}] for benchmark project ${group.benchmarkProject}.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
group.cases.push(evaluationCase);
|
|
56
|
+
}
|
|
57
|
+
const result = [...groups.values()];
|
|
58
|
+
assertUniqueSegments(result.map((group) => [group.benchmarkProject, group.projectSegment]), "benchmark project");
|
|
59
|
+
for (const group of result) {
|
|
60
|
+
assertUniqueSegments(group.cases.map((evaluationCase) => [evaluationCase.id, taskOutputSegment(evaluationCase.id)]), "case");
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
export function taskOutputSegment(caseId) {
|
|
65
|
+
return safeOutputSegment(caseId, "case");
|
|
66
|
+
}
|
|
67
|
+
// Reuses the shared experiment path-segment sanitizer and additionally rejects dot-only segments,
|
|
68
|
+
// which the sanitizer preserves and which would resolve outside the intended directory.
|
|
69
|
+
function safeOutputSegment(value, label) {
|
|
70
|
+
const segment = sanitizePathSegment(value);
|
|
71
|
+
if (/^\.+$/.test(segment)) {
|
|
72
|
+
throw new Error(`Unsafe ${label} ID for an output path segment: ${JSON.stringify(value)}`);
|
|
73
|
+
}
|
|
74
|
+
return segment;
|
|
75
|
+
}
|
|
76
|
+
function assertUniqueSegments(entries, label) {
|
|
77
|
+
const owners = new Map();
|
|
78
|
+
for (const [id, segment] of entries) {
|
|
79
|
+
const owner = owners.get(segment);
|
|
80
|
+
if (owner !== undefined && owner !== id) {
|
|
81
|
+
throw new Error(`The ${label} IDs ${owner} and ${id} map to the same output path segment "${segment}".`);
|
|
82
|
+
}
|
|
83
|
+
owners.set(segment, id);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { buildMyDevKitIndex } from "../../../evaluation/runMyDevKitRetrieval.js";
|
|
4
|
+
/**
|
|
5
|
+
* Builds the index once through the shared `buildMyDevKitIndex` owner. A build that reports
|
|
6
|
+
* success but leaves no index directory is not treated as a prepared session.
|
|
7
|
+
*/
|
|
8
|
+
export async function prepareWarmIndexSession(options) {
|
|
9
|
+
const build = await buildMyDevKitIndex(options);
|
|
10
|
+
if (!build.ok) {
|
|
11
|
+
return { ok: false, warnings: [...build.warnings], build };
|
|
12
|
+
}
|
|
13
|
+
if (!existsSync(build.indexDir)) {
|
|
14
|
+
const message = `my-dev-kit index reported success but index directory is missing: ${build.indexDir}`;
|
|
15
|
+
if (options.requireKit) {
|
|
16
|
+
throw new Error(message);
|
|
17
|
+
}
|
|
18
|
+
return { ok: false, warnings: [...build.warnings, message], build };
|
|
19
|
+
}
|
|
20
|
+
const session = Object.freeze({
|
|
21
|
+
indexDir: build.indexDir,
|
|
22
|
+
buildDurationMs: build.durationMs,
|
|
23
|
+
targetRoot: options.target.absoluteTargetRoot,
|
|
24
|
+
sourceRoots: Object.freeze([...options.target.sourceRoots]),
|
|
25
|
+
buildCommand: build.command
|
|
26
|
+
});
|
|
27
|
+
return { ok: true, session, build };
|
|
28
|
+
}
|
|
29
|
+
function normalizeRoot(root) {
|
|
30
|
+
const resolved = path.resolve(root);
|
|
31
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
32
|
+
}
|
|
33
|
+
/** Rejects reuse of a session against a different target root or source-root configuration. */
|
|
34
|
+
export function assertWarmIndexSessionMatchesTarget(session, target) {
|
|
35
|
+
if (normalizeRoot(session.targetRoot) !== normalizeRoot(target.absoluteTargetRoot)) {
|
|
36
|
+
throw new Error(`Warm index session target root ${session.targetRoot} does not match ${target.absoluteTargetRoot}.`);
|
|
37
|
+
}
|
|
38
|
+
const sameSourceRoots = session.sourceRoots.length === target.sourceRoots.length &&
|
|
39
|
+
session.sourceRoots.every((sourceRoot, index) => sourceRoot === target.sourceRoots[index]);
|
|
40
|
+
if (!sameSourceRoots) {
|
|
41
|
+
throw new Error(`Warm index session source roots [${session.sourceRoots.join(", ")}] do not match [${target.sourceRoots.join(", ")}].`);
|
|
42
|
+
}
|
|
43
|
+
}
|