@dailephd/my-dev-kit-lab 0.6.0 → 0.6.2

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 (44) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +7 -4
  3. package/benchmarks/contracts/incremental-change-staleness-scenarios.json +360 -0
  4. package/dist/src/commands/runExperimentRunCommand.js +4 -3
  5. package/dist/src/evaluation/affectedNeighborhood.js +712 -0
  6. package/dist/src/evaluation/affectedNeighborhoodMetrics.js +51 -0
  7. package/dist/src/evaluation/indexSnapshot.js +1 -1
  8. package/dist/src/experiments/defaultRegistry.js +2 -0
  9. package/dist/src/experiments/plugins/incrementalChangeStaleness/comparison.js +101 -0
  10. package/dist/src/experiments/plugins/incrementalChangeStaleness/config.js +66 -0
  11. package/dist/src/experiments/plugins/incrementalChangeStaleness/disposableTarget.js +107 -0
  12. package/dist/src/experiments/plugins/incrementalChangeStaleness/execution.js +404 -0
  13. package/dist/src/experiments/plugins/incrementalChangeStaleness/executionArtifact.js +160 -0
  14. package/dist/src/experiments/plugins/incrementalChangeStaleness/index.js +13 -0
  15. package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecycle.js +276 -0
  16. package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecyclePolicy.js +152 -0
  17. package/dist/src/experiments/plugins/incrementalChangeStaleness/mutationExecution.js +144 -0
  18. package/dist/src/experiments/plugins/incrementalChangeStaleness/plugin.js +274 -0
  19. package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioCatalog.js +42 -0
  20. package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioMutation.js +116 -0
  21. package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioSelection.js +79 -0
  22. package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioTypes.js +37 -0
  23. package/dist/src/experiments/plugins/incrementalChangeStaleness/scenarioValidation.js +168 -0
  24. package/dist/src/experiments/plugins/incrementalChangeStaleness/sourceState.js +145 -0
  25. package/dist/src/experiments/plugins/incrementalChangeStaleness/treatmentSession.js +6 -0
  26. package/dist/src/experiments/plugins/index.js +1 -0
  27. package/dist/src/experiments/plugins/warmIndexReuse/execution.js +21 -0
  28. package/dist/src/experiments/plugins/warmIndexReuse/executionArtifact.js +1 -0
  29. package/dist/src/experiments/plugins/warmIndexReuse/metrics.js +68 -0
  30. package/dist/src/experiments/plugins/warmIndexReuse/warmIndexSession.js +9 -1
  31. package/dist/src/report/experiments/buildIncrementalChangeStalenessReport.js +112 -0
  32. package/dist/src/report/experiments/buildPluginExperimentReport.js +17 -2
  33. package/dist/src/report/experiments/buildWarmIndexReuseReport.js +80 -3
  34. package/dist/src/report/experiments/incrementalChangeStalenessReportModel.js +4 -0
  35. package/dist/src/report/experiments/index.js +3 -0
  36. package/dist/src/report/experiments/renderIncrementalChangeStalenessHtml.js +239 -0
  37. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +3 -0
  38. package/dist/src/report/experiments/renderPluginExperimentReportText.js +222 -0
  39. package/dist/src/report/experiments/renderWarmIndexReuseHtml.js +98 -0
  40. package/dist/src/report/experiments/warmIndexReuseReportModel.js +5 -0
  41. package/docs/METRICS.md +79 -3
  42. package/examples/tutorial-browser/prepare.mjs +13 -13
  43. package/examples/tutorial-browser/server.mjs +13 -13
  44. package/package.json +1 -1
@@ -0,0 +1,404 @@
1
+ import path from "node:path";
2
+ import { runAgentPrompt } from "../../../agents/index.js";
3
+ import { resolveWithinRoot } from "../../../core/pathSafety.js";
4
+ import { assessAffectedNeighborhood, mapAffectedNeighborhoodSeeds } from "../../../evaluation/affectedNeighborhood.js";
5
+ import { classifyAgentRunOutcome } from "../../../evaluation/classifyAgentRunOutcome.js";
6
+ import { parseAgentAnswer } from "../../../evaluation/parseAgentAnswer.js";
7
+ import { readEvaluationCases } from "../../../evaluation/readEvaluationCases.js";
8
+ import { runMyDevKitRetrievalFromIndex } from "../../../evaluation/runMyDevKitRetrieval.js";
9
+ import { scoreCorrectness } from "../../../evaluation/scoreCorrectness.js";
10
+ import { generatePromptVariants } from "../../../prompts/index.js";
11
+ import { buildRequiredFileEvidence, classifyStaleRisk, compareCorrectness, compareRequiredFileEvidence } from "./comparison.js";
12
+ import { WARM_INDEX_BENCHMARK_CASES_PATH } from "./scenarioCatalog.js";
13
+ // ---------------------------------------------------------------------------
14
+ // v0.6.2 Batch 4 -- treatment retrieval, correctness, and required-file
15
+ // evidence. Reuses existing generic owners exactly (mapAffectedNeighborhoodSeeds
16
+ // / assessAffectedNeighborhood, runMyDevKitRetrievalFromIndex, scoreCorrectness,
17
+ // generatePromptVariants + runAgentPrompt("fake-agent") + parseAgentAnswer +
18
+ // classifyAgentRunOutcome). No new correctness/retrieval/fake-agent algorithm
19
+ // is introduced here.
20
+ // ---------------------------------------------------------------------------
21
+ const SCOREABLE_STATUSES = new Set(["completed", "invalid-output"]);
22
+ function errorMessage(error) {
23
+ return error instanceof Error ? error.message : String(error);
24
+ }
25
+ /**
26
+ * Resolves one scenario's query/answer key. `inherit` reuses the referenced base case's existing
27
+ * query/answer key unchanged; `scenario` uses the scenario's own post-mutation query/answer key.
28
+ * The two are never merged. Reads the same canonical benchmark-case contract the lifecycle already
29
+ * validated against; never mutates it.
30
+ */
31
+ export async function resolveIncrementalChangeStalenessQueryAndAnswer(options) {
32
+ const { scenario, repoRoot } = options;
33
+ if (scenario.answerPolicy === "scenario") {
34
+ if (!scenario.scenarioQuery || !scenario.scenarioAnswerKey) {
35
+ throw new Error(`Scenario ${scenario.id}: answerPolicy is "scenario" but scenarioQuery/scenarioAnswerKey are missing.`);
36
+ }
37
+ return {
38
+ query: scenario.scenarioQuery,
39
+ answerKey: scenario.scenarioAnswerKey,
40
+ expectedFiles: [...scenario.scenarioAnswerKey.expectedFiles],
41
+ expectedSymbols: [...scenario.scenarioAnswerKey.expectedSymbols]
42
+ };
43
+ }
44
+ const cases = await readEvaluationCases(path.resolve(repoRoot, WARM_INDEX_BENCHMARK_CASES_PATH), repoRoot);
45
+ const baseCase = cases.find((candidate) => candidate.id === scenario.baseCaseId);
46
+ if (!baseCase) {
47
+ throw new Error(`Scenario ${scenario.id}: base case ${scenario.baseCaseId} was not found for query/answer resolution.`);
48
+ }
49
+ if (!baseCase.answerKey) {
50
+ throw new Error(`Scenario ${scenario.id}: base case ${scenario.baseCaseId} declares no answerKey for the "inherit" answer policy.`);
51
+ }
52
+ return {
53
+ query: baseCase.query,
54
+ answerKey: baseCase.answerKey,
55
+ expectedFiles: [...baseCase.answerKey.expectedFiles],
56
+ expectedSymbols: [...baseCase.answerKey.expectedSymbols]
57
+ };
58
+ }
59
+ // ---------------------------------------------------------------------------
60
+ // Derived in-memory task descriptor (section 9). Never written to disk and
61
+ // never replaces the canonical benchmark case/answer-key files.
62
+ // ---------------------------------------------------------------------------
63
+ /**
64
+ * Builds one in-memory `EvaluationCase`-shaped task descriptor: identity/project/source ownership
65
+ * comes from the canonical base case; query and expected files/symbols come from the resolved
66
+ * query/answer. Used for affected-neighborhood task mapping, treatment retrieval, and fake-agent
67
+ * prompt generation; it is never materialized as a benchmark case file.
68
+ */
69
+ export function buildIncrementalChangeStalenessDerivedTask(args) {
70
+ const base = args.baseEvaluationCase;
71
+ return {
72
+ ...base,
73
+ id: `${args.scenario.id}:${base.id}`,
74
+ title: `${args.scenario.id} (${base.title})`,
75
+ query: args.resolved.query,
76
+ expectedFiles: [...args.resolved.expectedFiles],
77
+ expectedSymbols: [...args.resolved.expectedSymbols],
78
+ answerKey: args.resolved.answerKey,
79
+ projectProfileRef: base.projectProfileRef ?? base.benchmarkProject
80
+ };
81
+ }
82
+ // ---------------------------------------------------------------------------
83
+ // Affected-neighborhood execution (sections 10-14, 56).
84
+ // ---------------------------------------------------------------------------
85
+ /**
86
+ * Assesses one treatment's affected neighborhood using its BASELINE change authority (never the
87
+ * refreshed graph, even for full-refresh): the baseline index's snapshot/graph plus the treatment's
88
+ * already-established post-mutation baseline freshness.
89
+ */
90
+ export function assessIncrementalChangeStalenessAffectedNeighborhood(treatment, derivedTask) {
91
+ const changeAuthority = treatment.changeAuthority;
92
+ const seedMapping = mapAffectedNeighborhoodSeeds({
93
+ indexSnapshot: changeAuthority.baselineIndex.snapshot,
94
+ freshness: changeAuthority.postMutationBaselineFreshness,
95
+ graph: changeAuthority.baselineIndex.graph
96
+ });
97
+ return assessAffectedNeighborhood({ graph: changeAuthority.baselineIndex.graph, seedMapping, task: derivedTask });
98
+ }
99
+ const SYMMETRIC_NUMERIC_FIELDS = [
100
+ "changedFileCount",
101
+ "changedSymbolCount",
102
+ "affectedNodeCount",
103
+ "affectedEdgeCount",
104
+ "taskOverlapCount",
105
+ "taskOverlapPercent"
106
+ ];
107
+ /**
108
+ * Section 13: requires equality of assessment status, relationship, reindexRecommendation, and the
109
+ * availability/value of all six numeric metrics between the two treatment baselines. Does not
110
+ * require raw graph identity or absolute paths to match.
111
+ */
112
+ export function checkIncrementalChangeStalenessAffectedNeighborhoodSymmetry(stale, fullRefresh) {
113
+ const mismatches = [];
114
+ if (stale.status !== fullRefresh.status)
115
+ mismatches.push(`status (${stale.status} vs ${fullRefresh.status})`);
116
+ if (stale.relationship !== fullRefresh.relationship)
117
+ mismatches.push(`relationship (${stale.relationship} vs ${fullRefresh.relationship})`);
118
+ if (stale.reindexRecommendation !== fullRefresh.reindexRecommendation) {
119
+ mismatches.push(`reindexRecommendation (${stale.reindexRecommendation} vs ${fullRefresh.reindexRecommendation})`);
120
+ }
121
+ for (const field of SYMMETRIC_NUMERIC_FIELDS) {
122
+ if (stale[field] !== fullRefresh[field]) {
123
+ mismatches.push(`${field} (${String(stale[field])} vs ${String(fullRefresh[field])})`);
124
+ }
125
+ }
126
+ if (mismatches.length === 0)
127
+ return { symmetric: true };
128
+ return { symmetric: false, reason: `Affected-neighborhood assessments are asymmetric: ${mismatches.join("; ")}.` };
129
+ }
130
+ /** Same classification rule as the existing warm-index-reuse execution owner. */
131
+ export function classifyIncrementalChangeStalenessRetrieval(result) {
132
+ if (!result.skipped)
133
+ return "completed";
134
+ return result.commands.some((command) => !command.ok) ? "failed" : "skipped";
135
+ }
136
+ export async function runIncrementalChangeStalenessTreatmentRetrieval(args) {
137
+ return runMyDevKitRetrievalFromIndex({
138
+ evaluationCase: args.derivedTask,
139
+ kitCommand: args.kitCommand,
140
+ indexDir: args.indexDir,
141
+ commandsDir: args.commandsDir,
142
+ requireKit: false
143
+ });
144
+ }
145
+ /**
146
+ * Section 27: a `failed` retrieval status means the actual-files-read set cannot be trusted as
147
+ * complete (unknown); `completed` and `skipped` (search ran and legitimately found no candidate)
148
+ * both provide a complete, trustworthy files-read set, possibly empty.
149
+ */
150
+ export function buildIncrementalChangeStalenessRequiredFileEvidence(args) {
151
+ if (!args.retrieval) {
152
+ return buildRequiredFileEvidence({
153
+ requiredFiles: args.requiredFiles,
154
+ observedFiles: [],
155
+ observedFilesComplete: false,
156
+ unknownReason: "No retrieval was attempted for this treatment."
157
+ });
158
+ }
159
+ const status = classifyIncrementalChangeStalenessRetrieval(args.retrieval);
160
+ return buildRequiredFileEvidence({
161
+ requiredFiles: args.requiredFiles,
162
+ observedFiles: args.retrieval.filesRead,
163
+ observedFilesComplete: status !== "failed",
164
+ unknownReason: status === "failed"
165
+ ? `Retrieval failed (${args.retrieval.warnings.join(" ") || "my-dev-kit retrieval command failed."}); the actual-files-read set is incomplete.`
166
+ : undefined
167
+ });
168
+ }
169
+ export async function runIncrementalChangeStalenessFakeAgent(args) {
170
+ try {
171
+ const [promptVariant] = generatePromptVariants({
172
+ cases: [args.derivedTask],
173
+ projectProfiles: [...args.projectProfiles],
174
+ strategies: ["my-dev-kit-guided"],
175
+ complexityLevels: ["short"]
176
+ });
177
+ if (!promptVariant) {
178
+ throw new Error(`Failed to generate a fake-agent prompt for case ${args.derivedTask.id}.`);
179
+ }
180
+ const agentRunResult = await runAgentPrompt({
181
+ runId: args.runId,
182
+ agentId: "fake-agent",
183
+ promptVariant,
184
+ promptText: promptVariant.promptText,
185
+ cwd: args.cwd,
186
+ outDir: args.outDir,
187
+ env: args.env
188
+ });
189
+ const parsedAnswer = parseAgentAnswer({
190
+ text: agentRunResult.finalAnswerText,
191
+ answerKey: promptVariant.expectedAnswerKey,
192
+ tokenUsage: agentRunResult.tokenUsage
193
+ });
194
+ const classification = classifyAgentRunOutcome({ agentRunResult, parsedAnswer });
195
+ const correctness = scoreCorrectness({
196
+ caseId: args.derivedTask.id,
197
+ answerKey: promptVariant.expectedAnswerKey,
198
+ parsedAnswer,
199
+ status: classification.status
200
+ });
201
+ const scoreable = SCOREABLE_STATUSES.has(classification.status) && Number.isFinite(correctness.correctnessScore);
202
+ return {
203
+ status: classification.status,
204
+ correctness: {
205
+ available: scoreable,
206
+ score: scoreable ? correctness.correctnessScore : null,
207
+ passed: scoreable ? correctness.passed : null,
208
+ failureReasons: [...correctness.failureReasons]
209
+ },
210
+ correctnessDetail: scoreable ? correctness : null,
211
+ durationMs: agentRunResult.durationMs,
212
+ warnings: [...classification.warnings],
213
+ errors: [...classification.errors]
214
+ };
215
+ }
216
+ catch (error) {
217
+ return {
218
+ status: "failed",
219
+ correctness: { available: false, score: null, passed: null, failureReasons: [] },
220
+ correctnessDetail: null,
221
+ durationMs: null,
222
+ warnings: [],
223
+ errors: [errorMessage(error)]
224
+ };
225
+ }
226
+ }
227
+ function correctnessComparable(fakeAgent) {
228
+ if (!fakeAgent || !fakeAgent.correctness.available || fakeAgent.correctness.score === null) {
229
+ return { available: false };
230
+ }
231
+ return { available: true, score: fakeAgent.correctness.score };
232
+ }
233
+ const INCONCLUSIVE_NO_EXECUTION = {
234
+ correctnessRelation: "unknown",
235
+ requiredFileEvidenceRelation: "unknown",
236
+ staleRiskClassification: "inconclusive",
237
+ reasonCodes: ["correctness-unavailable", "required-file-evidence-unavailable"]
238
+ };
239
+ export const defaultIncrementalChangeStalenessExecutionDeps = {
240
+ runRetrieval: runIncrementalChangeStalenessTreatmentRetrieval,
241
+ runFakeAgent: runIncrementalChangeStalenessFakeAgent
242
+ };
243
+ async function executeOneTreatment(args) {
244
+ const treatment = args.session.treatments[args.treatmentId];
245
+ const baselineFreshness = treatment.changeAuthority.postMutationBaselineFreshness;
246
+ const activeIndexPhase = treatment.activeRetrieval.role;
247
+ let retrieval = null;
248
+ let retrievalStatus = "not-run";
249
+ let failureReason = null;
250
+ try {
251
+ retrieval = await args.deps.runRetrieval({
252
+ derivedTask: args.derivedTask,
253
+ kitCommand: args.kitCommand,
254
+ indexDir: treatment.activeRetrieval.index.indexDir,
255
+ commandsDir: args.retrievalCommandsDir
256
+ });
257
+ retrievalStatus = classifyIncrementalChangeStalenessRetrieval(retrieval);
258
+ }
259
+ catch (error) {
260
+ failureReason = `Treatment retrieval threw: ${errorMessage(error)}`;
261
+ retrievalStatus = "failed";
262
+ }
263
+ let fakeAgent = null;
264
+ if (retrieval !== null) {
265
+ fakeAgent = await args.deps.runFakeAgent({
266
+ derivedTask: args.derivedTask,
267
+ projectProfiles: args.projectProfiles,
268
+ outDir: args.agentOutDir,
269
+ cwd: args.cwd,
270
+ runId: `${args.derivedTask.id}.${args.treatmentId}`,
271
+ env: args.env
272
+ });
273
+ }
274
+ const requiredFileEvidence = buildIncrementalChangeStalenessRequiredFileEvidence({
275
+ requiredFiles: args.requiredFiles,
276
+ retrieval
277
+ });
278
+ const status = retrievalStatus === "failed" || fakeAgent?.status === "failed"
279
+ ? "partial"
280
+ : retrievalStatus === "completed" && fakeAgent && fakeAgent.correctness.available
281
+ ? "completed"
282
+ : "partial";
283
+ return {
284
+ treatmentId: args.treatmentId,
285
+ status,
286
+ failureReason,
287
+ activeIndexPhase,
288
+ baselineFreshness,
289
+ affectedNeighborhood: args.affectedNeighborhood,
290
+ retrieval,
291
+ retrievalStatus,
292
+ fakeAgent,
293
+ requiredFileEvidence
294
+ };
295
+ }
296
+ /**
297
+ * Executes the frozen Batch 4 order (section 36) for one ready lifecycle session: resolve query and
298
+ * answer, derive the task descriptor, assess both treatments' affected neighborhoods from BASELINE
299
+ * change authority, verify symmetry, then run stale retrieval/fake-agent/correctness/required-file
300
+ * evidence, then full-refresh, then compare and classify. A lifecycle that is not `ready` is
301
+ * reported as a failed scenario execution with no fabricated treatment evidence (section 39).
302
+ */
303
+ export async function executeIncrementalChangeStalenessScenario(options) {
304
+ const deps = { ...defaultIncrementalChangeStalenessExecutionDeps, ...options.deps };
305
+ const scenario = options.scenario;
306
+ if (options.lifecycle.status !== "ready") {
307
+ // Section 39: scenario-level failure; no treatment execution, comparison stays inconclusive.
308
+ return {
309
+ scenarioId: scenario.id,
310
+ category: scenario.category,
311
+ benchmarkProjectId: options.baseCase.benchmarkProjectId,
312
+ baseCaseId: options.baseCase.caseId,
313
+ answerPolicy: scenario.answerPolicy,
314
+ query: null,
315
+ expectedFiles: [],
316
+ expectedSymbols: [],
317
+ status: "failed",
318
+ failureReason: `${options.lifecycle.failure.code}: ${options.lifecycle.failure.message}`,
319
+ session: null,
320
+ stale: null,
321
+ fullRefresh: null,
322
+ comparison: INCONCLUSIVE_NO_EXECUTION
323
+ };
324
+ }
325
+ const session = options.lifecycle.session;
326
+ const resolved = await resolveIncrementalChangeStalenessQueryAndAnswer({ scenario, repoRoot: options.repoRoot });
327
+ const cases = await readEvaluationCases(path.resolve(options.repoRoot, WARM_INDEX_BENCHMARK_CASES_PATH), options.repoRoot);
328
+ const baseEvaluationCase = cases.find((candidate) => candidate.id === scenario.baseCaseId);
329
+ if (!baseEvaluationCase) {
330
+ throw new Error(`Scenario ${scenario.id}: base case ${scenario.baseCaseId} was not found for task-descriptor construction.`);
331
+ }
332
+ const derivedTask = buildIncrementalChangeStalenessDerivedTask({ scenario, baseEvaluationCase, resolved });
333
+ const staleAffected = assessIncrementalChangeStalenessAffectedNeighborhood(session.treatments["stale-index"], derivedTask);
334
+ const fullAffected = assessIncrementalChangeStalenessAffectedNeighborhood(session.treatments["full-refresh"], derivedTask);
335
+ const symmetry = checkIncrementalChangeStalenessAffectedNeighborhoodSymmetry(staleAffected, fullAffected);
336
+ const scenarioBase = {
337
+ scenarioId: scenario.id,
338
+ category: scenario.category,
339
+ benchmarkProjectId: scenario.benchmarkProjectId,
340
+ baseCaseId: scenario.baseCaseId,
341
+ answerPolicy: scenario.answerPolicy,
342
+ query: resolved.query,
343
+ expectedFiles: resolved.expectedFiles,
344
+ expectedSymbols: resolved.expectedSymbols,
345
+ session
346
+ };
347
+ if (!symmetry.symmetric) {
348
+ return {
349
+ ...scenarioBase,
350
+ status: "failed",
351
+ failureReason: `AFFECTED_NEIGHBORHOOD_ASYMMETRY: ${symmetry.reason}`,
352
+ stale: null,
353
+ fullRefresh: null,
354
+ comparison: INCONCLUSIVE_NO_EXECUTION
355
+ };
356
+ }
357
+ const retrievalCommandsDir = (treatmentId) => resolveWithinRoot(options.runOwnedRoot, path.join("commands", scenario.id, treatmentId, "retrieval"));
358
+ const agentOutDir = (treatmentId) => resolveWithinRoot(options.runOwnedRoot, path.join("agents", scenario.id, treatmentId));
359
+ // Stale-index runs first, deterministically, before full-refresh (section 37).
360
+ const stale = await executeOneTreatment({
361
+ session,
362
+ treatmentId: "stale-index",
363
+ derivedTask,
364
+ affectedNeighborhood: staleAffected,
365
+ requiredFiles: resolved.expectedFiles,
366
+ kitCommand: session.kitCommand,
367
+ retrievalCommandsDir: retrievalCommandsDir("stale-index"),
368
+ agentOutDir: agentOutDir("stale-index"),
369
+ projectProfiles: options.projectProfiles,
370
+ cwd: options.cwd,
371
+ env: options.env,
372
+ deps
373
+ });
374
+ const fullRefresh = await executeOneTreatment({
375
+ session,
376
+ treatmentId: "full-refresh",
377
+ derivedTask,
378
+ affectedNeighborhood: fullAffected,
379
+ requiredFiles: resolved.expectedFiles,
380
+ kitCommand: session.kitCommand,
381
+ retrievalCommandsDir: retrievalCommandsDir("full-refresh"),
382
+ agentOutDir: agentOutDir("full-refresh"),
383
+ projectProfiles: options.projectProfiles,
384
+ cwd: options.cwd,
385
+ env: options.env,
386
+ deps
387
+ });
388
+ const correctnessRelation = compareCorrectness(correctnessComparable(stale.fakeAgent), correctnessComparable(fullRefresh.fakeAgent));
389
+ const requiredFileEvidenceRelation = compareRequiredFileEvidence(stale.requiredFileEvidence, fullRefresh.requiredFileEvidence);
390
+ const classification = classifyStaleRisk(correctnessRelation, requiredFileEvidenceRelation);
391
+ return {
392
+ ...scenarioBase,
393
+ status: "ready",
394
+ failureReason: null,
395
+ stale,
396
+ fullRefresh,
397
+ comparison: {
398
+ correctnessRelation,
399
+ requiredFileEvidenceRelation,
400
+ staleRiskClassification: classification.staleRiskClassification,
401
+ reasonCodes: classification.reasonCodes
402
+ }
403
+ };
404
+ }
@@ -0,0 +1,160 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { resolveWithinRoot } from "../../../core/pathSafety.js";
3
+ // ---------------------------------------------------------------------------
4
+ // v0.6.2 Batch 4 -- final persisted execution/comparison artifact. Owns only
5
+ // the artifact types, the schema constant, the bounded projection from
6
+ // runtime evidence, summary counts, and writing the artifact file. No
7
+ // comparison formula and no artifact serialization logic for another plugin
8
+ // lives here.
9
+ // ---------------------------------------------------------------------------
10
+ export const INCREMENTAL_CHANGE_STALENESS_EXECUTION_ARTIFACT_FILE = "incremental-change-staleness-execution.json";
11
+ export const INCREMENTAL_CHANGE_STALENESS_EXECUTION_SCHEMA_VERSION = "my-dev-kit-lab-incremental-change-staleness-execution-v1";
12
+ function sortedUnique(values) {
13
+ return [...new Set(values)].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
14
+ }
15
+ function summarizeCommand(command) {
16
+ return {
17
+ commandId: command.commandId,
18
+ ok: command.ok,
19
+ exitCode: command.exitCode,
20
+ durationMs: command.durationMs,
21
+ stdoutPath: command.stdoutPath,
22
+ stderrPath: command.stderrPath,
23
+ telemetryPath: command.telemetryPath,
24
+ error: command.error ?? null
25
+ };
26
+ }
27
+ function summarizeRetrieval(treatment) {
28
+ const retrieval = treatment.retrieval;
29
+ if (!retrieval) {
30
+ return {
31
+ status: "not-run",
32
+ warnings: [],
33
+ totalChars: 0,
34
+ totalEstimatedTokens: 0,
35
+ tokenCountMethod: "unavailable",
36
+ filesRead: [],
37
+ selectedNodeId: null,
38
+ selectedFile: null,
39
+ selectedSymbol: null,
40
+ durationMs: 0,
41
+ commands: []
42
+ };
43
+ }
44
+ return {
45
+ // Bounded status only; never embeds retrieval.contextText (the full context body).
46
+ status: treatment.retrievalStatus === "not-run" ? "not-run" : treatment.retrievalStatus,
47
+ warnings: [...retrieval.warnings],
48
+ totalChars: retrieval.totalChars,
49
+ totalEstimatedTokens: retrieval.totalEstimatedTokens,
50
+ tokenCountMethod: retrieval.tokenCountMethod,
51
+ filesRead: sortedUnique(retrieval.filesRead),
52
+ selectedNodeId: retrieval.selectedNodeId ?? null,
53
+ selectedFile: retrieval.selectedFile ?? null,
54
+ selectedSymbol: retrieval.selectedSymbol ?? null,
55
+ durationMs: retrieval.durationMs,
56
+ commands: retrieval.commands.map(summarizeCommand)
57
+ };
58
+ }
59
+ function summarizeFakeAgent(treatment) {
60
+ if (!treatment.fakeAgent)
61
+ return null;
62
+ const agent = treatment.fakeAgent;
63
+ return {
64
+ status: agent.status,
65
+ correctness: { ...agent.correctness, failureReasons: [...agent.correctness.failureReasons] },
66
+ correctnessDetail: agent.correctnessDetail ? { ...agent.correctnessDetail, failureReasons: [...agent.correctnessDetail.failureReasons] } : null,
67
+ durationMs: agent.durationMs,
68
+ warnings: [...agent.warnings],
69
+ errors: [...agent.errors]
70
+ };
71
+ }
72
+ function summarizeRequiredFileEvidence(evidence) {
73
+ return {
74
+ status: evidence.status,
75
+ requiredFiles: sortedUnique(evidence.requiredFiles),
76
+ observedFiles: sortedUnique(evidence.observedFiles),
77
+ missingFiles: sortedUnique(evidence.missingFiles),
78
+ reason: evidence.reason
79
+ };
80
+ }
81
+ function summarizeTreatment(treatment) {
82
+ if (!treatment)
83
+ return null;
84
+ return {
85
+ treatmentId: treatment.treatmentId,
86
+ status: treatment.status,
87
+ failureReason: treatment.failureReason,
88
+ activeIndexPhase: treatment.activeIndexPhase,
89
+ baselineFreshness: structuredClone(treatment.baselineFreshness),
90
+ affectedNeighborhood: structuredClone(treatment.affectedNeighborhood),
91
+ retrieval: summarizeRetrieval(treatment),
92
+ fakeAgent: summarizeFakeAgent(treatment),
93
+ requiredFileEvidence: summarizeRequiredFileEvidence(treatment.requiredFileEvidence)
94
+ };
95
+ }
96
+ function summarizeLifecycle(execution) {
97
+ const session = execution.session;
98
+ if (!session)
99
+ return null;
100
+ return {
101
+ preMutationEquivalence: session.preMutationEquivalence.result,
102
+ postMutationEquivalence: session.postMutationEquivalence.result,
103
+ controlledFilePaths: [...session.controlledChangedPaths],
104
+ staleBaselineFreshnessStatus: session.treatments["stale-index"].changeAuthority.postMutationBaselineFreshness.status,
105
+ fullRefreshBaselineFreshnessStatus: session.treatments["full-refresh"].changeAuthority.postMutationBaselineFreshness.status,
106
+ fullRefreshRefreshedFreshnessStatus: session.treatments["full-refresh"].refreshedFreshness?.status ?? null,
107
+ myDevKitVersion: session.toolIdentity.version ?? null,
108
+ sourceRoots: [...session.baseCase.sourceRoots]
109
+ };
110
+ }
111
+ function summarizeScenario(execution) {
112
+ return {
113
+ scenarioId: execution.scenarioId,
114
+ category: execution.category,
115
+ benchmarkProjectId: execution.benchmarkProjectId,
116
+ baseCaseId: execution.baseCaseId,
117
+ answerPolicy: execution.answerPolicy,
118
+ query: execution.query,
119
+ expectedFiles: sortedUnique(execution.expectedFiles),
120
+ expectedSymbols: sortedUnique(execution.expectedSymbols),
121
+ status: execution.status,
122
+ failureReason: execution.failureReason,
123
+ lifecycle: summarizeLifecycle(execution),
124
+ // Treatment order in the persisted record is always stale-index then full-refresh.
125
+ stale: summarizeTreatment(execution.stale),
126
+ fullRefresh: summarizeTreatment(execution.fullRefresh),
127
+ comparison: { ...execution.comparison, reasonCodes: [...execution.comparison.reasonCodes] }
128
+ };
129
+ }
130
+ function summarize(scenarios) {
131
+ return {
132
+ scenarioCount: scenarios.length,
133
+ readyScenarioCount: scenarios.filter((scenario) => scenario.status === "ready").length,
134
+ failedScenarioCount: scenarios.filter((scenario) => scenario.status === "failed").length,
135
+ observedStaleRegressionCount: scenarios.filter((scenario) => scenario.comparison.staleRiskClassification === "observed-stale-regression").length,
136
+ noObservedStaleRegressionCount: scenarios.filter((scenario) => scenario.comparison.staleRiskClassification === "no-observed-stale-regression").length,
137
+ inconclusiveCount: scenarios.filter((scenario) => scenario.comparison.staleRiskClassification === "inconclusive").length
138
+ };
139
+ }
140
+ /**
141
+ * Builds the final artifact from already-executed scenario evidence, in the exact order the
142
+ * caller supplies (canonical selected-scenario order). Treatment order within each scenario is
143
+ * always stale-index then full-refresh (`INCREMENTAL_CHANGE_STALENESS_TREATMENT_IDS`).
144
+ */
145
+ export function buildIncrementalChangeStalenessExecutionArtifact(args) {
146
+ const scenarios = args.executions.map(summarizeScenario);
147
+ return {
148
+ schemaVersion: INCREMENTAL_CHANGE_STALENESS_EXECUTION_SCHEMA_VERSION,
149
+ runId: args.runId,
150
+ pluginId: args.pluginId,
151
+ scenarios,
152
+ summary: summarize(scenarios)
153
+ };
154
+ }
155
+ export async function writeIncrementalChangeStalenessExecutionArtifact(outDir, artifact) {
156
+ await mkdir(outDir, { recursive: true });
157
+ const artifactPath = resolveWithinRoot(outDir, INCREMENTAL_CHANGE_STALENESS_EXECUTION_ARTIFACT_FILE);
158
+ await writeFile(artifactPath, `${JSON.stringify(artifact, null, 2)}\n`, "utf8");
159
+ return artifactPath;
160
+ }
@@ -0,0 +1,13 @@
1
+ export * from "./scenarioTypes.js";
2
+ export * from "./scenarioMutation.js";
3
+ export * from "./scenarioValidation.js";
4
+ export * from "./scenarioCatalog.js";
5
+ export * from "./disposableTarget.js";
6
+ export * from "./sourceState.js";
7
+ export * from "./mutationExecution.js";
8
+ export * from "./treatmentSession.js";
9
+ export * from "./lifecyclePolicy.js";
10
+ export * from "./scenarioSelection.js";
11
+ export * from "./lifecycle.js";
12
+ export * from "./config.js";
13
+ export * from "./plugin.js";