@dailephd/my-dev-kit-lab 0.6.2 → 0.7.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 (138) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +11 -8
  3. package/benchmarks/contracts/context-window-scaling-cases.json +220 -0
  4. package/benchmarks/projects/context-window-scaling-fixed-ts/README.md +14 -0
  5. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-1/carrier-depot-catalog-01.ts +302 -0
  6. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-1/course-module-catalog-06.ts +302 -0
  7. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-1/library-shelf-catalog-07.ts +302 -0
  8. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-1/sensor-station-catalog-05.ts +302 -0
  9. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-1/staff-rota-catalog-03.ts +302 -0
  10. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-1/supplier-contract-catalog-02.ts +302 -0
  11. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-1/vehicle-route-catalog-04.ts +302 -0
  12. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-2/archive-box-catalog-12.ts +302 -0
  13. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-2/clinic-ward-catalog-08.ts +302 -0
  14. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-2/greenhouse-bay-catalog-13.ts +302 -0
  15. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-2/harbor-berth-catalog-10.ts +302 -0
  16. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-2/kitchen-section-catalog-15.ts +302 -0
  17. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-2/market-stall-catalog-09.ts +302 -0
  18. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-2/station-platform-catalog-14.ts +302 -0
  19. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-2/workshop-bench-catalog-11.ts +302 -0
  20. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-3/archive-box-catalog-28.ts +302 -0
  21. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-3/carrier-depot-catalog-17.ts +302 -0
  22. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-3/clinic-ward-catalog-24.ts +302 -0
  23. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-3/course-module-catalog-22.ts +302 -0
  24. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-3/greenhouse-bay-catalog-29.ts +302 -0
  25. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-3/harbor-berth-catalog-26.ts +302 -0
  26. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-3/kitchen-section-catalog-31.ts +302 -0
  27. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-3/library-shelf-catalog-23.ts +302 -0
  28. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-3/market-stall-catalog-25.ts +302 -0
  29. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-3/sensor-station-catalog-21.ts +302 -0
  30. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-3/staff-rota-catalog-19.ts +302 -0
  31. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-3/station-platform-catalog-30.ts +302 -0
  32. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-3/supplier-contract-catalog-18.ts +302 -0
  33. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-3/vehicle-route-catalog-20.ts +302 -0
  34. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-3/warehouse-zone-catalog-16.ts +302 -0
  35. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-3/workshop-bench-catalog-27.ts +302 -0
  36. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/archive-box-catalog-44.ts +302 -0
  37. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/carrier-depot-catalog-33.ts +302 -0
  38. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/carrier-depot-catalog-49.ts +302 -0
  39. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/clinic-ward-catalog-40.ts +302 -0
  40. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/clinic-ward-catalog-56.ts +302 -0
  41. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/course-module-catalog-38.ts +302 -0
  42. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/course-module-catalog-54.ts +302 -0
  43. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/greenhouse-bay-catalog-45.ts +302 -0
  44. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/harbor-berth-catalog-42.ts +302 -0
  45. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/harbor-berth-catalog-58.ts +302 -0
  46. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/kitchen-section-catalog-47.ts +302 -0
  47. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/library-shelf-catalog-39.ts +302 -0
  48. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/library-shelf-catalog-55.ts +302 -0
  49. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/market-stall-catalog-41.ts +302 -0
  50. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/market-stall-catalog-57.ts +302 -0
  51. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/sensor-station-catalog-37.ts +302 -0
  52. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/sensor-station-catalog-53.ts +302 -0
  53. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/staff-rota-catalog-35.ts +302 -0
  54. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/staff-rota-catalog-51.ts +302 -0
  55. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/station-platform-catalog-46.ts +302 -0
  56. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/supplier-contract-catalog-34.ts +302 -0
  57. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/supplier-contract-catalog-50.ts +302 -0
  58. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/vehicle-route-catalog-36.ts +302 -0
  59. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/vehicle-route-catalog-52.ts +302 -0
  60. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/warehouse-zone-catalog-32.ts +302 -0
  61. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/warehouse-zone-catalog-48.ts +302 -0
  62. package/benchmarks/projects/context-window-scaling-fixed-ts/src/filler/tier-4/workshop-bench-catalog-43.ts +302 -0
  63. package/benchmarks/projects/context-window-scaling-fixed-ts/src/tasks/auditRedaction.ts +25 -0
  64. package/benchmarks/projects/context-window-scaling-fixed-ts/src/tasks/inventoryReservation.ts +26 -0
  65. package/benchmarks/projects/context-window-scaling-fixed-ts/src/tasks/invoiceTotals.ts +25 -0
  66. package/benchmarks/projects/context-window-scaling-fixed-ts/src/tasks/shippingQuote.ts +32 -0
  67. package/benchmarks/projects/context-window-scaling-fixed-ts/tests/tasks/auditRedaction.test.ts +17 -0
  68. package/benchmarks/projects/context-window-scaling-fixed-ts/tests/tasks/inventoryReservation.test.ts +19 -0
  69. package/benchmarks/projects/context-window-scaling-fixed-ts/tests/tasks/invoiceTotals.test.ts +14 -0
  70. package/benchmarks/projects/context-window-scaling-fixed-ts/tests/tasks/shippingQuote.test.ts +19 -0
  71. package/dist/src/cli/help.js +27 -10
  72. package/dist/src/commands/runExperimentDescribeCommand.js +5 -2
  73. package/dist/src/commands/runExperimentRunCommand.js +64 -8
  74. package/dist/src/evaluation/benchmarkMetadata.js +5 -1
  75. package/dist/src/evaluation/parseAgentAnswer.js +172 -24
  76. package/dist/src/evaluation/runMyDevKitRetrieval.js +120 -7
  77. package/dist/src/experiments/defaultRegistry.js +2 -0
  78. package/dist/src/experiments/plugins/contextWindowScaling/cliBudgets.js +31 -0
  79. package/dist/src/experiments/plugins/contextWindowScaling/config.js +84 -0
  80. package/dist/src/experiments/plugins/contextWindowScaling/contextBudget.js +43 -0
  81. package/dist/src/experiments/plugins/contextWindowScaling/evaluation.js +36 -0
  82. package/dist/src/experiments/plugins/contextWindowScaling/execution.js +187 -0
  83. package/dist/src/experiments/plugins/contextWindowScaling/executionArtifact.js +15 -0
  84. package/dist/src/experiments/plugins/contextWindowScaling/executionArtifactReader.js +186 -0
  85. package/dist/src/experiments/plugins/contextWindowScaling/index.js +14 -0
  86. package/dist/src/experiments/plugins/contextWindowScaling/metadata.js +12 -0
  87. package/dist/src/experiments/plugins/contextWindowScaling/metrics.js +149 -0
  88. package/dist/src/experiments/plugins/contextWindowScaling/plugin.js +185 -0
  89. package/dist/src/experiments/plugins/contextWindowScaling/projectProfile.js +51 -0
  90. package/dist/src/experiments/plugins/contextWindowScaling/relevantFiles.js +76 -0
  91. package/dist/src/experiments/plugins/contextWindowScaling/successEvidence.js +60 -0
  92. package/dist/src/experiments/plugins/contextWindowScaling/types.js +1 -0
  93. package/dist/src/experiments/plugins/incrementalChangeStaleness/comparisonV2.js +49 -0
  94. package/dist/src/experiments/plugins/incrementalChangeStaleness/config.js +5 -4
  95. package/dist/src/experiments/plugins/incrementalChangeStaleness/disposableTarget.js +20 -2
  96. package/dist/src/experiments/plugins/incrementalChangeStaleness/execution.js +42 -16
  97. package/dist/src/experiments/plugins/incrementalChangeStaleness/executionArtifact.js +4 -4
  98. package/dist/src/experiments/plugins/incrementalChangeStaleness/executionArtifactV2.js +289 -0
  99. package/dist/src/experiments/plugins/incrementalChangeStaleness/executionV2.js +195 -0
  100. package/dist/src/experiments/plugins/incrementalChangeStaleness/indexClone.js +76 -0
  101. package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecycle.js +64 -32
  102. package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecyclePolicy.js +6 -4
  103. package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecyclePolicyV2.js +78 -0
  104. package/dist/src/experiments/plugins/incrementalChangeStaleness/lifecycleV2.js +298 -0
  105. package/dist/src/experiments/plugins/incrementalChangeStaleness/plugin.js +213 -12
  106. package/dist/src/experiments/plugins/incrementalChangeStaleness/treatmentSessionV2.js +5 -0
  107. package/dist/src/experiments/plugins/index.js +1 -0
  108. package/dist/src/experiments/plugins/warmIndexReuse/agentEvaluation.js +1 -1
  109. package/dist/src/experiments/plugins/warmIndexReuse/warmIndexSession.js +3 -1
  110. package/dist/src/mobile/android/advancedSecurity/candidateEvidence.js +9 -2
  111. package/dist/src/mobile/android/advancedSecurity/redaction.js +19 -10
  112. package/dist/src/mobile/android/advancedSecurity/secretCandidates/analyzeSecretCandidates.js +12 -2
  113. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/analyzeSigningConfiguration.js +1 -1
  114. package/dist/src/mobile/android/advancedSecurity/signingConfiguration/extractSigningConfigurations.js +7 -7
  115. package/dist/src/plots/buildContextWindowScalingPlotData.js +96 -0
  116. package/dist/src/plots/index.js +1 -0
  117. package/dist/src/plots/writePlotArtifacts.js +12 -3
  118. package/dist/src/report/experiments/buildContextWindowScalingReport.js +84 -0
  119. package/dist/src/report/experiments/buildIncrementalChangeStalenessPluginReport.js +33 -0
  120. package/dist/src/report/experiments/buildIncrementalChangeStalenessReport.js +1 -1
  121. package/dist/src/report/experiments/buildIncrementalChangeStalenessReportV2.js +80 -0
  122. package/dist/src/report/experiments/buildPluginExperimentReport.js +24 -4
  123. package/dist/src/report/experiments/contextWindowScalingReportModel.js +3 -0
  124. package/dist/src/report/experiments/incrementalChangeStalenessReportModelV2.js +3 -0
  125. package/dist/src/report/experiments/incrementalChangeStalenessReportRowsV2.js +146 -0
  126. package/dist/src/report/experiments/index.js +7 -0
  127. package/dist/src/report/experiments/renderContextWindowScalingHtml.js +99 -0
  128. package/dist/src/report/experiments/renderContextWindowScalingText.js +108 -0
  129. package/dist/src/report/experiments/renderIncrementalChangeStalenessHtml.js +4 -0
  130. package/dist/src/report/experiments/renderIncrementalChangeStalenessHtmlV2.js +111 -0
  131. package/dist/src/report/experiments/renderIncrementalChangeStalenessTextV2.js +96 -0
  132. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +3 -0
  133. package/dist/src/report/experiments/renderPluginExperimentReportText.js +14 -0
  134. package/dist/src/securityValidation/staticScans/codeql.js +8 -8
  135. package/docs/METRICS.md +58 -0
  136. package/examples/tutorial-browser/prepare.mjs +13 -13
  137. package/examples/tutorial-browser/server.mjs +13 -13
  138. package/package.json +1 -1
@@ -0,0 +1,76 @@
1
+ import { existsSync } from "node:fs";
2
+ import { cp, lstat, mkdir, readdir, readFile, realpath } from "node:fs/promises";
3
+ import { createHash } from "node:crypto";
4
+ import path from "node:path";
5
+ import { resolveWithinRoot } from "../../../core/pathSafety.js";
6
+ /**
7
+ * Narrow v0.6.3 lifecycle-preparation helper: copies one already-produced my-dev-kit index
8
+ * directory byte-for-byte to a new sibling directory so an upstream incremental refresh can run
9
+ * in place against the copy while the original baseline stays immutable change authority.
10
+ *
11
+ * Both paths must resolve inside `runOwnedRoot` (textually and, for the destination parent, by
12
+ * real path), the destination must not exist, symlinks in the source are refused, and the copy
13
+ * is verified file-for-file by SHA-256. Index contents are never edited. Any failure throws.
14
+ */
15
+ export async function cloneIncrementalChangeStalenessIndexDirectory(runOwnedRoot, sourceDir, destinationDir) {
16
+ const root = path.resolve(runOwnedRoot);
17
+ const source = resolveWithinRoot(root, path.resolve(sourceDir));
18
+ const destination = resolveWithinRoot(root, path.resolve(destinationDir));
19
+ if (!existsSync(source)) {
20
+ throw new Error(`Index clone source does not exist: ${source}.`);
21
+ }
22
+ if (existsSync(destination)) {
23
+ throw new Error(`Index clone destination already exists; refusing to overwrite: ${destination}.`);
24
+ }
25
+ const relativeToSource = path.relative(source, destination);
26
+ if (relativeToSource === "" || (!relativeToSource.startsWith("..") && !path.isAbsolute(relativeToSource))) {
27
+ throw new Error(`Index clone destination ${destination} must not be inside the source ${source}.`);
28
+ }
29
+ const sourceFiles = await listRegularFiles(source);
30
+ // The destination parent must be real-path contained by the real run-owned root (no symlink escape).
31
+ const parent = path.dirname(destination);
32
+ await mkdir(parent, { recursive: true });
33
+ const [realParent, realRoot] = await Promise.all([realpath(parent), realpath(root)]);
34
+ const parentRelative = path.relative(realRoot, realParent);
35
+ if (parentRelative === ".." || parentRelative.startsWith(`..${path.sep}`) || path.isAbsolute(parentRelative)) {
36
+ throw new Error(`Index clone destination parent escapes the run-owned root via a symbolic link: ${parent}.`);
37
+ }
38
+ await cp(source, destination, { recursive: true, errorOnExist: true, force: false, dereference: false });
39
+ const copiedFiles = await listRegularFiles(destination);
40
+ if (copiedFiles.length !== sourceFiles.length || copiedFiles.some((file, index) => file !== sourceFiles[index])) {
41
+ throw new Error(`Index clone is incomplete: ${destination} does not contain exactly the files of ${source}.`);
42
+ }
43
+ for (const file of sourceFiles) {
44
+ const [left, right] = await Promise.all([sha256(path.join(source, file)), sha256(path.join(destination, file))]);
45
+ if (left !== right) {
46
+ throw new Error(`Index clone content differs for ${file}.`);
47
+ }
48
+ }
49
+ }
50
+ async function sha256(filePath) {
51
+ return createHash("sha256").update(await readFile(filePath)).digest("hex");
52
+ }
53
+ /** Sorted POSIX-relative regular files; refuses symlinks and other non-regular entries. */
54
+ async function listRegularFiles(root) {
55
+ const files = [];
56
+ async function walk(dir) {
57
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
58
+ const full = path.join(dir, entry.name);
59
+ const stat = await lstat(full);
60
+ if (stat.isSymbolicLink()) {
61
+ throw new Error(`Index directory contains a symbolic link, which is not copied: ${full}.`);
62
+ }
63
+ if (stat.isDirectory()) {
64
+ await walk(full);
65
+ }
66
+ else if (stat.isFile()) {
67
+ files.push(path.relative(root, full).replace(/\\/g, "/"));
68
+ }
69
+ else {
70
+ throw new Error(`Index directory contains a non-regular entry: ${full}.`);
71
+ }
72
+ }
73
+ }
74
+ await walk(root);
75
+ return files.sort();
76
+ }
@@ -24,17 +24,69 @@ export function incrementalChangeStalenessIndexDir(runOwnedRoot, scenarioId, tre
24
24
  export function incrementalChangeStalenessCommandsDir(runOwnedRoot, scenarioId, treatmentId, role) {
25
25
  return resolveWithinRoot(path.resolve(runOwnedRoot), path.join("commands", scenarioId, treatmentId, role));
26
26
  }
27
- class LifecycleFailure extends Error {
27
+ export class LifecycleFailure extends Error {
28
28
  failure;
29
29
  constructor(failure) {
30
30
  super(failure.message);
31
31
  this.failure = failure;
32
32
  }
33
33
  }
34
- function fail(failure) {
34
+ export function fail(failure) {
35
35
  if (failure)
36
36
  throw new LifecycleFailure(failure);
37
37
  }
38
+ /**
39
+ * Shared by the v0.6.2 and v0.6.3 lifecycles: runs the index through the established warm-index
40
+ * session owner (index -> version probe -> snapshot -> graph), then proves target/index identity.
41
+ * `mode` defaults to an ordinary full build. The index directory must be absent unless
42
+ * `existingIndexDir` is set (an in-place refresh of an already-cloned baseline).
43
+ */
44
+ export async function prepareTreatmentIndexEvidence(params) {
45
+ const { deps, target, role, indexDir, sourceRoots } = params;
46
+ const treatmentId = target.treatmentId;
47
+ if (params.existingIndexDir ? !existsSync(indexDir) : existsSync(indexDir)) {
48
+ fail({
49
+ code: "index-directory-collision",
50
+ message: params.existingIndexDir
51
+ ? `Expected an existing cloned ${treatmentId} ${role} index directory: ${indexDir}.`
52
+ : `Refusing to overwrite existing ${treatmentId} ${role} index directory: ${indexDir}.`,
53
+ treatmentId
54
+ });
55
+ }
56
+ const commandsDir = incrementalChangeStalenessCommandsDir(params.runOwnedRoot, params.scenarioId, treatmentId, role);
57
+ const prepared = await deps.prepareIndexSession({
58
+ target: { absoluteTargetRoot: target.targetRoot, sourceRoots: [...sourceRoots] },
59
+ kitCommand: params.kitCommand,
60
+ indexDir,
61
+ commandsDir,
62
+ requireKit: false,
63
+ ...(params.mode ? { mode: params.mode } : {})
64
+ });
65
+ if (!prepared.ok) {
66
+ fail({
67
+ code: role === "baseline" ? "baseline-index-build-failed" : "refresh-index-build-failed",
68
+ message: `${treatmentId} ${role} my-dev-kit index build failed: ${prepared.warnings.join(" ") || `exit code ${prepared.build.command.exitCode}`}`,
69
+ treatmentId
70
+ });
71
+ throw new Error("unreachable");
72
+ }
73
+ params.onIndexPrepared?.();
74
+ const evidence = Object.freeze({
75
+ role,
76
+ treatmentId,
77
+ builtRelativeToMutation: role === "baseline" ? "pre-mutation" : "post-mutation",
78
+ indexDir: prepared.session.indexDir,
79
+ commandsDir,
80
+ targetRoot: prepared.session.targetRoot,
81
+ sourceRoots: prepared.session.sourceRoots,
82
+ buildDurationMs: prepared.session.buildDurationMs,
83
+ buildCommand: prepared.session.buildCommand,
84
+ snapshot: prepared.session.indexSnapshot,
85
+ graph: prepared.session.affectedNeighborhoodGraph
86
+ });
87
+ fail(evaluateIndexEvidence(evidence, { targetRoot: target.targetRoot, sourceRoots, indexDir }));
88
+ return { evidence, build: prepared.build };
89
+ }
38
90
  /**
39
91
  * Owns the matched Batch 3 lifecycle for ONE frozen scenario:
40
92
  *
@@ -73,40 +125,20 @@ export async function prepareIncrementalChangeStalenessScenarioLifecycle(options
73
125
  if (existsSync(indexDir)) {
74
126
  fail({ code: "index-directory-collision", message: `Refusing to overwrite existing ${treatmentId} ${role} index directory: ${indexDir}.`, treatmentId });
75
127
  }
76
- const commandsDir = incrementalChangeStalenessCommandsDir(runOwnedRoot, scenario.id, treatmentId, role);
77
128
  indexBuildCounts[treatmentId] += 1;
78
- const prepared = await deps.prepareIndexSession({
79
- target: { absoluteTargetRoot: target.targetRoot, sourceRoots: [...sourceRoots] },
129
+ const { evidence } = await prepareTreatmentIndexEvidence({
130
+ deps,
131
+ runOwnedRoot,
132
+ scenarioId: scenario.id,
80
133
  kitCommand,
81
- indexDir,
82
- commandsDir,
83
- requireKit: false
84
- });
85
- if (!prepared.ok) {
86
- fail({
87
- code: role === "baseline" ? "baseline-index-build-failed" : "refresh-index-build-failed",
88
- message: `${treatmentId} ${role} my-dev-kit index build failed: ${prepared.warnings.join(" ") || `exit code ${prepared.build.command.exitCode}`}`,
89
- treatmentId
90
- });
91
- throw new Error("unreachable");
92
- }
93
- // prepareIndexSession resolves only after the index command, snapshot capture, and graph
94
- // loading have all completed, in that order.
95
- events.push(`${treatmentId}:${role}-index-built`, `${treatmentId}:${role}-snapshot-captured`, `${treatmentId}:${role}-graph-loaded`);
96
- const evidence = Object.freeze({
134
+ sourceRoots,
135
+ target,
97
136
  role,
98
- treatmentId,
99
- builtRelativeToMutation: role === "baseline" ? "pre-mutation" : "post-mutation",
100
- indexDir: prepared.session.indexDir,
101
- commandsDir,
102
- targetRoot: prepared.session.targetRoot,
103
- sourceRoots: prepared.session.sourceRoots,
104
- buildDurationMs: prepared.session.buildDurationMs,
105
- buildCommand: prepared.session.buildCommand,
106
- snapshot: prepared.session.indexSnapshot,
107
- graph: prepared.session.affectedNeighborhoodGraph
137
+ indexDir,
138
+ // prepareIndexSession resolves only after the index command, snapshot capture, and graph
139
+ // loading have all completed, in that order.
140
+ onIndexPrepared: () => events.push(`${treatmentId}:${role}-index-built`, `${treatmentId}:${role}-snapshot-captured`, `${treatmentId}:${role}-graph-loaded`)
108
141
  });
109
- fail(evaluateIndexEvidence(evidence, { targetRoot: target.targetRoot, sourceRoots, indexDir }));
110
142
  events.push(`${treatmentId}:${role}-identity-verified`);
111
143
  return evidence;
112
144
  }
@@ -130,7 +130,7 @@ export function evaluateBaselineStaleFreshness(assessment, controlledChangedPath
130
130
  * baseline freshness change evidence must agree path-for-path, including
131
131
  * baseline and current content identity.
132
132
  */
133
- export function evaluateChangedPathSymmetry(stale, fullRefresh) {
133
+ export function evaluateChangedPathSymmetry(stale, fullRefresh, labels = { reference: "stale-index", other: "full-refresh" }) {
134
134
  const key = (assessment) => assessment.changes
135
135
  .map((change) => `${normalizeRelativePath(change.path)}\t${change.changeType}\t${change.baselineSha256}\t${change.currentSha256 ?? "null"}`)
136
136
  .sort();
@@ -138,15 +138,17 @@ export function evaluateChangedPathSymmetry(stale, fullRefresh) {
138
138
  const right = key(fullRefresh);
139
139
  const same = left.length === right.length && left.every((value, index) => value === right[index]);
140
140
  if (!same) {
141
- return failure("treatment-changed-paths-conflict", `stale-index and full-refresh baseline freshness disagree about the controlled change: stale=[${left.join("; ")}] full-refresh=[${right.join("; ")}].`, null);
141
+ return failure("treatment-changed-paths-conflict", `${labels.reference} and ${labels.other} baseline freshness disagree about the controlled change: ${labels.reference}=[${left.join("; ")}] ${labels.other}=[${right.join("; ")}].`, null);
142
142
  }
143
143
  return null;
144
144
  }
145
145
  /** The refreshed index must be proven complete `fresh` against the unchanged mutated target. */
146
- export function evaluateRefreshedFreshness(assessment) {
146
+ export function evaluateRefreshedFreshness(assessment, treatmentId = "full-refresh") {
147
147
  const complete = assessment.baselineSnapshotStatus === "complete" && assessment.unresolvedFileCount === 0;
148
148
  if (assessment.status !== "fresh" || !complete || assessment.changedFileCount !== 0 || assessment.missingFileCount !== 0) {
149
- return failure("refreshed-freshness-not-fresh", `full-refresh refreshed index freshness is ${assessment.status} (snapshot ${assessment.baselineSnapshotStatus}, ${assessment.changedFileCount} changed, ${assessment.missingFileCount} missing, ${assessment.unresolvedFileCount} unresolved); complete fresh is required.`, "full-refresh");
149
+ return failure("refreshed-freshness-not-fresh", `${treatmentId} refreshed index freshness is ${assessment.status} (snapshot ${assessment.baselineSnapshotStatus}, ${assessment.changedFileCount} changed, ${assessment.missingFileCount} missing, ${assessment.unresolvedFileCount} unresolved); complete fresh is required.`, treatmentId);
150
150
  }
151
151
  return null;
152
152
  }
153
+ /** Shared failure constructor for the v0.6.3 lifecycle policy module. */
154
+ export { failure as lifecycleFailure };
@@ -0,0 +1,78 @@
1
+ import { lifecycleFailure } from "./lifecyclePolicy.js";
2
+ /** Pure v0.6.3 four-treatment lifecycle policy; inspects already-produced evidence only. */
3
+ /**
4
+ * v0.6.3: every treatment baseline is bootstrapped with `--incremental --refresh-scope changed-files`
5
+ * against a brand-new index directory. Upstream must truthfully report the expected bootstrap full
6
+ * fallback (`cache-missing`); this is bootstrap evidence, never an experimental partial-refresh fallback.
7
+ */
8
+ export function evaluateBaselineBootstrapEvidence(build, treatmentId) {
9
+ const evidence = build.incrementalRefresh;
10
+ const label = `${treatmentId} baseline bootstrap`;
11
+ if (build.mode.kind !== "incremental" || build.mode.refreshScope !== "changed-files") {
12
+ return lifecycleFailure("baseline-bootstrap-contract-mismatch", `${label} was not requested as incremental changed-files.`, treatmentId);
13
+ }
14
+ if (!evidence) {
15
+ return lifecycleFailure("baseline-bootstrap-contract-mismatch", `${label} returned no incrementalRefresh evidence.`, treatmentId);
16
+ }
17
+ if (evidence.requestedScope !== "changed-files" ||
18
+ evidence.appliedScope !== "full" ||
19
+ evidence.selectionStatus !== "fallback-full" ||
20
+ evidence.fallbackReason !== "cache-missing") {
21
+ return lifecycleFailure("baseline-bootstrap-contract-mismatch", `${label} expected requested=changed-files applied=full status=fallback-full reason=cache-missing but observed requested=${evidence.requestedScope} applied=${evidence.appliedScope} status=${evidence.selectionStatus} reason=${evidence.fallbackReason}.`, treatmentId);
22
+ }
23
+ return null;
24
+ }
25
+ /**
26
+ * v0.6.3: classifies a post-mutation partial-refresh build. A truthful `fallback-full` is a valid
27
+ * realization (FALLBACK_FULL), never collapsed to APPLIED_PARTIAL. `not-needed` after an already
28
+ * proven controlled mutation contradicts that mutation and fails the lifecycle. Missing or invalid
29
+ * evidence fails too.
30
+ */
31
+ export function evaluatePartialRefreshRealization(build, requestedScope, treatmentId) {
32
+ const evidence = build.incrementalRefresh;
33
+ if (build.mode.kind !== "incremental" || build.mode.refreshScope !== requestedScope || !evidence || evidence.requestedScope !== requestedScope) {
34
+ return {
35
+ failure: lifecycleFailure("refresh-evidence-invalid", `${treatmentId} refresh did not carry valid ${requestedScope} incrementalRefresh evidence.`, treatmentId),
36
+ realization: null
37
+ };
38
+ }
39
+ if (evidence.selectionStatus === "not-needed" || evidence.appliedScope === "none") {
40
+ return {
41
+ failure: lifecycleFailure("refresh-not-needed-after-mutation", `${treatmentId} refresh reported not-needed (applied=${evidence.appliedScope}) although the controlled mutation was already proven.`, treatmentId),
42
+ realization: null
43
+ };
44
+ }
45
+ if (evidence.selectionStatus === "applied" && evidence.appliedScope === requestedScope) {
46
+ return { failure: null, realization: "APPLIED_PARTIAL" };
47
+ }
48
+ if (evidence.selectionStatus === "fallback-full" && evidence.appliedScope === "full") {
49
+ return { failure: null, realization: "FALLBACK_FULL" };
50
+ }
51
+ return {
52
+ failure: lifecycleFailure("refresh-evidence-invalid", `${treatmentId} refresh reported an unclassifiable outcome (status=${evidence.selectionStatus}, applied=${evidence.appliedScope}).`, treatmentId),
53
+ realization: null
54
+ };
55
+ }
56
+ /** v0.6.3 per-scenario counts: 7 index invocations and 7 freshness assessments. */
57
+ export const INCREMENTAL_CHANGE_STALENESS_V2_EXPECTED_INDEX_INVOCATIONS = {
58
+ "stale-index": 1,
59
+ "changed-files-refresh": 2,
60
+ "affected-neighborhood-refresh": 2,
61
+ "full-refresh": 2
62
+ };
63
+ export const INCREMENTAL_CHANGE_STALENESS_V2_EXPECTED_FRESHNESS_ASSESSMENTS = {
64
+ "stale-index": 1,
65
+ "changed-files-refresh": 2,
66
+ "affected-neighborhood-refresh": 2,
67
+ "full-refresh": 2
68
+ };
69
+ export function evaluateV2InvocationCounts(indexInvocationCounts, freshnessAssessmentCounts) {
70
+ for (const treatmentId of Object.keys(INCREMENTAL_CHANGE_STALENESS_V2_EXPECTED_INDEX_INVOCATIONS)) {
71
+ const expectedIndex = INCREMENTAL_CHANGE_STALENESS_V2_EXPECTED_INDEX_INVOCATIONS[treatmentId];
72
+ const expectedFreshness = INCREMENTAL_CHANGE_STALENESS_V2_EXPECTED_FRESHNESS_ASSESSMENTS[treatmentId];
73
+ if (indexInvocationCounts[treatmentId] !== expectedIndex || freshnessAssessmentCounts[treatmentId] !== expectedFreshness) {
74
+ return lifecycleFailure("invocation-count-mismatch", `${treatmentId} performed ${indexInvocationCounts[treatmentId]} index invocation(s) and ${freshnessAssessmentCounts[treatmentId]} freshness assessment(s); expected ${expectedIndex} and ${expectedFreshness}.`, treatmentId);
75
+ }
76
+ }
77
+ return null;
78
+ }
@@ -0,0 +1,298 @@
1
+ import path from "node:path";
2
+ import { createDisposableTreatmentTarget, INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_IDS, INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_INTENTS } from "./disposableTarget.js";
3
+ import { cloneIncrementalChangeStalenessIndexDirectory } from "./indexClone.js";
4
+ import { defaultIncrementalChangeStalenessLifecycleDeps, fail, incrementalChangeStalenessIndexDir, incrementalChangeStalenessTargetsRoot, LifecycleFailure, prepareTreatmentIndexEvidence } from "./lifecycle.js";
5
+ import { evaluateBaselineStaleFreshness, evaluateChangedPathSymmetry, evaluateRefreshedFreshness, evaluateToolIdentity, normalizeRelativePath, normalizeRootForComparison } from "./lifecyclePolicy.js";
6
+ import { evaluateBaselineBootstrapEvidence, evaluatePartialRefreshRealization, evaluateV2InvocationCounts } from "./lifecyclePolicyV2.js";
7
+ import { compareIncrementalChangeStalenessSourceStates } from "./sourceState.js";
8
+ import { INCREMENTAL_CHANGE_STALENESS_LIFECYCLE_SESSION_V2_SCHEMA_VERSION } from "./treatmentSessionV2.js";
9
+ const PARTIAL_REFRESH_SCOPES = {
10
+ "changed-files-refresh": "changed-files",
11
+ "affected-neighborhood-refresh": "affected-neighborhood"
12
+ };
13
+ /** Every baseline is bootstrapped incrementally so upstream writes the cache metadata a later partial refresh needs. */
14
+ const BOOTSTRAP_MODE = { kind: "incremental", refreshScope: "changed-files" };
15
+ const V2_TREATMENT_IDS = INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_IDS;
16
+ function withRefreshEvidence(evidence, buildMode, incrementalRefresh) {
17
+ return Object.freeze({ ...evidence, buildMode, incrementalRefresh });
18
+ }
19
+ /**
20
+ * v0.6.3 internal four-treatment lifecycle for ONE frozen scenario (runtime only; not yet wired into the
21
+ * public plugin, execution artifact, comparison, or reports):
22
+ *
23
+ * 1. four independent disposable copies (stale-index, changed-files-refresh, affected-neighborhood-refresh,
24
+ * full-refresh), all derived from the same immutable canonical benchmark;
25
+ * 2. pre-mutation controlled source-state equivalence of every treatment against the stale-index reference;
26
+ * 3. all four baselines bootstrapped with `--incremental --refresh-scope changed-files` into brand-new
27
+ * index directories (expected bootstrap evidence: full fallback, `cache-missing`);
28
+ * 4. each partial treatment's untouched baseline is cloned byte-for-byte into its own refreshed directory;
29
+ * 5. BARRIER: only after every baseline + clone is ready is the same frozen mutation applied to all four;
30
+ * 6. post-mutation source-state equivalence and symmetric complete-stale baseline freshness;
31
+ * 7. stale-index keeps its baseline; each partial treatment refreshes its clone in place and full-refresh
32
+ * builds a new full index; every refreshed index must be complete `fresh`.
33
+ *
34
+ * Seven index invocations and seven freshness assessments per scenario. Performs no retrieval,
35
+ * affected-neighborhood task traversal, or comparison classification. Sequential by design; deletes nothing.
36
+ */
37
+ export async function prepareIncrementalChangeStalenessScenarioLifecycleV2(options) {
38
+ const cloneIndexDirectory = options.deps?.cloneIndexDirectory ?? cloneIncrementalChangeStalenessIndexDirectory;
39
+ const deps = { ...defaultIncrementalChangeStalenessLifecycleDeps, ...options.deps };
40
+ const { repoRoot, scenario, baseCase, kitCommand } = options;
41
+ const runOwnedRoot = path.resolve(options.runOwnedRoot);
42
+ const events = [];
43
+ const indexInvocationCounts = Object.fromEntries(V2_TREATMENT_IDS.map((id) => [id, 0]));
44
+ const freshnessAssessmentCounts = Object.fromEntries(V2_TREATMENT_IDS.map((id) => [id, 0]));
45
+ const sourceRoots = baseCase.sourceRoots;
46
+ const controlledChangedPaths = scenario.mutation.files.map((file) => normalizeRelativePath(file.path)).sort();
47
+ async function prepareIndex(target, role, indexDir, mode, existingIndexDir = false) {
48
+ const treatmentId = target.treatmentId;
49
+ indexInvocationCounts[treatmentId] += 1;
50
+ const { evidence, build } = await prepareTreatmentIndexEvidence({
51
+ deps,
52
+ runOwnedRoot,
53
+ scenarioId: scenario.id,
54
+ kitCommand,
55
+ sourceRoots,
56
+ target,
57
+ role,
58
+ indexDir,
59
+ mode,
60
+ existingIndexDir,
61
+ onIndexPrepared: () => events.push(`${treatmentId}:${role}-index-built`, `${treatmentId}:${role}-snapshot-captured`, `${treatmentId}:${role}-graph-loaded`)
62
+ });
63
+ events.push(`${treatmentId}:${role}-identity-verified`);
64
+ return { evidence, build };
65
+ }
66
+ async function captureState(target) {
67
+ return deps.captureSourceState(target.targetRoot, sourceRoots, target.treatmentId, baseCase.benchmarkProjectId);
68
+ }
69
+ async function mutate(target) {
70
+ const receipt = await deps.executeMutation(scenario, target, sourceRoots, repoRoot);
71
+ if (receipt.status !== "applied") {
72
+ fail({ code: "mutation-failed", message: `${target.treatmentId} mutation was rejected: ${receipt.errors.join(" ")}`, treatmentId: target.treatmentId });
73
+ }
74
+ events.push(`${target.treatmentId}:mutation-applied`);
75
+ return receipt;
76
+ }
77
+ async function assess(index, target) {
78
+ freshnessAssessmentCounts[target.treatmentId] += 1;
79
+ return deps.assessIndexFreshness({ snapshot: index.snapshot, targetRoot: target.targetRoot, sourceRoots });
80
+ }
81
+ function compareAgainstReference(reference, states, code, phase) {
82
+ const comparisons = {};
83
+ for (const treatmentId of V2_TREATMENT_IDS) {
84
+ if (treatmentId === "stale-index")
85
+ continue;
86
+ const comparison = compareIncrementalChangeStalenessSourceStates(reference, states[treatmentId]);
87
+ comparisons[treatmentId] = comparison;
88
+ if (comparison.result !== "equivalent") {
89
+ fail({
90
+ code,
91
+ message: `${treatmentId} is not ${phase} equivalent to the stale-index reference (${comparison.result}): ${comparison.reasons.join("; ")}`,
92
+ treatmentId
93
+ });
94
+ }
95
+ }
96
+ return comparisons;
97
+ }
98
+ try {
99
+ const baselineDirs = {};
100
+ const refreshedDirs = {};
101
+ for (const treatmentId of V2_TREATMENT_IDS) {
102
+ baselineDirs[treatmentId] = incrementalChangeStalenessIndexDir(runOwnedRoot, scenario.id, treatmentId, "baseline");
103
+ refreshedDirs[treatmentId] = incrementalChangeStalenessIndexDir(runOwnedRoot, scenario.id, treatmentId, "refreshed");
104
+ }
105
+ const allDirs = [...Object.values(baselineDirs), ...Object.values(refreshedDirs)].map(normalizeRootForComparison);
106
+ if (new Set(allDirs).size !== allDirs.length) {
107
+ fail({ code: "index-directory-collision", message: "Treatment/phase index directories are not distinct.", treatmentId: null });
108
+ }
109
+ // 1. Four independent disposable copies through the established owner, in fixed order.
110
+ const targets = {};
111
+ for (const treatmentId of V2_TREATMENT_IDS) {
112
+ try {
113
+ targets[treatmentId] = await createDisposableTreatmentTarget({
114
+ repoRoot,
115
+ runRoot: incrementalChangeStalenessTargetsRoot(runOwnedRoot),
116
+ scenarioId: scenario.id,
117
+ treatmentId,
118
+ benchmarkProjectId: baseCase.benchmarkProjectId,
119
+ canonicalProjectRootRelative: baseCase.canonicalProjectRootRelative
120
+ });
121
+ }
122
+ catch (error) {
123
+ fail({ code: "treatment-target-creation-failed", message: `${treatmentId}: ${error.message}`, treatmentId });
124
+ }
125
+ }
126
+ const targetRootKeys = V2_TREATMENT_IDS.map((id) => normalizeRootForComparison(targets[id].targetRoot));
127
+ if (new Set(targetRootKeys).size !== targetRootKeys.length) {
128
+ fail({ code: "treatment-target-creation-failed", message: "Treatment target roots are not distinct.", treatmentId: null });
129
+ }
130
+ events.push("treatment-targets-created");
131
+ // 2. Pre-mutation equivalence gates all indexing and mutation; stale-index is only the comparison reference.
132
+ const preStates = {};
133
+ for (const treatmentId of V2_TREATMENT_IDS)
134
+ preStates[treatmentId] = await captureState(targets[treatmentId]);
135
+ events.push("pre-mutation-source-state-captured");
136
+ const preMutationEquivalence = compareAgainstReference(preStates["stale-index"], preStates, "pre-mutation-not-equivalent", "pre-mutation");
137
+ events.push("pre-mutation-equivalence-proven");
138
+ // 3. Baselines: every treatment bootstraps incrementally into a brand-new index directory.
139
+ const baselines = {};
140
+ const bootstraps = {};
141
+ for (const treatmentId of V2_TREATMENT_IDS) {
142
+ const { evidence, build } = await prepareIndex(targets[treatmentId], "baseline", baselineDirs[treatmentId], BOOTSTRAP_MODE);
143
+ fail(evaluateBaselineBootstrapEvidence(build, treatmentId));
144
+ bootstraps[treatmentId] = build.incrementalRefresh;
145
+ baselines[treatmentId] = withRefreshEvidence(evidence, build.mode, build.incrementalRefresh);
146
+ events.push(`${treatmentId}:baseline-bootstrap-verified`);
147
+ }
148
+ const baselineTools = evaluateToolIdentity(V2_TREATMENT_IDS.map((id) => baselines[id]));
149
+ fail(baselineTools.failure);
150
+ events.push("baseline-tool-identity-verified");
151
+ // 4. Preserve each partial treatment's immutable baseline; the refresh runs in place against a byte-for-byte clone.
152
+ for (const treatmentId of ["changed-files-refresh", "affected-neighborhood-refresh"]) {
153
+ try {
154
+ await cloneIndexDirectory(runOwnedRoot, baselineDirs[treatmentId], refreshedDirs[treatmentId]);
155
+ }
156
+ catch (error) {
157
+ fail({ code: "index-clone-failed", message: `${treatmentId} baseline index clone failed: ${error.message}`, treatmentId });
158
+ }
159
+ events.push(`${treatmentId}:baseline-index-cloned`);
160
+ }
161
+ // 5. Barrier: all four baselines are index + snapshot + graph READY and clones exist. Only now may mutation begin.
162
+ events.push("baseline-barrier-ready");
163
+ const receipts = {};
164
+ for (const treatmentId of V2_TREATMENT_IDS)
165
+ receipts[treatmentId] = await mutate(targets[treatmentId]);
166
+ // 6. Post-mutation equivalence.
167
+ const postStates = {};
168
+ for (const treatmentId of V2_TREATMENT_IDS)
169
+ postStates[treatmentId] = await captureState(targets[treatmentId]);
170
+ events.push("post-mutation-source-state-captured");
171
+ const postMutationEquivalence = compareAgainstReference(postStates["stale-index"], postStates, "post-mutation-not-equivalent", "post-mutation");
172
+ events.push("post-mutation-equivalence-proven");
173
+ // 7. Baseline (change-authority) freshness against each mutated target, symmetric with the reference.
174
+ const baselineFreshness = {};
175
+ for (const treatmentId of V2_TREATMENT_IDS) {
176
+ baselineFreshness[treatmentId] = await assess(baselines[treatmentId], targets[treatmentId]);
177
+ fail(evaluateBaselineStaleFreshness(baselineFreshness[treatmentId], controlledChangedPaths, treatmentId));
178
+ events.push(`${treatmentId}:baseline-freshness-stale`);
179
+ }
180
+ for (const treatmentId of V2_TREATMENT_IDS) {
181
+ if (treatmentId === "stale-index")
182
+ continue;
183
+ fail(evaluateChangedPathSymmetry(baselineFreshness["stale-index"], baselineFreshness[treatmentId], { reference: "stale-index", other: treatmentId }));
184
+ }
185
+ events.push("baseline-changed-paths-symmetric");
186
+ // 8. stale-index: no rebuild; baseline stays the active retrieval index.
187
+ events.push("stale-index:baseline-retained-as-active");
188
+ // 9. Partial treatments: refresh the cloned working index in place through the shared build owner.
189
+ const refreshed = {};
190
+ const refreshedFreshness = {};
191
+ const realizations = {};
192
+ for (const treatmentId of ["changed-files-refresh", "affected-neighborhood-refresh"]) {
193
+ const scope = PARTIAL_REFRESH_SCOPES[treatmentId];
194
+ const { evidence, build } = await prepareIndex(targets[treatmentId], "refreshed", refreshedDirs[treatmentId], { kind: "incremental", refreshScope: scope }, true);
195
+ const realization = evaluatePartialRefreshRealization(build, scope, treatmentId);
196
+ fail(realization.failure);
197
+ realizations[treatmentId] = realization.realization;
198
+ events.push(`${treatmentId}:refresh-realization-${realization.realization === "APPLIED_PARTIAL" ? "applied-partial" : "fallback-full"}`);
199
+ refreshed[treatmentId] = withRefreshEvidence(evidence, build.mode, build.incrementalRefresh);
200
+ refreshedFreshness[treatmentId] = await assess(evidence, targets[treatmentId]);
201
+ fail(evaluateRefreshedFreshness(refreshedFreshness[treatmentId], treatmentId));
202
+ events.push(`${treatmentId}:refreshed-freshness-fresh`);
203
+ }
204
+ // 10. full-refresh: a NEW complete post-mutation index in a separate directory (baseline is not cloned).
205
+ {
206
+ const { evidence, build } = await prepareIndex(targets["full-refresh"], "refreshed", refreshedDirs["full-refresh"], undefined);
207
+ refreshed["full-refresh"] = withRefreshEvidence(evidence, build.mode, build.incrementalRefresh);
208
+ refreshedFreshness["full-refresh"] = await assess(evidence, targets["full-refresh"]);
209
+ fail(evaluateRefreshedFreshness(refreshedFreshness["full-refresh"], "full-refresh"));
210
+ events.push("full-refresh:refreshed-freshness-fresh");
211
+ }
212
+ // 11. Scenario-level proofs.
213
+ const allTools = evaluateToolIdentity([...V2_TREATMENT_IDS.map((id) => baselines[id]), ...V2_TREATMENT_IDS.flatMap((id) => (refreshed[id] ? [refreshed[id]] : []))]);
214
+ fail(allTools.failure);
215
+ events.push("tool-identity-consistent");
216
+ fail(evaluateV2InvocationCounts(indexInvocationCounts, freshnessAssessmentCounts));
217
+ events.push("invocation-counts-verified");
218
+ events.push("lifecycle-ready");
219
+ const common = (treatmentId) => ({
220
+ treatmentIntent: INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_INTENTS[treatmentId],
221
+ target: targets[treatmentId],
222
+ preMutationSourceState: preStates[treatmentId],
223
+ postMutationSourceState: postStates[treatmentId],
224
+ mutationReceipt: receipts[treatmentId],
225
+ changeAuthority: Object.freeze({
226
+ baselineIndex: baselines[treatmentId],
227
+ baselineBootstrap: bootstraps[treatmentId],
228
+ postMutationBaselineFreshness: baselineFreshness[treatmentId]
229
+ }),
230
+ indexInvocationCount: indexInvocationCounts[treatmentId],
231
+ freshnessAssessmentCount: freshnessAssessmentCounts[treatmentId],
232
+ lifecycleStatus: "ready",
233
+ warnings: Object.freeze([])
234
+ });
235
+ const partialSession = (treatmentId) => Object.freeze({
236
+ ...common(treatmentId),
237
+ treatmentId,
238
+ activeRetrieval: Object.freeze({ role: "refreshed", index: refreshed[treatmentId] }),
239
+ postMutationIndexBuilt: true,
240
+ refreshedIndex: refreshed[treatmentId],
241
+ refreshedFreshness: refreshedFreshness[treatmentId],
242
+ refreshRealization: realizations[treatmentId]
243
+ });
244
+ const staleSession = Object.freeze({
245
+ ...common("stale-index"),
246
+ treatmentId: "stale-index",
247
+ activeRetrieval: Object.freeze({ role: "baseline", index: baselines["stale-index"] }),
248
+ postMutationIndexBuilt: false,
249
+ refreshedIndex: null,
250
+ refreshedFreshness: null,
251
+ refreshRealization: null
252
+ });
253
+ const fullSession = Object.freeze({
254
+ ...common("full-refresh"),
255
+ treatmentId: "full-refresh",
256
+ activeRetrieval: Object.freeze({ role: "refreshed", index: refreshed["full-refresh"] }),
257
+ postMutationIndexBuilt: true,
258
+ refreshedIndex: refreshed["full-refresh"],
259
+ refreshedFreshness: refreshedFreshness["full-refresh"],
260
+ refreshRealization: null
261
+ });
262
+ const session = Object.freeze({
263
+ schemaVersion: INCREMENTAL_CHANGE_STALENESS_LIFECYCLE_SESSION_V2_SCHEMA_VERSION,
264
+ scenarioId: scenario.id,
265
+ scenario,
266
+ baseCase,
267
+ runOwnedRoot,
268
+ kitCommand,
269
+ toolIdentity: allTools.tool,
270
+ controlledChangedPaths: Object.freeze(controlledChangedPaths),
271
+ preMutationEquivalence: Object.freeze(preMutationEquivalence),
272
+ postMutationEquivalence: Object.freeze(postMutationEquivalence),
273
+ treatments: Object.freeze({
274
+ "stale-index": staleSession,
275
+ "changed-files-refresh": partialSession("changed-files-refresh"),
276
+ "affected-neighborhood-refresh": partialSession("affected-neighborhood-refresh"),
277
+ "full-refresh": fullSession
278
+ }),
279
+ totalIndexInvocationCount: V2_TREATMENT_IDS.reduce((sum, id) => sum + indexInvocationCounts[id], 0),
280
+ totalFreshnessAssessmentCount: V2_TREATMENT_IDS.reduce((sum, id) => sum + freshnessAssessmentCounts[id], 0),
281
+ lifecycleEvents: Object.freeze([...events]),
282
+ status: "ready"
283
+ });
284
+ return { status: "ready", session };
285
+ }
286
+ catch (error) {
287
+ const failure = error instanceof LifecycleFailure
288
+ ? error.failure
289
+ : { code: "lifecycle-error", message: `Lifecycle failed unexpectedly: ${error instanceof Error ? error.message : String(error)}`, treatmentId: null };
290
+ return {
291
+ status: "failed",
292
+ scenarioId: scenario.id,
293
+ failure,
294
+ lifecycleEvents: Object.freeze([...events]),
295
+ indexInvocationCounts: Object.freeze({ ...indexInvocationCounts })
296
+ };
297
+ }
298
+ }