@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,58 @@
1
+ import { invalidExperimentConfig, isPlainObject, mergeConfig, validExperimentConfig } from "../../config.js";
2
+ export const defaultWarmIndexReuseConfig = {
3
+ casesPath: "examples/token-savings-cases.json",
4
+ projectProfilesPath: "benchmarks/contracts/benchmark-project-profiles.json",
5
+ outDir: "lab-output/warm-index-reuse",
6
+ kitCommand: "npx @dailephd/my-dev-kit@latest",
7
+ };
8
+ const SUPPORTED_FIELDS = [
9
+ "casesPath",
10
+ "projectProfilesPath",
11
+ "outDir",
12
+ "kitCommand",
13
+ "caseIds",
14
+ "benchmarkProjects",
15
+ ];
16
+ export const warmIndexReuseConfigDefinition = {
17
+ fields: [
18
+ { name: "casesPath", type: "string", required: true, description: "Evaluation cases file." },
19
+ { name: "outDir", type: "string", required: true, description: "Experiment output root." },
20
+ { name: "projectProfilesPath", type: "string", description: "Benchmark project profiles file." },
21
+ { name: "kitCommand", type: "string", description: "my-dev-kit command used for index and retrieval." },
22
+ { name: "caseIds", type: "array", description: "Evaluation case IDs to include." },
23
+ { name: "benchmarkProjects", type: "array", description: "Benchmark project IDs to include." },
24
+ ],
25
+ };
26
+ /**
27
+ * Closed validation: fields outside the warm-index contract (for example agent-matrix settings)
28
+ * are rejected instead of being silently ignored.
29
+ */
30
+ export function validateWarmIndexReuseConfig(config) {
31
+ if (config !== undefined && !isPlainObject(config)) {
32
+ return invalidExperimentConfig(["warm index reuse config must be an object."]);
33
+ }
34
+ const errors = [];
35
+ const unsupported = Object.keys(config ?? {}).filter((key) => !SUPPORTED_FIELDS.includes(key));
36
+ if (unsupported.length > 0) {
37
+ errors.push(`Unsupported warm-index-reuse config field(s): ${unsupported.sort().join(", ")}.`);
38
+ }
39
+ const normalized = mergeConfig(defaultWarmIndexReuseConfig, config);
40
+ for (const field of ["casesPath", "projectProfilesPath", "outDir", "kitCommand"]) {
41
+ const value = normalized[field];
42
+ if (typeof value !== "string" || !value.trim()) {
43
+ errors.push(`${field} must be a non-empty string.`);
44
+ }
45
+ }
46
+ for (const field of ["caseIds", "benchmarkProjects"]) {
47
+ const value = normalized[field];
48
+ if (value === undefined)
49
+ continue;
50
+ if (!Array.isArray(value) || value.length === 0 || !value.every((item) => typeof item === "string" && item.trim())) {
51
+ errors.push(`${field} must be a non-empty array of non-empty strings when provided.`);
52
+ }
53
+ }
54
+ if (errors.length > 0) {
55
+ return invalidExperimentConfig(errors);
56
+ }
57
+ return validExperimentConfig(normalized);
58
+ }
@@ -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";