@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
@@ -7,10 +7,10 @@ import { toAffectedNeighborhoodMetricValues } from "../../../evaluation/affected
7
7
  import { sanitizePathSegment } from "../../outputPaths.js";
8
8
  import { summarizeExperimentRun } from "../../results.js";
9
9
  import { defaultIncrementalChangeStalenessConfig, incrementalChangeStalenessConfigDefinition, validateIncrementalChangeStalenessConfig } from "./config.js";
10
- import { DEFAULT_INCREMENTAL_CHANGE_STALENESS_RUNTIME_ROOT_RELATIVE, INCREMENTAL_CHANGE_STALENESS_TREATMENT_IDS, removeIncrementalChangeStalenessRuntimeRoot } from "./disposableTarget.js";
11
- import { executeIncrementalChangeStalenessScenario } from "./execution.js";
12
- import { buildIncrementalChangeStalenessExecutionArtifact, writeIncrementalChangeStalenessExecutionArtifact } from "./executionArtifact.js";
13
- import { prepareIncrementalChangeStalenessScenarioLifecycle } from "./lifecycle.js";
10
+ import { DEFAULT_INCREMENTAL_CHANGE_STALENESS_RUNTIME_ROOT_RELATIVE, INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_IDS, INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_INTENTS, removeIncrementalChangeStalenessRuntimeRoot } from "./disposableTarget.js";
11
+ import { buildIncrementalChangeStalenessExecutionArtifactV2, writeIncrementalChangeStalenessExecutionArtifactV2 } from "./executionArtifactV2.js";
12
+ import { executeIncrementalChangeStalenessScenarioV2 } from "./executionV2.js";
13
+ import { prepareIncrementalChangeStalenessScenarioLifecycleV2 } from "./lifecycleV2.js";
14
14
  import { BENCHMARK_PROJECT_PROFILES_PATH } from "./scenarioCatalog.js";
15
15
  import { resolveIncrementalChangeStalenessScenarios } from "./scenarioSelection.js";
16
16
  export const INCREMENTAL_CHANGE_STALENESS_PLUGIN_ID = "incremental-change-staleness";
@@ -21,13 +21,39 @@ export const INCREMENTAL_CHANGE_STALENESS_RUN_OWNER_MARKER = ".incremental-chang
21
21
  export const incrementalChangeStalenessMetadata = {
22
22
  id: INCREMENTAL_CHANGE_STALENESS_PLUGIN_ID,
23
23
  name: "Incremental Change Staleness",
24
- description: "Compares stale-index and full-refresh my-dev-kit retrieval after the same frozen controlled source change: matched correctness, required-file evidence, and a conservative stale-risk classification.",
24
+ description: "Compares no refresh, my-dev-kit changed-files incremental refresh, my-dev-kit affected-neighborhood incremental refresh, and full refresh after the same deterministic controlled source change. Comparisons are scoped to the observed evidence (correctness and required-file presence); full refresh is a reference treatment, not an asserted winner, and a partial treatment may truthfully fall back to a full rebuild.",
25
25
  schemaVersion: "1.0.0",
26
26
  status: "experimental",
27
27
  supportedTargets: ["self"],
28
28
  supportedOutputs: ["json", "html"]
29
29
  };
30
+ export const CHANGED_FILES_REFRESH_VARIANT_ID = "changed-files-refresh";
31
+ export const AFFECTED_NEIGHBORHOOD_REFRESH_VARIANT_ID = "affected-neighborhood-refresh";
32
+ /** Public v0.6.3 treatments, in the fixed lifecycle order. Factual descriptions only; no treatment is called best or preferred. */
30
33
  const TREATMENT_VARIANTS = [
34
+ {
35
+ id: STALE_INDEX_VARIANT_ID,
36
+ name: "No refresh (stale index)",
37
+ description: "Keeps the pre-mutation baseline index and performs no post-mutation refresh."
38
+ },
39
+ {
40
+ id: CHANGED_FILES_REFRESH_VARIANT_ID,
41
+ name: "Changed-files refresh",
42
+ description: "Uses my-dev-kit's incremental changed-files refresh after the controlled mutation; upstream may truthfully fall back to a full rebuild."
43
+ },
44
+ {
45
+ id: AFFECTED_NEIGHBORHOOD_REFRESH_VARIANT_ID,
46
+ name: "Affected-neighborhood refresh",
47
+ description: "Uses my-dev-kit's affected-neighborhood incremental refresh after the controlled mutation; upstream may truthfully fall back to a full rebuild."
48
+ },
49
+ {
50
+ id: FULL_REFRESH_VARIANT_ID,
51
+ name: "Full refresh",
52
+ description: "Builds a complete post-mutation index and uses it as the comparison reference."
53
+ }
54
+ ];
55
+ /** The released v0.6.2 two-treatment variants, kept only for the historical V1 run mapper. */
56
+ const V1_TREATMENT_VARIANTS = [
31
57
  {
32
58
  id: STALE_INDEX_VARIANT_ID,
33
59
  name: "Stale index",
@@ -78,7 +104,7 @@ export const incrementalChangeStalenessPlugin = {
78
104
  metadata: incrementalChangeStalenessMetadata,
79
105
  defaultConfig: defaultIncrementalChangeStalenessConfig,
80
106
  configDefinition: incrementalChangeStalenessConfigDefinition,
81
- supportedVariants: [...INCREMENTAL_CHANGE_STALENESS_TREATMENT_IDS],
107
+ supportedVariants: [...INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_IDS],
82
108
  validateConfig: validateIncrementalChangeStalenessConfig,
83
109
  async run(context) {
84
110
  if (!context.target.isSelf) {
@@ -94,14 +120,14 @@ export const incrementalChangeStalenessPlugin = {
94
120
  const projectProfiles = await readBenchmarkProjectProfiles(path.resolve(context.toolRoot, BENCHMARK_PROJECT_PROFILES_PATH), context.toolRoot);
95
121
  const executions = [];
96
122
  for (const resolved of resolvedScenarios) {
97
- const lifecycle = await prepareIncrementalChangeStalenessScenarioLifecycle({
123
+ const lifecycle = await prepareIncrementalChangeStalenessScenarioLifecycleV2({
98
124
  repoRoot: context.toolRoot,
99
125
  runOwnedRoot,
100
126
  scenario: resolved.scenario,
101
127
  baseCase: resolved.baseCase,
102
128
  kitCommand: context.config.kitCommand
103
129
  });
104
- const execution = await executeIncrementalChangeStalenessScenario({
130
+ const execution = await executeIncrementalChangeStalenessScenarioV2({
105
131
  repoRoot: context.toolRoot,
106
132
  scenario: resolved.scenario,
107
133
  lifecycle,
@@ -113,15 +139,15 @@ export const incrementalChangeStalenessPlugin = {
113
139
  executions.push(execution);
114
140
  }
115
141
  const outDir = context.outputRoot ?? path.resolve(context.toolRoot, context.config.outDir);
116
- const artifact = buildIncrementalChangeStalenessExecutionArtifact({
142
+ const artifact = buildIncrementalChangeStalenessExecutionArtifactV2({
117
143
  runId: context.runId,
118
144
  pluginId: INCREMENTAL_CHANGE_STALENESS_PLUGIN_ID,
119
145
  executions
120
146
  });
121
147
  // The artifact is written before the framework's separate `cleanup` phase runs, so it is
122
148
  // written before the run-owned ephemeral index/target state is ever destroyed.
123
- const artifactPath = await writeIncrementalChangeStalenessExecutionArtifact(outDir, artifact);
124
- return mapIncrementalChangeStalenessExecutionsToRun({
149
+ const artifactPath = await writeIncrementalChangeStalenessExecutionArtifactV2(outDir, artifact);
150
+ return mapIncrementalChangeStalenessExecutionsToRunV2({
125
151
  runId: context.runId,
126
152
  startedAt,
127
153
  completedAt: new Date().toISOString(),
@@ -239,7 +265,7 @@ export function mapIncrementalChangeStalenessExecutionsToRun(args) {
239
265
  completedAt: args.completedAt,
240
266
  status,
241
267
  target: args.target,
242
- variants: TREATMENT_VARIANTS.map((variant) => ({ ...variant })),
268
+ variants: V1_TREATMENT_VARIANTS.map((variant) => ({ ...variant })),
243
269
  cases,
244
270
  metrics: [
245
271
  { id: "incremental-change-staleness-scenario-count", name: "Scenario count", value: args.executions.length, unit: "count", description: "Selected scenarios in this run." },
@@ -272,3 +298,178 @@ export function mapIncrementalChangeStalenessExecutionsToRun(args) {
272
298
  run.summary = summarizeExperimentRun(run);
273
299
  return run;
274
300
  }
301
+ // ---------------------------------------------------------------------------
302
+ // v0.6.3 four-treatment mapping. Maps already-executed V2 evidence; recomputes nothing.
303
+ // ---------------------------------------------------------------------------
304
+ const UPSTREAM_REFRESH_METRIC_SPECS = [
305
+ { id: "upstream-refresh-fresh-extraction-file-count", name: "Upstream fresh-extraction files", field: "freshExtractionFileCount", description: "Files my-dev-kit freshly re-extracted during the refresh invocation (upstream execution evidence, not Lab task overlap)." },
306
+ { id: "upstream-refresh-reused-file-count", name: "Upstream reused files", field: "reusedFileCount", description: "Files my-dev-kit reused from the previous index during the refresh invocation (upstream execution evidence)." },
307
+ { id: "upstream-refresh-forced-neighbor-reanalysis-file-count", name: "Upstream forced-neighbor reanalysis files", field: "forcedNeighborReanalysisFileCount", description: "Neighbor files my-dev-kit forced to re-analyze during the refresh invocation (upstream execution evidence)." },
308
+ { id: "upstream-refresh-seed-file-count", name: "Upstream seed files", field: "seedFileCount", description: "Seed files my-dev-kit selected for the affected-neighborhood refresh (upstream execution evidence; absent when upstream reported none)." },
309
+ { id: "upstream-refresh-seed-symbol-count", name: "Upstream seed symbols", field: "seedSymbolCount", description: "Seed symbols my-dev-kit selected for the affected-neighborhood refresh (upstream execution evidence; absent when upstream reported none)." },
310
+ { id: "upstream-refresh-affected-node-count", name: "Upstream affected nodes", field: "affectedNodeCount", description: "Graph nodes my-dev-kit treated as affected during the refresh (upstream execution evidence, distinct from the Lab affected-neighborhood metrics)." },
311
+ { id: "upstream-refresh-affected-edge-count", name: "Upstream affected edges", field: "affectedEdgeCount", description: "Graph edges my-dev-kit treated as affected during the refresh (upstream execution evidence, distinct from the Lab affected-neighborhood metrics)." }
312
+ ];
313
+ function upstreamRefreshMetrics(treatment, caseId) {
314
+ const refresh = treatment.refreshExecution;
315
+ if (refresh.kind !== "incremental")
316
+ return [];
317
+ const metrics = [];
318
+ for (const spec of UPSTREAM_REFRESH_METRIC_SPECS) {
319
+ const value = refresh.incrementalRefresh[spec.field];
320
+ // Null upstream fields are unavailable evidence, never a zero-valued metric.
321
+ if (typeof value === "number")
322
+ metrics.push(metric(spec.id, spec.name, spec.description, "count", value, treatment.treatmentId, caseId));
323
+ }
324
+ return metrics;
325
+ }
326
+ function comparisonMetadata(execution, treatmentId) {
327
+ const comparison = execution.referenceComparisons.find((entry) => entry.candidateTreatmentId === treatmentId);
328
+ if (!comparison)
329
+ return {};
330
+ if (comparison.kind === "stale-risk") {
331
+ return {
332
+ comparisonCorrectnessRelation: comparison.comparison.correctnessRelation,
333
+ comparisonRequiredFileEvidenceRelation: comparison.comparison.requiredFileEvidenceRelation,
334
+ staleRiskClassification: comparison.comparison.staleRiskClassification
335
+ };
336
+ }
337
+ return {
338
+ referenceComparisonCorrectnessRelation: comparison.correctnessRelation,
339
+ referenceComparisonRequiredFileEvidenceRelation: comparison.requiredFileEvidenceRelation,
340
+ referenceComparisonClassification: comparison.classification
341
+ };
342
+ }
343
+ function treatmentOutcomeV2(execution, treatmentId) {
344
+ const caseId = execution.scenarioId;
345
+ const treatmentIntent = INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_INTENTS[treatmentId];
346
+ const base = { id: `${caseId}:${treatmentId}`, caseId, variantId: treatmentId, artifacts: [] };
347
+ const treatment = execution.treatments.find((entry) => entry.treatmentId === treatmentId);
348
+ if (execution.status === "failed" || !treatment) {
349
+ return {
350
+ ...base,
351
+ status: "failed",
352
+ metrics: [],
353
+ warnings: [],
354
+ failures: [
355
+ {
356
+ code: "incremental-change-staleness-scenario-failed",
357
+ message: execution.failureReason ?? "Scenario execution failed before treatment evidence was produced.",
358
+ variantId: treatmentId,
359
+ caseId,
360
+ recoverable: false
361
+ }
362
+ ],
363
+ metadata: { treatmentId, treatmentIntent, scenarioStatus: execution.status }
364
+ };
365
+ }
366
+ const status = treatment.status === "completed" ? "completed" : treatment.status === "failed" ? "failed" : "partial";
367
+ const refresh = treatment.refreshExecution;
368
+ const incremental = refresh.kind === "incremental" ? refresh.incrementalRefresh : null;
369
+ return {
370
+ ...base,
371
+ status,
372
+ metrics: [...treatmentMetrics(treatment, caseId), ...upstreamRefreshMetrics(treatment, caseId)],
373
+ warnings: treatment.retrievalStatus === "failed" || treatment.fakeAgent?.status === "failed"
374
+ ? [{ code: "incremental-change-staleness-treatment-partial", message: treatment.failureReason ?? "Treatment retrieval or fake-agent evaluation did not fully complete.", variantId: treatmentId, caseId }]
375
+ : [],
376
+ failures: [],
377
+ metadata: {
378
+ treatmentId,
379
+ treatmentIntent,
380
+ activeIndexPhase: treatment.activeIndexPhase,
381
+ treatmentStatus: treatment.status,
382
+ retrievalStatus: treatment.retrievalStatus,
383
+ fakeAgentStatus: treatment.fakeAgent?.status ?? "not-run",
384
+ requiredFileEvidenceStatus: treatment.requiredFileEvidence.status,
385
+ affectedNeighborhoodRelationship: treatment.affectedNeighborhood.relationship,
386
+ affectedNeighborhoodReindexRecommendation: treatment.affectedNeighborhood.reindexRecommendation,
387
+ refreshKind: refresh.kind,
388
+ refreshRealization: refresh.realization,
389
+ // Upstream categorical evidence only for incremental treatments; never invented for the others.
390
+ ...(incremental
391
+ ? {
392
+ requestedScope: incremental.requestedScope,
393
+ appliedScope: incremental.appliedScope,
394
+ selectionStatus: incremental.selectionStatus,
395
+ fallbackReason: incremental.fallbackReason
396
+ }
397
+ : {}),
398
+ ...comparisonMetadata(execution, treatmentId)
399
+ }
400
+ };
401
+ }
402
+ function countByRealization(executions, treatmentId, realization) {
403
+ return executions.filter((execution) => execution.treatments.find((treatment) => treatment.treatmentId === treatmentId)?.refreshExecution.realization === realization).length;
404
+ }
405
+ export function mapIncrementalChangeStalenessExecutionsToRunV2(args) {
406
+ const cases = args.executions.map((execution) => {
407
+ const partialClass = (candidate) => {
408
+ const comparison = execution.referenceComparisons.find((entry) => entry.candidateTreatmentId === candidate);
409
+ return comparison && comparison.kind === "partial-refresh" ? comparison.classification : null;
410
+ };
411
+ const stale = execution.referenceComparisons.find((entry) => entry.candidateTreatmentId === "stale-index");
412
+ return {
413
+ id: execution.scenarioId,
414
+ name: `${execution.scenarioId} (${execution.category})`,
415
+ // Exactly four outcomes per scenario, in the fixed lifecycle order (never a "raw" outcome).
416
+ outcomes: INCREMENTAL_CHANGE_STALENESS_V2_TREATMENT_IDS.map((treatmentId) => treatmentOutcomeV2(execution, treatmentId)),
417
+ metadata: {
418
+ category: execution.category,
419
+ benchmarkProject: execution.benchmarkProjectId,
420
+ baseCaseId: execution.baseCaseId,
421
+ scenarioStatus: execution.status,
422
+ staleRiskClassification: stale && stale.kind === "stale-risk" ? stale.comparison.staleRiskClassification : null,
423
+ changedFilesReferenceClassification: partialClass("changed-files-refresh"),
424
+ affectedNeighborhoodReferenceClassification: partialClass("affected-neighborhood-refresh")
425
+ }
426
+ };
427
+ });
428
+ const outcomeStatuses = cases.flatMap((experimentCase) => experimentCase.outcomes.map((outcome) => outcome.status));
429
+ const status = outcomeStatuses.length > 0 && outcomeStatuses.every((value) => value === "failed")
430
+ ? "failed"
431
+ : outcomeStatuses.every((value) => value === "completed")
432
+ ? "completed"
433
+ : "partial";
434
+ const count = (id, name, value, description) => ({ id, name, value, unit: "count", description });
435
+ const run = {
436
+ runId: args.runId,
437
+ pluginId: INCREMENTAL_CHANGE_STALENESS_PLUGIN_ID,
438
+ startedAt: args.startedAt,
439
+ completedAt: args.completedAt,
440
+ status,
441
+ target: args.target,
442
+ variants: TREATMENT_VARIANTS.map((variant) => ({ ...variant })),
443
+ cases,
444
+ metrics: [
445
+ count("incremental-change-staleness-scenario-count", "Scenario count", args.executions.length, "Selected scenarios in this run."),
446
+ count("incremental-change-staleness-observed-regression-count", "Observed stale regression count", args.executions.filter((execution) => {
447
+ const stale = execution.referenceComparisons.find((entry) => entry.candidateTreatmentId === "stale-index");
448
+ return stale?.kind === "stale-risk" && stale.comparison.staleRiskClassification === "observed-stale-regression";
449
+ }).length, "Scenarios whose no-refresh treatment was classified observed-stale-regression against full refresh."),
450
+ count("incremental-change-staleness-changed-files-applied-partial-count", "Changed-files applied-partial count", countByRealization(args.executions, "changed-files-refresh", "APPLIED_PARTIAL"), "Scenarios where the changed-files refresh was applied as a partial refresh."),
451
+ count("incremental-change-staleness-changed-files-fallback-full-count", "Changed-files fallback-full count", countByRealization(args.executions, "changed-files-refresh", "FALLBACK_FULL"), "Scenarios where the changed-files refresh fell back to a full rebuild."),
452
+ count("incremental-change-staleness-affected-neighborhood-applied-partial-count", "Affected-neighborhood applied-partial count", countByRealization(args.executions, "affected-neighborhood-refresh", "APPLIED_PARTIAL"), "Scenarios where the affected-neighborhood refresh was applied as a partial refresh."),
453
+ count("incremental-change-staleness-affected-neighborhood-fallback-full-count", "Affected-neighborhood fallback-full count", countByRealization(args.executions, "affected-neighborhood-refresh", "FALLBACK_FULL"), "Scenarios where the affected-neighborhood refresh fell back to a full rebuild.")
454
+ ],
455
+ artifacts: [
456
+ {
457
+ id: "incremental-change-staleness-execution",
458
+ label: "Incremental-change-staleness execution evidence",
459
+ path: args.artifactPath,
460
+ kind: "artifact",
461
+ mimeType: "application/json",
462
+ description: "Per-scenario four-treatment execution, refresh-realization, and full-refresh reference-comparison evidence, without raw context bodies."
463
+ }
464
+ ],
465
+ warnings: [],
466
+ failures: [],
467
+ metadata: {
468
+ kitCommand: args.kitCommand,
469
+ scenarioIds: args.executions.map((execution) => execution.scenarioId),
470
+ executionArtifactPath: args.artifactPath
471
+ }
472
+ };
473
+ run.summary = summarizeExperimentRun(run);
474
+ return run;
475
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * v0.6.3 in-memory (runtime-only) four-treatment lifecycle session schema version. This is NOT the
3
+ * persisted execution artifact; persistence is a later batch.
4
+ */
5
+ export const INCREMENTAL_CHANGE_STALENESS_LIFECYCLE_SESSION_V2_SCHEMA_VERSION = "2.0.0";
@@ -1,3 +1,4 @@
1
1
  export * from "./contextStrategyComparison/index.js";
2
+ export * from "./contextWindowScaling/index.js";
2
3
  export * from "./incrementalChangeStaleness/index.js";
3
4
  export * from "./warmIndexReuse/index.js";
@@ -41,7 +41,7 @@ export async function evaluateWarmIndexFakeAgents(options) {
41
41
  }
42
42
  return result;
43
43
  }
44
- async function evaluateFakeSide(args) {
44
+ export async function evaluateFakeSide(args) {
45
45
  const promptStrategy = SIDE_STRATEGIES[args.variantId];
46
46
  const base = { variantId: args.variantId, agentId: "fake-agent", promptStrategy };
47
47
  try {
@@ -5,7 +5,9 @@ import { loadAffectedNeighborhoodGraphEvidence } from "../../../evaluation/affec
5
5
  import { captureIndexSnapshot } from "../../../evaluation/indexSnapshot.js";
6
6
  /**
7
7
  * Builds the index once through the shared `buildMyDevKitIndex` owner. A build that reports
8
- * success but leaves no index directory is not treated as a prepared session.
8
+ * success but leaves no index directory is not treated as a prepared session. `mode` is optional
9
+ * and defaults to an ordinary full build; only callers that explicitly request an incremental
10
+ * build mode get one.
9
11
  */
10
12
  export async function prepareWarmIndexSession(options) {
11
13
  const build = await buildMyDevKitIndex(options);
@@ -19,14 +19,21 @@ function buildCandidateEvidenceId(ruleId, location, fingerprint) {
19
19
  const locationPart = [location.path, location.line !== undefined ? `l${location.line}` : undefined, location.column !== undefined ? `c${location.column}` : undefined]
20
20
  .filter((part) => Boolean(part))
21
21
  .join(":");
22
- return [ruleId, slug(locationPart), fingerprint.slice(0, 16)].join("--");
22
+ const payloadStart = fingerprint.indexOf(":") + 1;
23
+ return [ruleId, slug(locationPart), fingerprint.slice(payloadStart, payloadStart + 16)].join("--");
23
24
  }
24
25
  // Never accepts a raw secret value as a plain string parameter name to
25
26
  // discourage accidental logging at call sites — `rawValue` is redacted and
26
27
  // fingerprinted here, once, and the raw value is never stored on the
27
28
  // returned object.
28
29
  export function makeCandidateEvidence(input) {
29
- const fingerprint = fingerprintCandidateValue(input.rawValue);
30
+ const fingerprint = fingerprintCandidateValue(input.rawValue, {
31
+ ruleId: input.ruleId,
32
+ sourcePath: input.location.path,
33
+ line: input.location.line,
34
+ column: input.location.column,
35
+ purpose: "candidate-evidence"
36
+ });
30
37
  const redactedPreview = redactedPreviewForCandidate(input.rawValue);
31
38
  const id = buildCandidateEvidenceId(input.ruleId, input.location, fingerprint);
32
39
  return {
@@ -1,4 +1,5 @@
1
- import { createHash } from "node:crypto";
1
+ import { scryptSync } from "node:crypto";
2
+ import path from "node:path";
2
3
  import { stripUnsafeControlChars } from "../../../securityValidation/attackScenarios/exploitEvidence.js";
3
4
  // ---------------------------------------------------------------------------
4
5
  // v0.4.1 Batch 1 — redaction and fingerprint primitives for later Android
@@ -13,8 +14,8 @@ import { stripUnsafeControlChars } from "../../../securityValidation/attackScena
13
14
  // payload). Android candidate evidence has a different, narrower need: a
14
15
  // human reviewing a static-candidate finding benefits from a short prefix/
15
16
  // suffix so they can recognize *which* secret in their own source it is
16
- // without the full value ever being reconstructable — so this module adds
17
- // that prefix/suffix-preserving preview and a non-reversible fingerprint on
17
+ // without retaining the full value — so this module adds that
18
+ // prefix/suffix-preserving preview and a one-way fingerprint on
18
19
  // top of the shared control-character stripping, instead of creating a
19
20
  // second, competing full-redaction implementation.
20
21
  // ---------------------------------------------------------------------------
@@ -42,13 +43,21 @@ export function redactedPreviewForCandidate(input, options = {}) {
42
43
  const suffix = bounded.slice(-edgeLength);
43
44
  return `${prefix}${MASK}${suffix}`;
44
45
  }
45
- // Stable, non-reversible fingerprint for deduplication. Uses Node's built-in
46
- // sha256 (no new dependency). Distinct from redactedPreviewForCandidate: the
47
- // fingerprint is stored for dedup, the preview is stored for human review —
48
- // neither one can be used to recover the raw value.
49
- export function fingerprintCandidateValue(input) {
46
+ // Deterministic, one-way evidence identity. Scrypt makes guessing more costly
47
+ // than a fast hash, but low-entropy values are not cryptographically
48
+ // unrecoverable. The context salt separates equivalent values at distinct
49
+ // evidence locations without storing a secret salt or the raw value.
50
+ export function fingerprintCandidateValue(input, context) {
50
51
  if (input === undefined)
51
52
  return "unavailable";
52
- const hash = createHash("sha256").update(input, "utf8").digest("hex");
53
- return `sha256:${hash}`;
53
+ const contextSalt = Buffer.from(JSON.stringify([
54
+ "my-dev-kit-lab/android-candidate-fingerprint/v2",
55
+ context.ruleId,
56
+ path.posix.normalize(context.sourcePath.replaceAll("\\", "/")),
57
+ context.line ?? null,
58
+ context.column ?? null,
59
+ context.purpose
60
+ ]), "utf8");
61
+ const derived = scryptSync(input, contextSalt, 32, { N: 16384, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
62
+ return `scrypt-v1:${derived.toString("hex")}`;
54
63
  }
@@ -51,7 +51,6 @@ export function analyzeSecretCandidateFile(targetRoot, file) {
51
51
  const fileKind = fileKindFor(file.relativePath);
52
52
  for (const block of matchPrivateKeyBlocks(file.content)) {
53
53
  const location = buildAndroidSourceLocation(targetRoot, file.absolutePath, { line: block.line });
54
- const fingerprint = fingerprintCandidateValue(block.rawBlock);
55
54
  if (!block.terminated) {
56
55
  candidates.push(makeCandidateEvidence({
57
56
  ruleId: "android-secret-private-key-candidate",
@@ -66,6 +65,12 @@ export function analyzeSecretCandidateFile(targetRoot, file) {
66
65
  }));
67
66
  continue;
68
67
  }
68
+ const fingerprint = fingerprintCandidateValue(block.rawBlock, {
69
+ ruleId: "android-secret-private-key-candidate",
70
+ sourcePath: location.path,
71
+ line: location.line,
72
+ purpose: "private-key-finding"
73
+ });
69
74
  findings.push(makeAndroidFinding({
70
75
  ruleId: "android-secret-private-key-candidate",
71
76
  title: `Private key material found (${block.headerLabel})`,
@@ -98,7 +103,12 @@ export function analyzeSecretCandidateFile(targetRoot, file) {
98
103
  }));
99
104
  continue;
100
105
  }
101
- const fingerprint = fingerprintCandidateValue(match.rawValue);
106
+ const fingerprint = fingerprintCandidateValue(match.rawValue, {
107
+ ruleId: classification.ruleId,
108
+ sourcePath: location.path,
109
+ line: location.line,
110
+ purpose: `credential-finding:${classification.sensitiveDataCategory}`
111
+ });
102
112
  findings.push(makeAndroidFinding({
103
113
  ruleId: classification.ruleId,
104
114
  title: `Hardcoded ${match.identifier} literal found`,
@@ -149,7 +149,7 @@ export function analyzeModuleSigningConfiguration(targetRoot, gradleModule, buil
149
149
  const buildFileRelativePath = gradleModule.buildFilePath ?? modulePath;
150
150
  const buildFileAbsolutePath = path.join(targetRoot, buildFileRelativePath);
151
151
  const location = loc(targetRoot, buildFileAbsolutePath);
152
- const signingConfigs = extractSigningConfigurations(buildFileText);
152
+ const signingConfigs = extractSigningConfigurations(buildFileText, { sourcePath: location.path });
153
153
  const literalCredentialConfigNames = new Set();
154
154
  for (const config of signingConfigs) {
155
155
  const storePasswordResult = analyzeCredential("storePassword", config.storePassword, config.name, modulePath, location, buildFileRelativePath);
@@ -44,17 +44,17 @@ function extractStoreFileValue(content) {
44
44
  return { state: "literal", literalValue: fileWrapped[1] };
45
45
  return extractPathValue(content, ["storeFile"]);
46
46
  }
47
- function extractCredentialValue(content, keys) {
47
+ function extractCredentialValue(content, keys, context) {
48
48
  const result = extractLiteral(content, keys);
49
49
  const state = classifyExpressionState(result);
50
50
  if (state === "literal" && result.value !== undefined) {
51
- return { state, redactedPreview: redactedPreviewForCandidate(result.value), fingerprint: fingerprintCandidateValue(result.value) };
51
+ return { state, redactedPreview: redactedPreviewForCandidate(result.value), fingerprint: fingerprintCandidateValue(result.value, context) };
52
52
  }
53
53
  if (result.raw !== undefined)
54
54
  return { state, rawExpression: boundedRawExpression(result.raw) };
55
55
  return { state: "missing" };
56
56
  }
57
- function extractSigningConfigEntry(name, content) {
57
+ function extractSigningConfigEntry(name, content, sourcePath) {
58
58
  const v1 = extractBooleanLiteral(content, ["enableV1Signing", "v1SigningEnabled"]);
59
59
  const v2 = extractBooleanLiteral(content, ["enableV2Signing", "v2SigningEnabled"]);
60
60
  const v3 = extractBooleanLiteral(content, ["enableV3Signing", "v3SigningEnabled"]);
@@ -62,9 +62,9 @@ function extractSigningConfigEntry(name, content) {
62
62
  return {
63
63
  name,
64
64
  storeFile: extractStoreFileValue(content),
65
- storePassword: extractCredentialValue(content, ["storePassword"]),
65
+ storePassword: extractCredentialValue(content, ["storePassword"], { ruleId: "android-signing-password-literal", sourcePath, purpose: `signing-credential:${name}:storePassword` }),
66
66
  keyAlias: extractPathValue(content, ["keyAlias"]),
67
- keyPassword: extractCredentialValue(content, ["keyPassword"]),
67
+ keyPassword: extractCredentialValue(content, ["keyPassword"], { ruleId: "android-signing-password-literal", sourcePath, purpose: `signing-credential:${name}:keyPassword` }),
68
68
  enableV1Signing: v1.value,
69
69
  enableV2Signing: v2.value,
70
70
  enableV3Signing: v3.value,
@@ -73,9 +73,9 @@ function extractSigningConfigEntry(name, content) {
73
73
  }
74
74
  // Extracts every named entry inside a module build file's `signingConfigs { }`
75
75
  // block. Returns an empty array (never throws) when no such block exists.
76
- export function extractSigningConfigurations(rawBuildFileText) {
76
+ export function extractSigningConfigurations(rawBuildFileText, context) {
77
77
  const text = stripComments(rawBuildFileText);
78
78
  return extractNamedSubBlocks(text, "signingConfigs")
79
- .map(({ name, content }) => extractSigningConfigEntry(name, content))
79
+ .map(({ name, content }) => extractSigningConfigEntry(name, content, context.sourcePath))
80
80
  .sort((a, b) => a.name.localeCompare(b.name));
81
81
  }
@@ -0,0 +1,96 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { CONTEXT_WINDOW_SCALING_EXECUTION_ARTIFACT_FILE } from "../experiments/plugins/contextWindowScaling/executionArtifact.js";
5
+ import { parseContextWindowScalingExecutionArtifact } from "../experiments/plugins/contextWindowScaling/executionArtifactReader.js";
6
+ import { aggregateContextWindowScaling } from "../experiments/plugins/contextWindowScaling/metrics.js";
7
+ import { STANDARD_CONTEXT_BUDGETS } from "../experiments/plugins/contextWindowScaling/contextBudget.js";
8
+ // ---------------------------------------------------------------------------
9
+ // Context-window-scaling plot data. Maps values already calculated by
10
+ // aggregateContextWindowScaling() (carried on ContextWindowScalingRun.aggregate) to plot
11
+ // points. Nothing is recalculated; a null aggregate value becomes a skipped point, never 0.
12
+ // ---------------------------------------------------------------------------
13
+ export const CONTEXT_WINDOW_SCALING_PLOT_IDS = [
14
+ "context-window-scaling-context-size",
15
+ "context-window-scaling-success-rate-by-budget",
16
+ "context-window-scaling-correctness-by-budget",
17
+ ];
18
+ export const CONTEXT_WINDOW_SCALING_SKIP_REASONS = {
19
+ contextSize: "context-size-unavailable",
20
+ successRate: "success-rate-unavailable",
21
+ correctness: "correctness-unavailable",
22
+ };
23
+ /** Standard budgets read as 8k/16k/32k/64k; any other budget keeps its exact integer form. */
24
+ export function formatContextBudgetLabel(budget) {
25
+ return STANDARD_CONTEXT_BUDGETS.includes(budget) ? `${budget / 1024}k` : String(budget);
26
+ }
27
+ /**
28
+ * Returns the aggregate rebuilt from `<experimentDir>/context-window-scaling-execution.json`, or null
29
+ * when that artifact is absent. A present artifact is validated (schema version and structure) and
30
+ * aggregated by the canonical aggregateContextWindowScaling(); malformed input fails, never falls back.
31
+ */
32
+ export async function readContextWindowScalingPlotSource(experimentDir) {
33
+ const artifactPath = path.join(experimentDir, CONTEXT_WINDOW_SCALING_EXECUTION_ARTIFACT_FILE);
34
+ if (!existsSync(artifactPath))
35
+ return null;
36
+ let parsed;
37
+ try {
38
+ parsed = JSON.parse(await readFile(artifactPath, "utf8"));
39
+ }
40
+ catch (error) {
41
+ throw new Error(`Invalid context-window-scaling execution artifact (${artifactPath}): not valid JSON (${error instanceof Error ? error.message : String(error)}).`);
42
+ }
43
+ const artifact = parseContextWindowScalingExecutionArtifact(parsed);
44
+ return {
45
+ aggregate: aggregateContextWindowScaling({ contextBudgets: artifact.contextBudgets, cases: artifact.cases }),
46
+ generatedAt: artifact.completedAt,
47
+ };
48
+ }
49
+ export function buildContextWindowScalingPlotData(args) {
50
+ const { aggregate } = args;
51
+ const skippedPoints = [];
52
+ const series = (id, title, xLabel, yLabel) => ({
53
+ id,
54
+ title,
55
+ xLabel,
56
+ yLabel,
57
+ kind: "scatter",
58
+ points: [],
59
+ warnings: [],
60
+ });
61
+ const contextSize = series(CONTEXT_WINDOW_SCALING_PLOT_IDS[0], "Raw vs Retrieved Context Size", "Case ordinal (canonical case order)", "Estimated context tokens");
62
+ const successRate = series(CONTEXT_WINDOW_SCALING_PLOT_IDS[1], "Success Rate by Context Budget", "Context budget (estimated tokens)", "Success rate (percent)");
63
+ const correctness = series(CONTEXT_WINDOW_SCALING_PLOT_IDS[2], "Correctness by Context Budget", "Context budget (estimated tokens)", "Mean correctness score");
64
+ const add = (plot, x, value, group, label, reason, metadata) => {
65
+ if (value !== null && Number.isFinite(value)) {
66
+ plot.points.push({ x, y: value, group, label, metadata });
67
+ }
68
+ else {
69
+ skippedPoints.push({ plotId: plot.id, label, reason });
70
+ }
71
+ };
72
+ // Case ordinal follows the aggregate's canonical case order (first appearance of each caseId).
73
+ const caseOrdinals = new Map();
74
+ for (const summary of aggregate.caseTreatmentContextSummaries) {
75
+ if (!caseOrdinals.has(summary.caseId))
76
+ caseOrdinals.set(summary.caseId, caseOrdinals.size + 1);
77
+ }
78
+ for (const summary of aggregate.caseTreatmentContextSummaries) {
79
+ add(contextSize, caseOrdinals.get(summary.caseId), summary.contextStatus === "available" ? summary.estimatedTokens : null, summary.variantId, `${summary.caseId} ${summary.variantId}`, CONTEXT_WINDOW_SCALING_SKIP_REASONS.contextSize, { caseId: summary.caseId });
80
+ }
81
+ for (const summary of aggregate.budgetTreatmentSummaries) {
82
+ const label = `${summary.variantId} ${formatContextBudgetLabel(summary.contextBudgetTokens)}`;
83
+ const metadata = { contextBudgetTokens: summary.contextBudgetTokens };
84
+ add(successRate, summary.contextBudgetTokens, summary.successRatePercent, summary.variantId, label, CONTEXT_WINDOW_SCALING_SKIP_REASONS.successRate, metadata);
85
+ add(correctness, summary.contextBudgetTokens, summary.meanCorrectnessScore, summary.variantId, label, CONTEXT_WINDOW_SCALING_SKIP_REASONS.correctness, metadata);
86
+ }
87
+ const plots = [contextSize, successRate, correctness];
88
+ const warnings = [];
89
+ for (const plot of plots) {
90
+ if (plot.points.length === 0) {
91
+ plot.warnings.push("No comparable data available.");
92
+ warnings.push(`${plot.title}: no comparable data available.`);
93
+ }
94
+ }
95
+ return { generatedAt: args.generatedAt, sourceExperimentDir: args.experimentDir, plots, skippedPoints, warnings };
96
+ }
@@ -1,5 +1,6 @@
1
1
  export * from "./types.js";
2
2
  export * from "./buildExperimentPlotData.js";
3
3
  export * from "./buildWarmIndexPlotData.js";
4
+ export * from "./buildContextWindowScalingPlotData.js";
4
5
  export * from "./renderSvgChart.js";
5
6
  export * from "./writePlotArtifacts.js";
@@ -1,6 +1,7 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { resolveWithinRoot } from "../core/pathSafety.js";
4
+ import { buildContextWindowScalingPlotData, readContextWindowScalingPlotSource } from "./buildContextWindowScalingPlotData.js";
4
5
  import { buildExperimentPlotData } from "./buildExperimentPlotData.js";
5
6
  import { buildWarmIndexPlotData, readWarmIndexPlotSource } from "./buildWarmIndexPlotData.js";
6
7
  import { renderSvgChart } from "./renderSvgChart.js";
@@ -16,10 +17,18 @@ export async function writePlotArtifacts(options) {
16
17
  // A warm-index-reuse plugin output directory is detected by its plugin report; every other
17
18
  // directory keeps the legacy controlled-experiment plot path unchanged.
18
19
  const experimentDir = path.resolve(options.repoRoot ?? process.cwd(), options.experimentDir);
20
+ // A context-window-scaling directory is detected by its V1 execution artifact; evidence for more
21
+ // than one plugin family in the same directory is ambiguous and fails instead of choosing one.
22
+ const scalingSource = await readContextWindowScalingPlotSource(experimentDir);
19
23
  const warmSection = await readWarmIndexPlotSource(experimentDir);
20
- const data = warmSection
21
- ? buildWarmIndexPlotData({ section: warmSection, experimentDir })
22
- : await buildExperimentPlotData({ experimentDir: options.experimentDir, repoRoot: options.repoRoot });
24
+ if (scalingSource && warmSection) {
25
+ throw new Error(`Ambiguous experiment directory for plots (${experimentDir}): it contains both context-window-scaling and warm-index-reuse evidence.`);
26
+ }
27
+ const data = scalingSource
28
+ ? buildContextWindowScalingPlotData({ aggregate: scalingSource.aggregate, experimentDir, generatedAt: scalingSource.generatedAt })
29
+ : warmSection
30
+ ? buildWarmIndexPlotData({ section: warmSection, experimentDir })
31
+ : await buildExperimentPlotData({ experimentDir: options.experimentDir, repoRoot: options.repoRoot });
23
32
  return writePlotArtifactsFromData({ data, outDir: options.outDir });
24
33
  }
25
34
  export async function writePlotArtifactsFromData(options) {