@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
@@ -0,0 +1,158 @@
1
+ import path from "node:path";
2
+ import { resolveWithinRoot } from "../../../core/pathSafety.js";
3
+ import { runMyDevKitRetrievalFromIndex } from "../../../evaluation/runMyDevKitRetrieval.js";
4
+ import { runRawFullFileBaseline } from "../../../evaluation/runRawFullFileBaseline.js";
5
+ import { groupWarmIndexCases, taskOutputSegment } from "./selection.js";
6
+ import { assertWarmIndexSessionMatchesTarget, prepareWarmIndexSession, } from "./warmIndexSession.js";
7
+ /**
8
+ * Runs the warm-index lifecycle for already selected cases: one index setup attempt per valid
9
+ * benchmark-project group, then per task one raw baseline and one retrieval against that group's
10
+ * session. Never indexes per task and never retries a failed index.
11
+ */
12
+ export async function executeWarmIndexReuse(options) {
13
+ const projects = [];
14
+ for (const group of groupWarmIndexCases(options.cases)) {
15
+ const indexDir = resolveWithinRoot(options.outputRoot, path.join("indexes", group.projectSegment));
16
+ const commandsDir = resolveWithinRoot(options.outputRoot, path.join("commands", group.projectSegment));
17
+ const project = {
18
+ benchmarkProject: group.benchmarkProject,
19
+ projectSegment: group.projectSegment,
20
+ targetRoot: group.targetRoot,
21
+ sourceRoots: group.sourceRoots,
22
+ indexDir,
23
+ tasks: [],
24
+ status: "skipped",
25
+ warnings: [],
26
+ errors: [...group.structuralErrors],
27
+ };
28
+ let setupFailure;
29
+ if (group.structuralErrors.length > 0) {
30
+ setupFailure = {
31
+ side: "warm",
32
+ code: "warm-index-group-inconsistent",
33
+ message: group.structuralErrors.join(" "),
34
+ };
35
+ }
36
+ else {
37
+ try {
38
+ // requireKit: false keeps the measured index command as evidence when setup fails.
39
+ const prepared = await prepareWarmIndexSession({
40
+ target: { absoluteTargetRoot: group.targetRoot, sourceRoots: [...group.sourceRoots] },
41
+ kitCommand: options.kitCommand,
42
+ indexDir,
43
+ commandsDir: path.join(commandsDir, "index"),
44
+ requireKit: false,
45
+ });
46
+ project.build = prepared.build;
47
+ if (prepared.ok) {
48
+ project.session = prepared.session;
49
+ }
50
+ else {
51
+ project.warnings.push(...prepared.warnings);
52
+ const message = prepared.warnings.join(" ") || "my-dev-kit index setup failed.";
53
+ project.errors.push(message);
54
+ setupFailure = { side: "warm", code: "warm-index-setup-failed", message };
55
+ }
56
+ }
57
+ catch (error) {
58
+ const message = errorMessage(error);
59
+ project.errors.push(message);
60
+ setupFailure = { side: "warm", code: "warm-index-setup-failed", message };
61
+ }
62
+ }
63
+ for (const evaluationCase of group.cases) {
64
+ project.tasks.push(await executeTask({
65
+ evaluationCase,
66
+ kitCommand: options.kitCommand,
67
+ session: project.session,
68
+ setupFailure,
69
+ commandsDir: path.join(commandsDir, taskOutputSegment(evaluationCase.id)),
70
+ }));
71
+ }
72
+ project.status = aggregateStatus(project.tasks.map((task) => task.status));
73
+ projects.push(project);
74
+ }
75
+ return projects;
76
+ }
77
+ async function executeTask(args) {
78
+ const { evaluationCase } = args;
79
+ const errors = [];
80
+ const warnings = [];
81
+ let rawBaseline;
82
+ let rawStatus;
83
+ try {
84
+ rawBaseline = await runRawFullFileBaseline(evaluationCase);
85
+ rawStatus = "completed";
86
+ }
87
+ catch (error) {
88
+ rawStatus = "failed";
89
+ errors.push({ side: "raw", code: "raw-baseline-failed", message: errorMessage(error) });
90
+ }
91
+ let warmRetrieval;
92
+ let warmStatus = "failed";
93
+ if (!args.session) {
94
+ errors.push(args.setupFailure ?? { side: "warm", code: "warm-index-setup-failed", message: "No warm index session." });
95
+ }
96
+ else {
97
+ try {
98
+ assertWarmIndexSessionMatchesTarget(args.session, evaluationCase);
99
+ }
100
+ catch (error) {
101
+ errors.push({ side: "warm", code: "warm-session-target-mismatch", message: errorMessage(error) });
102
+ }
103
+ if (!errors.some((error) => error.side === "warm")) {
104
+ try {
105
+ // requireKit: false retains subordinate command evidence; a skipped result caused by a
106
+ // failed command is classified as failed below rather than as an honest skip.
107
+ warmRetrieval = await runMyDevKitRetrievalFromIndex({
108
+ evaluationCase,
109
+ kitCommand: args.kitCommand,
110
+ indexDir: args.session.indexDir,
111
+ commandsDir: args.commandsDir,
112
+ requireKit: false,
113
+ });
114
+ warnings.push(...warmRetrieval.warnings);
115
+ warmStatus = classifyWarmRetrieval(warmRetrieval);
116
+ if (warmStatus === "failed") {
117
+ errors.push({
118
+ side: "warm",
119
+ code: "warm-retrieval-failed",
120
+ message: warmRetrieval.warnings.join(" ") || "my-dev-kit retrieval command failed.",
121
+ });
122
+ }
123
+ }
124
+ catch (error) {
125
+ errors.push({ side: "warm", code: "warm-retrieval-failed", message: errorMessage(error) });
126
+ }
127
+ }
128
+ }
129
+ return {
130
+ caseId: evaluationCase.id,
131
+ benchmarkProject: evaluationCase.benchmarkProject,
132
+ status: aggregateStatus([rawStatus, warmStatus]),
133
+ rawStatus,
134
+ warmStatus,
135
+ rawBaseline,
136
+ warmRetrieval,
137
+ warnings,
138
+ errors,
139
+ };
140
+ }
141
+ function classifyWarmRetrieval(result) {
142
+ if (!result.skipped)
143
+ return "completed";
144
+ return result.commands.some((command) => !command.ok) ? "failed" : "skipped";
145
+ }
146
+ /** Same aggregation rule as the generic per-case summary: uniform statuses win, mixed is partial. */
147
+ export function aggregateStatus(statuses) {
148
+ if (statuses.length === 0)
149
+ return "skipped";
150
+ for (const status of ["completed", "skipped", "failed"]) {
151
+ if (statuses.every((value) => value === status))
152
+ return status;
153
+ }
154
+ return "partial";
155
+ }
156
+ function errorMessage(error) {
157
+ return error instanceof Error ? error.message : String(error);
158
+ }
@@ -0,0 +1,77 @@
1
+ export const WARM_INDEX_EXECUTION_ARTIFACT_FILE = "warm-index-execution.json";
2
+ export const WARM_INDEX_EXECUTION_SCHEMA_VERSION = "my-dev-kit-lab-warm-index-execution-v1";
3
+ export function summarizeCommand(command) {
4
+ return {
5
+ commandId: command.commandId,
6
+ ok: command.ok,
7
+ exitCode: command.exitCode,
8
+ durationMs: command.durationMs,
9
+ stdoutPath: command.stdoutPath,
10
+ stderrPath: command.stderrPath,
11
+ telemetryPath: command.telemetryPath,
12
+ error: command.error ?? null,
13
+ };
14
+ }
15
+ function summarizeTask(task) {
16
+ const raw = task.rawBaseline;
17
+ const warm = task.warmRetrieval;
18
+ return {
19
+ caseId: task.caseId,
20
+ status: task.status,
21
+ rawStatus: task.rawStatus,
22
+ warmStatus: task.warmStatus,
23
+ rawBaseline: raw
24
+ ? {
25
+ targetRoot: raw.targetRoot,
26
+ filesIncluded: [...raw.filesIncluded],
27
+ totalFiles: raw.totalFiles,
28
+ totalChars: raw.totalChars,
29
+ totalEstimatedTokens: raw.totalEstimatedTokens,
30
+ tokenCountMethod: raw.tokenCountMethod,
31
+ durationMs: raw.durationMs,
32
+ }
33
+ : null,
34
+ warmRetrieval: warm
35
+ ? {
36
+ skipped: warm.skipped,
37
+ warnings: [...warm.warnings],
38
+ totalChars: warm.totalChars,
39
+ totalEstimatedTokens: warm.totalEstimatedTokens,
40
+ tokenCountMethod: warm.tokenCountMethod,
41
+ filesRead: [...warm.filesRead],
42
+ selectedNodeId: warm.selectedNodeId ?? null,
43
+ selectedFile: warm.selectedFile ?? null,
44
+ selectedSymbol: warm.selectedSymbol ?? null,
45
+ durationMs: warm.durationMs,
46
+ commands: warm.commands.map(summarizeCommand),
47
+ }
48
+ : null,
49
+ warnings: [...task.warnings],
50
+ errors: task.errors.map((error) => ({ ...error })),
51
+ };
52
+ }
53
+ /** Bounded, context-free project summary used for both the persisted artifact and the run record. */
54
+ export function summarizeProjectExecution(project) {
55
+ return {
56
+ benchmarkProject: project.benchmarkProject,
57
+ sessionKey: project.projectSegment,
58
+ status: project.status,
59
+ targetRoot: project.targetRoot,
60
+ sourceRoots: [...project.sourceRoots],
61
+ indexDir: project.indexDir,
62
+ sessionPrepared: project.session !== undefined,
63
+ buildDurationMs: project.build ? project.build.durationMs : null,
64
+ indexCommand: project.build ? summarizeCommand(project.build.command) : null,
65
+ tasks: project.tasks.map(summarizeTask),
66
+ warnings: [...project.warnings],
67
+ errors: [...project.errors],
68
+ };
69
+ }
70
+ export function buildWarmIndexExecutionArtifact(args) {
71
+ return {
72
+ schemaVersion: WARM_INDEX_EXECUTION_SCHEMA_VERSION,
73
+ runId: args.runId,
74
+ pluginId: args.pluginId,
75
+ projects: args.projects.map(summarizeProjectExecution),
76
+ };
77
+ }
@@ -0,0 +1,118 @@
1
+ import path from "node:path";
2
+ import { runAgentPrompt } from "../../../agents/index.js";
3
+ import { resolveWithinRoot } from "../../../core/pathSafety.js";
4
+ import { classifyAgentRunOutcome } from "../../../evaluation/classifyAgentRunOutcome.js";
5
+ import { parseAgentAnswer } from "../../../evaluation/parseAgentAnswer.js";
6
+ import { scoreCorrectness } from "../../../evaluation/scoreCorrectness.js";
7
+ import { generatePromptVariants } from "../../../prompts/index.js";
8
+ import { taskOutputSegment } from "./selection.js";
9
+ // Classified statuses for which the scorer evaluated an actual agent answer.
10
+ const SCOREABLE_STATUSES = new Set(["completed", "invalid-output"]);
11
+ const SIDE_STRATEGIES = {
12
+ "raw-full-file": "raw-full-file",
13
+ "warm-index-reuse": "my-dev-kit-guided",
14
+ };
15
+ export async function evaluateWarmIndexFakeAgents(options) {
16
+ const casesById = new Map(options.cases.map((evaluationCase) => [evaluationCase.id, evaluationCase]));
17
+ const result = [];
18
+ for (const project of options.projects) {
19
+ const tasks = [];
20
+ for (const task of project.tasks) {
21
+ const evaluationCase = casesById.get(task.caseId);
22
+ const run = (variantId) => evaluateSide({
23
+ evaluationCase,
24
+ caseId: task.caseId,
25
+ variantId,
26
+ projectProfiles: options.projectProfiles,
27
+ outDir: resolveWithinRoot(options.outputRoot, path.join("agents", project.projectSegment, taskOutputSegment(task.caseId), variantId)),
28
+ cwd: options.cwd,
29
+ env: options.env,
30
+ });
31
+ tasks.push({
32
+ caseId: task.caseId,
33
+ raw: task.rawBaseline ? await run("raw-full-file") : null,
34
+ warm: task.warmRetrieval ? await run("warm-index-reuse") : null,
35
+ });
36
+ }
37
+ result.push({ benchmarkProject: project.benchmarkProject, tasks });
38
+ }
39
+ return result;
40
+ }
41
+ async function evaluateSide(args) {
42
+ const promptStrategy = SIDE_STRATEGIES[args.variantId];
43
+ const base = { variantId: args.variantId, agentId: "fake-agent", promptStrategy };
44
+ try {
45
+ if (!args.evaluationCase) {
46
+ throw new Error(`Evaluation case ${args.caseId} is not available for fake-agent evaluation.`);
47
+ }
48
+ const [promptVariant] = generatePromptVariants({
49
+ cases: [args.evaluationCase],
50
+ projectProfiles: [...args.projectProfiles],
51
+ strategies: [promptStrategy],
52
+ complexityLevels: ["short"],
53
+ });
54
+ if (!promptVariant) {
55
+ throw new Error(`Failed to generate a fake-agent prompt for case ${args.caseId}.`);
56
+ }
57
+ const agentRunResult = await runAgentPrompt({
58
+ runId: `${args.caseId}.fake-agent.${args.variantId}`,
59
+ agentId: "fake-agent",
60
+ promptVariant,
61
+ promptText: promptVariant.promptText,
62
+ cwd: args.cwd,
63
+ outDir: args.outDir,
64
+ env: args.env,
65
+ });
66
+ const parsedAnswer = parseAgentAnswer({
67
+ text: agentRunResult.finalAnswerText,
68
+ answerKey: promptVariant.expectedAnswerKey,
69
+ tokenUsage: agentRunResult.tokenUsage,
70
+ });
71
+ const classification = classifyAgentRunOutcome({ agentRunResult, parsedAnswer });
72
+ const correctness = scoreCorrectness({
73
+ caseId: args.caseId,
74
+ answerKey: promptVariant.expectedAnswerKey,
75
+ parsedAnswer,
76
+ status: classification.status,
77
+ });
78
+ const scoreable = SCOREABLE_STATUSES.has(classification.status) && Number.isFinite(correctness.correctnessScore);
79
+ const totalTokens = agentRunResult.tokenUsage.totalTokens;
80
+ return {
81
+ ...base,
82
+ status: classification.status,
83
+ correctness: {
84
+ available: scoreable,
85
+ score: scoreable ? correctness.correctnessScore : null,
86
+ passed: scoreable ? correctness.passed : null,
87
+ failureReasons: [...correctness.failureReasons],
88
+ },
89
+ tokenUsage: {
90
+ totalTokens: typeof totalTokens === "number" && Number.isFinite(totalTokens) ? totalTokens : null,
91
+ source: agentRunResult.tokenUsageSource,
92
+ reliability: agentRunResult.tokenUsageReliability,
93
+ },
94
+ durationMs: agentRunResult.durationMs,
95
+ warnings: [...classification.warnings],
96
+ errors: [...classification.errors],
97
+ artifactPaths: {
98
+ promptPath: path.join(args.outDir, "prompt.txt"),
99
+ agentRunResultPath: path.join(args.outDir, "agent-run-result.json"),
100
+ stdoutPath: agentRunResult.stdoutPath,
101
+ stderrPath: agentRunResult.stderrPath,
102
+ telemetryPath: agentRunResult.telemetryPath,
103
+ },
104
+ };
105
+ }
106
+ catch (error) {
107
+ return {
108
+ ...base,
109
+ status: "failed",
110
+ correctness: { available: false, score: null, passed: null, failureReasons: [] },
111
+ tokenUsage: { totalTokens: null, source: "unavailable", reliability: "unavailable" },
112
+ durationMs: null,
113
+ warnings: [],
114
+ errors: [error instanceof Error ? error.message : String(error)],
115
+ artifactPaths: {},
116
+ };
117
+ }
118
+ }
@@ -0,0 +1,8 @@
1
+ export * from "./config.js";
2
+ export * from "./execution.js";
3
+ export * from "./executionArtifact.js";
4
+ export * from "./fakeAgentEvaluation.js";
5
+ export * from "./metrics.js";
6
+ export * from "./plugin.js";
7
+ export * from "./selection.js";
8
+ export * from "./warmIndexSession.js";
@@ -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
+ }