sparkforensics-server 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/package.json +1 -1
  2. package/public/assets/{AutoscalingChurn-5hDGIXi4.js → AutoscalingChurn-8t3U3isq.js} +1 -1
  3. package/public/assets/{CacheUtilization-CT2SCnJJ.js → CacheUtilization-CJs9tTum.js} +1 -1
  4. package/public/assets/CachingOpportunity-D1NcHs0V.js +1 -0
  5. package/public/assets/{ColdStart-B38OiAOp.js → ColdStart-CdRJ9gD8.js} +1 -1
  6. package/public/assets/{ConfigAudit-BoplryCH.js → ConfigAudit-B5Gbe5js.js} +1 -1
  7. package/public/assets/CoreUsageArea-a--NTRGY.js +1 -0
  8. package/public/assets/{Dashboard-BgKodJl2.js → Dashboard-C1hNeX49.js} +2 -2
  9. package/public/assets/DisclosureContext-ZpE6CkHx.js +1 -0
  10. package/public/assets/{DuplicatePlanSubtree-BqS2A2j4.js → DuplicatePlanSubtree-dobXdz7E.js} +1 -1
  11. package/public/assets/{ExecutorUtilization-Dp52QGWo.js → ExecutorUtilization-D9grwi7t.js} +1 -1
  12. package/public/assets/{GcPressure-CT1yzR_s.js → GcPressure-8-G2ri10.js} +1 -1
  13. package/public/assets/{IncompleteRun-C7Qkjtjn.js → IncompleteRun-B91lky7a.js} +1 -1
  14. package/public/assets/{JobFailures-WHl3ELfh.js → JobFailures-BsWvBMyJ.js} +1 -1
  15. package/public/assets/{MemoryUtilization-D8IUDHJI.js → MemoryUtilization-BM5schZI.js} +1 -1
  16. package/public/assets/{OverBroadcast-6oSSAQ_R.js → OverBroadcast-DdN8JIJH.js} +1 -1
  17. package/public/assets/{PartitionSizing-BwdFL-mw.js → PartitionSizing-CY5HDYtp.js} +1 -1
  18. package/public/assets/{PlanExplorer-DcxmiQWL.js → PlanExplorer-DgHCDBD_.js} +1 -1
  19. package/public/assets/PlanGraphRoute-DH3KJb8j.js +1 -0
  20. package/public/assets/{RetryWaste-BTbeHkJc.js → RetryWaste-DOnE9vGF.js} +1 -1
  21. package/public/assets/{ShuffleIO-CRhrYNE0.js → ShuffleIO-MMnDESkF.js} +1 -1
  22. package/public/assets/{Skew-DSNXRG9V.js → Skew-BmZZJ5ms.js} +1 -1
  23. package/public/assets/{SlowHost-B7Lwu2EU.js → SlowHost-CsJzfEqo.js} +1 -1
  24. package/public/assets/{SmallFiles-C082c2rd.js → SmallFiles-BoK3R1Ex.js} +1 -1
  25. package/public/assets/{SpeculationWaste-BS2vpadr.js → SpeculationWaste-CnVxB5AJ.js} +1 -1
  26. package/public/assets/Spill-BhNicyBy.js +1 -0
  27. package/public/assets/{StageFailed-D3ac9Sou.js → StageFailed-mAhHU4l_.js} +1 -1
  28. package/public/assets/StageFindingGroup-BEtPWcT0.js +1 -0
  29. package/public/assets/{StageHeader-pI1nTwVf.js → StageHeader-DJkYkrsU.js} +1 -1
  30. package/public/assets/{StageShape-eO5le8iG.js → StageShape-BDxRknLp.js} +1 -1
  31. package/public/assets/{StageSlowness-BaNyfNvL.js → StageSlowness-BPRw-otN.js} +1 -1
  32. package/public/assets/{Straggler-D5Fnok_y.js → Straggler-CZCbnOWP.js} +1 -1
  33. package/public/assets/{TaskFailures-BWnC-K_G.js → TaskFailures-C57YcLFQ.js} +1 -1
  34. package/public/assets/{TinyTask-8_5lpq_2.js → TinyTask-Bi84qU7n.js} +1 -1
  35. package/public/assets/{UnderBroadcast-DJ0XSwth.js → UnderBroadcast-DdQV7htE.js} +1 -1
  36. package/public/assets/{finding-anchor-Cys8Z6Yn.js → finding-anchor-Cl6_5hfV.js} +1 -1
  37. package/public/assets/{impact-sort-BTQndbof.js → impact-sort-CssZ6MT2.js} +1 -1
  38. package/public/assets/index-2h_TgUAb.css +2 -0
  39. package/public/assets/index-B-03mG7K.js +14 -0
  40. package/public/assets/{parser-worker-DmeCnLWU.js → parser-worker-CA5RxqKc.js} +8 -8
  41. package/public/assets/{select-DIYN6D3y.js → select-CQjNhprm.js} +1 -1
  42. package/public/assets/{usePagedRows-BxvdFnPk.js → usePagedRows-Dyl6T8Ok.js} +1 -1
  43. package/public/assets/useRecentFiles-XZEnLlal.js +11 -0
  44. package/public/assets/{useSortMode-B8SRfAoG.js → useSortMode-Dhvit8dt.js} +1 -1
  45. package/public/docs/404.html +2 -2
  46. package/public/docs/assets/{app.D2GqAkhy.js → app.eYTPqTeq.js} +1 -1
  47. package/public/docs/assets/chunks/@localSearchIndexroot.u_e5bQz4.js +1 -0
  48. package/public/docs/assets/chunks/{VPLocalSearchBox.BS4cSmtW.js → VPLocalSearchBox.DsZrQKQR.js} +1 -1
  49. package/public/docs/assets/chunks/{theme.BuxVpb0H.js → theme.DsTNE2pE.js} +2 -2
  50. package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.CKjWG56Y.js +1 -0
  51. package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.CZA0JLtV.js +1 -0
  52. package/public/docs/assets/{contributor-guide_architecture_state-and-history_run-comparison-internals.md.uuAKpY0Y.lean.js → contributor-guide_architecture_state-and-history_run-comparison-internals.md.CZA0JLtV.lean.js} +1 -1
  53. package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.DC1YqXBp.js +1 -0
  54. package/public/docs/assets/{user-guide_getting-started_ci-and-automation.md.DpnfKz3x.js → user-guide_getting-started_ci-and-automation.md.OUQqw3mI.js} +5 -4
  55. package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.OUQqw3mI.lean.js +1 -0
  56. package/public/docs/assets/{user-guide_mcp-tools_compare-runs.md.CPowlb2q.js → user-guide_mcp-tools_compare-runs.md.Cc_gLoix.js} +3 -2
  57. package/public/docs/assets/{user-guide_mcp-tools_compare-runs.md.CPowlb2q.lean.js → user-guide_mcp-tools_compare-runs.md.Cc_gLoix.lean.js} +1 -1
  58. package/public/docs/assets/{user-guide_mcp-tools_evaluate-budgets.md.Dq5Azk8G.js → user-guide_mcp-tools_evaluate-budgets.md.BB0nDY3Z.js} +2 -2
  59. package/public/docs/assets/{user-guide_mcp-tools_evaluate-budgets.md.Dq5Azk8G.lean.js → user-guide_mcp-tools_evaluate-budgets.md.BB0nDY3Z.lean.js} +1 -1
  60. package/public/docs/assets/user-guide_run-comparison.md.BUALkohi.js +1 -0
  61. package/public/docs/assets/user-guide_run-comparison.md.BUALkohi.lean.js +1 -0
  62. package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.DSXGu2tA.js +1 -0
  63. package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.DSXGu2tA.lean.js +1 -0
  64. package/public/docs/assets/user-guide_understanding-findings.md.C9YRruMT.js +1 -0
  65. package/public/docs/assets/user-guide_understanding-findings.md.C9YRruMT.lean.js +1 -0
  66. package/public/docs/contributor-guide/architecture/board-widgets/caching-and-memory.html +3 -3
  67. package/public/docs/contributor-guide/architecture/board-widgets/plan-advisor.html +3 -3
  68. package/public/docs/contributor-guide/architecture/board-widgets/visual-system-and-templating.html +3 -3
  69. package/public/docs/contributor-guide/architecture/board-widgets.html +3 -3
  70. package/public/docs/contributor-guide/architecture/detector-contract/plan-attribution.html +3 -3
  71. package/public/docs/contributor-guide/architecture/detector-contract/threshold-tables.html +5 -5
  72. package/public/docs/contributor-guide/architecture/detector-contract.html +3 -3
  73. package/public/docs/contributor-guide/architecture/drill-down/plan-graph.html +3 -3
  74. package/public/docs/contributor-guide/architecture/drill-down.html +3 -3
  75. package/public/docs/contributor-guide/architecture/impact-estimation/caveats-tuning-and-coverage.html +3 -3
  76. package/public/docs/contributor-guide/architecture/impact-estimation/stage-union-rollup.html +3 -3
  77. package/public/docs/contributor-guide/architecture/impact-estimation.html +3 -3
  78. package/public/docs/contributor-guide/architecture/index.html +3 -3
  79. package/public/docs/contributor-guide/architecture/overview.html +3 -3
  80. package/public/docs/contributor-guide/architecture/state-and-history/history-server-intake.html +3 -3
  81. package/public/docs/contributor-guide/architecture/state-and-history/run-comparison-internals.html +5 -5
  82. package/public/docs/contributor-guide/architecture/state-and-history.html +3 -3
  83. package/public/docs/contributor-guide/architecture/widget-rendering/findings-tab.html +3 -3
  84. package/public/docs/contributor-guide/architecture/widget-rendering/investigation-routing.html +3 -3
  85. package/public/docs/contributor-guide/architecture/widget-rendering/list-sort-mode.html +3 -3
  86. package/public/docs/contributor-guide/architecture/widget-rendering/reference-section.html +3 -3
  87. package/public/docs/contributor-guide/architecture/widget-rendering/row-expand-contract.html +3 -3
  88. package/public/docs/contributor-guide/architecture/widget-rendering.html +3 -3
  89. package/public/docs/contributor-guide/architecture/worker-protocol/cli-and-mcp.html +5 -5
  90. package/public/docs/contributor-guide/architecture/worker-protocol/core-and-event-validation.html +3 -3
  91. package/public/docs/contributor-guide/architecture/worker-protocol/evidence-availability.html +3 -3
  92. package/public/docs/contributor-guide/architecture/worker-protocol/evidence-report.html +3 -3
  93. package/public/docs/contributor-guide/architecture/worker-protocol.html +3 -3
  94. package/public/docs/contributor-guide/contributing.html +3 -3
  95. package/public/docs/contributor-guide/development-setup.html +3 -3
  96. package/public/docs/contributor-guide/testing.html +3 -3
  97. package/public/docs/hashmap.json +1 -1
  98. package/public/docs/index.html +3 -3
  99. package/public/docs/tuning-reference/anti-patterns.html +3 -3
  100. package/public/docs/tuning-reference/aqe.html +3 -3
  101. package/public/docs/tuning-reference/bottleneck-broadcast-sizing.html +3 -3
  102. package/public/docs/tuning-reference/bottleneck-cold-start.html +3 -3
  103. package/public/docs/tuning-reference/bottleneck-duplicate-plan-subtree.html +3 -3
  104. package/public/docs/tuning-reference/bottleneck-failures.html +3 -3
  105. package/public/docs/tuning-reference/bottleneck-gc.html +3 -3
  106. package/public/docs/tuning-reference/bottleneck-job-failure-rate.html +3 -3
  107. package/public/docs/tuning-reference/bottleneck-memory-utilization.html +3 -3
  108. package/public/docs/tuning-reference/bottleneck-retry-waste.html +3 -3
  109. package/public/docs/tuning-reference/bottleneck-shuffle.html +3 -3
  110. package/public/docs/tuning-reference/bottleneck-skew.html +3 -3
  111. package/public/docs/tuning-reference/bottleneck-slow-host.html +3 -3
  112. package/public/docs/tuning-reference/bottleneck-small-files.html +3 -3
  113. package/public/docs/tuning-reference/bottleneck-spill.html +3 -3
  114. package/public/docs/tuning-reference/bottleneck-straggler.html +3 -3
  115. package/public/docs/tuning-reference/bottleneck-tiny-tasks.html +3 -3
  116. package/public/docs/tuning-reference/bottleneck-utilization.html +3 -3
  117. package/public/docs/tuning-reference/caching.html +3 -3
  118. package/public/docs/tuning-reference/cluster-config.html +3 -3
  119. package/public/docs/tuning-reference/config.html +3 -3
  120. package/public/docs/tuning-reference/data-formats.html +3 -3
  121. package/public/docs/tuning-reference/index.html +3 -3
  122. package/public/docs/tuning-reference/intro.html +3 -3
  123. package/public/docs/tuning-reference/joins.html +3 -3
  124. package/public/docs/tuning-reference/memory-model.html +3 -3
  125. package/public/docs/tuning-reference/metrics.html +3 -3
  126. package/public/docs/tuning-reference/partitioning.html +3 -3
  127. package/public/docs/tuning-reference/pyspark.html +3 -3
  128. package/public/docs/tuning-reference/shuffle.html +3 -3
  129. package/public/docs/tuning-reference/spark-architecture.html +3 -3
  130. package/public/docs/tuning-reference/table-formats.html +3 -3
  131. package/public/docs/user-guide/alternative-log-retrieval/managed-platforms.html +3 -3
  132. package/public/docs/user-guide/alternative-log-retrieval/ssh-bastion.html +3 -3
  133. package/public/docs/user-guide/alternative-log-retrieval.html +3 -3
  134. package/public/docs/user-guide/getting-started/ci-and-automation.html +8 -7
  135. package/public/docs/user-guide/getting-started/reading-the-dashboard.html +3 -3
  136. package/public/docs/user-guide/getting-started/regression-metric-keys.html +3 -3
  137. package/public/docs/user-guide/getting-started/tuning-thresholds.html +3 -3
  138. package/public/docs/user-guide/getting-started.html +3 -3
  139. package/public/docs/user-guide/mcp-tools/compare-runs.html +7 -6
  140. package/public/docs/user-guide/mcp-tools/diagnose-run.html +3 -3
  141. package/public/docs/user-guide/mcp-tools/evaluate-budgets.html +5 -5
  142. package/public/docs/user-guide/mcp-tools/get-finding-documentation.html +3 -3
  143. package/public/docs/user-guide/mcp-tools/get-finding-evidence.html +3 -3
  144. package/public/docs/user-guide/mcp-tools/get-reference-doc.html +3 -3
  145. package/public/docs/user-guide/mcp-tools/get-run-summary.html +3 -3
  146. package/public/docs/user-guide/mcp-tools/list-runs.html +3 -3
  147. package/public/docs/user-guide/mcp-tools.html +3 -3
  148. package/public/docs/user-guide/run-comparison/how-stages-are-matched.html +5 -5
  149. package/public/docs/user-guide/run-comparison.html +5 -5
  150. package/public/docs/user-guide/understanding-findings.html +5 -5
  151. package/public/export-template.html +3 -3
  152. package/public/index.html +3 -3
  153. package/vendor-core/cli/budgets.js +62 -2
  154. package/vendor-core/cli/regression-budgets.js +35 -0
  155. package/vendor-core/comparison-output.js +2 -1
  156. package/vendor-core/core-source-hash.txt +1 -1
  157. package/vendor-core/detectors.js +219 -56
  158. package/vendor-core/docs-content/detection/chrn.md +3 -1
  159. package/vendor-core/docs-content/detection/cold.md +2 -1
  160. package/vendor-core/docs-content/detection/plan.md +9 -2
  161. package/vendor-core/docs-content/detection/shfl.md +8 -2
  162. package/vendor-core/docs-content/detection/skew.md +11 -5
  163. package/vendor-core/docs-content/detection/slow.md +6 -2
  164. package/vendor-core/docs-content/detection/spill.md +4 -1
  165. package/vendor-core/docs-content/detection/strag.md +5 -3
  166. package/vendor-core/docs-content/detection/tiny.md +3 -2
  167. package/vendor-core/evidence-report.js +11 -11
  168. package/vendor-core/finding-presentation.js +39 -13
  169. package/vendor-core/finding-types.js +50 -6
  170. package/vendor-core/mcp-server-factory.js +6 -2
  171. package/vendor-core/mcp-tools.js +10 -1
  172. package/vendor-core/median.js +8 -0
  173. package/vendor-core/run-comparison.js +2 -0
  174. package/vendor-core/sequence-alignment.js +83 -0
  175. package/vendor-core/stage-alignment.js +387 -63
  176. package/vendor-core/stage-quantiles.js +3 -1
  177. package/vendor-core/stage-structure.js +83 -0
  178. package/public/assets/CachingOpportunity-D_O_WkbH.js +0 -1
  179. package/public/assets/CoreUsageArea-csgwNxkl.js +0 -1
  180. package/public/assets/DisclosureContext-B0siMVdg.js +0 -1
  181. package/public/assets/PlanGraphRoute-pqnHTt4i.js +0 -1
  182. package/public/assets/Spill-B4DVKHXx.js +0 -1
  183. package/public/assets/StageFindingGroup-D9naK109.js +0 -1
  184. package/public/assets/index-BcqTLKFp.css +0 -2
  185. package/public/assets/index-DCeWge12.js +0 -13
  186. package/public/assets/useRecentFiles-C3Ov47U7.js +0 -11
  187. package/public/docs/assets/chunks/@localSearchIndexroot.D4OdkQjy.js +0 -1
  188. package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.fkPBXsU2.js +0 -1
  189. package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.uuAKpY0Y.js +0 -1
  190. package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.Dk08VS7n.js +0 -1
  191. package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.DpnfKz3x.lean.js +0 -1
  192. package/public/docs/assets/user-guide_run-comparison.md.B6aNtDYF.js +0 -1
  193. package/public/docs/assets/user-guide_run-comparison.md.B6aNtDYF.lean.js +0 -1
  194. package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.XBzQ1oEU.js +0 -1
  195. package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.XBzQ1oEU.lean.js +0 -1
  196. package/public/docs/assets/user-guide_understanding-findings.md.Dqt2H_C1.js +0 -1
  197. package/public/docs/assets/user-guide_understanding-findings.md.Dqt2H_C1.lean.js +0 -1
  198. /package/public/docs/assets/{contributor-guide_architecture_detector-contract_threshold-tables.md.fkPBXsU2.lean.js → contributor-guide_architecture_detector-contract_threshold-tables.md.CKjWG56Y.lean.js} +0 -0
  199. /package/public/docs/assets/{contributor-guide_architecture_worker-protocol_cli-and-mcp.md.Dk08VS7n.lean.js → contributor-guide_architecture_worker-protocol_cli-and-mcp.md.DC1YqXBp.lean.js} +0 -0
package/public/index.html CHANGED
@@ -23,14 +23,14 @@
23
23
  document.documentElement.setAttribute('data-theme', 'light');
24
24
  } catch (_) {}
25
25
  </script>
26
- <script type="module" crossorigin src="./assets/index-DCeWge12.js"></script>
26
+ <script type="module" crossorigin src="./assets/index-B-03mG7K.js"></script>
27
27
  <link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-hePW80VL.js">
28
28
  <link rel="modulepreload" crossorigin href="./assets/charts-vendor-0VMaLVLN.js">
29
29
  <link rel="modulepreload" crossorigin href="./assets/plan-graph-vendor-BYl-AzN5.js">
30
30
  <link rel="modulepreload" crossorigin href="./assets/react-vendor-DEdfrRTJ.js">
31
- <link rel="modulepreload" crossorigin href="./assets/DisclosureContext-B0siMVdg.js">
31
+ <link rel="modulepreload" crossorigin href="./assets/DisclosureContext-ZpE6CkHx.js">
32
32
  <link rel="stylesheet" crossorigin href="./assets/plan-graph-vendor-DLioOiRN.css">
33
- <link rel="stylesheet" crossorigin href="./assets/index-BcqTLKFp.css">
33
+ <link rel="stylesheet" crossorigin href="./assets/index-2h_TgUAb.css">
34
34
  </head>
35
35
  <body>
36
36
  <div id="root"></div>
@@ -3,6 +3,7 @@ import { computeSkewRatio, ENTRY_BY_TYPE, } from '../det
3
3
  import { effectiveThresholds } from '../threshold-overrides.js';
4
4
  import { IMPACT_BAND_ORDER } from '../format-utils.js';
5
5
 
6
+ import { PAIR_DELTA_METRICS, } from '../stage-alignment.js';
6
7
 
7
8
 
8
9
 
@@ -14,13 +15,23 @@ const IMPACT_BANDS = Object.keys(IMPACT_BAND_ORDER) ;
14
15
 
15
16
 
16
17
 
18
+
19
+
20
+
21
+
22
+
17
23
 
24
+
25
+ export const STAGE_QUALITIES = ['exact', 'structural', 'aligned'];
26
+ export const DEFAULT_STAGE_BUDGET_QUALITIES = ['exact', 'structural'];
27
+ /** Metrics with a regression direction (higher is worse). Input and output volume are workload, not performance. */
28
+ export const NEUTRAL_PAIR_METRICS = new Set(['inputBytes', 'outputBytes']);
18
29
 
19
30
 
20
-
31
+
21
32
 
22
33
 
23
-
34
+
24
35
 
25
36
 
26
37
 
@@ -146,6 +157,50 @@ function checkRegression(comparison , maxRegressionPct
146
157
  : { name: 'max-regression', metric: regressionMetric, status: 'pass', detail: `Metric "${regressionMetric}" ${pctLabel}, within budget ${maxRegressionPct}%.` };
147
158
  }
148
159
 
160
+ function checkStageRegression(
161
+ comparison , { metric, maxPct } , qualities ,
162
+ ) {
163
+ const name = 'max-stage-regression' ;
164
+ if (!(PAIR_DELTA_METRICS ).includes(metric)) {
165
+ return { name, metric, status: 'inconclusive', detail: `Metric "${metric}" is not a paired-stage metric (expected one of: ${PAIR_DELTA_METRICS.join(', ')}).` };
166
+ }
167
+ if (NEUTRAL_PAIR_METRICS.has(metric)) {
168
+ return { name, metric, status: 'inconclusive', detail: `Metric "${metric}" measures workload volume, not performance: it has no regression direction to check.` };
169
+ }
170
+ const eligible = comparison.stagePairs.filter((p) => qualities.includes(p.quality));
171
+ if (eligible.length === 0) {
172
+ return { name, metric, status: 'inconclusive', detail: comparison.stagePairs.length === 0
173
+ ? 'No stages were paired between the two runs.'
174
+ : `None of the ${comparison.stagePairs.length} paired stage(s) has quality ${qualities.join(' or ')}.` };
175
+ }
176
+ const measured = eligible
177
+ .map((pair) => ({ pair, delta: pair.deltas[metric ] }))
178
+ .filter(({ delta }) => delta.baseline != null && delta.delta != null);
179
+ if (measured.length === 0) {
180
+ return { name, metric, status: 'inconclusive', detail: `Metric "${metric}" is unavailable for every eligible paired stage.` };
181
+ }
182
+ const regressions = measured
183
+ .filter(({ delta }) => delta.delta > 0)
184
+ .map(({ pair, delta }) => ({ pair, delta, pct: delta.baseline === 0 ? Infinity : (delta.delta / delta.baseline ) * 100 }))
185
+ .sort((a, b) => b.pct - a.pct || a.pair.pairId.localeCompare(b.pair.pairId));
186
+ const over = regressions.filter((r) => r.pct > maxPct);
187
+ const scope = `${measured.length} paired stage(s)`;
188
+ if (over.length === 0) {
189
+ const worst = regressions[0];
190
+ return { name, metric, status: 'pass', detail: worst
191
+ ? `No paired stage regressed "${metric}" beyond ${maxPct}% across ${scope}; the largest was ${formatPct(worst.pct)} (pair ${worst.pair.pairId}).`
192
+ : `No paired stage regressed "${metric}" across ${scope}.` };
193
+ }
194
+ const listed = over.slice(0, 3).map((r) => `pair ${r.pair.pairId} (${r.pair.quality}) ${formatPct(r.pct)}`).join(', ');
195
+ const more = over.length > 3 ? `, and ${over.length - 3} more` : '';
196
+ return { name, metric, status: 'violation',
197
+ detail: `${over.length} of ${scope} regressed "${metric}" beyond ${maxPct}%: ${listed}${more}.` };
198
+ }
199
+
200
+ function formatPct(pct ) {
201
+ return pct === Infinity ? 'from 0' : `+${pct.toFixed(1)}%`;
202
+ }
203
+
149
204
  function checkFailOnIntroduced(comparison , band ) {
150
205
  if (band !== 'all' && !IMPACT_BANDS.includes(band )) {
151
206
  return { name: 'fail-on-introduced', status: 'inconclusive', detail: `Impact band "${band}" is not recognized (expected "all" or one of ${IMPACT_BANDS.join(', ')}).` };
@@ -198,6 +253,11 @@ export function evaluateBudgets({ appModel, catalog, budgets, comparison, thresh
198
253
  for (const { metric, maxPct } of budgets.regressionBudgets ?? []) {
199
254
  pushComparisonBudget(results, comparison, 'max-regression', (c) => checkRegression(c, maxPct, metric), metric);
200
255
  }
256
+ const stageQualities = budgets.stageQualities ?? DEFAULT_STAGE_BUDGET_QUALITIES;
257
+ for (const budget of budgets.stageRegressionBudgets ?? []) {
258
+ pushComparisonBudget(results, comparison, 'max-stage-regression',
259
+ (c) => checkStageRegression(c, budget, stageQualities), budget.metric);
260
+ }
201
261
  if (budgets.failOnIntroduced !== undefined) {
202
262
  pushComparisonBudget(results, comparison, 'fail-on-introduced',
203
263
  (c) => checkFailOnIntroduced(c, budgets.failOnIntroduced ));
@@ -4,6 +4,9 @@
4
4
  import { readFileSync } from 'node:fs';
5
5
  import { resolve } from 'node:path';
6
6
  import { COMPARISON_METRIC_KEYS } from '../run-comparison.js';
7
+ import { PAIR_DELTA_METRICS } from '../stage-alignment.js';
8
+ import { NEUTRAL_PAIR_METRICS, STAGE_QUALITIES } from './budgets.js';
9
+
7
10
 
8
11
 
9
12
 
@@ -81,3 +84,35 @@ export function combineRegressionBudgets(sources )
81
84
  }
82
85
  return sources.map((s) => s.budget);
83
86
  }
87
+
88
+ /** Refuses a stage budget metric that is not a paired-stage metric or has no regression direction. */
89
+ export function assertStageBudgetMetric(metric , origin ) {
90
+ if (!(PAIR_DELTA_METRICS ).includes(metric)) {
91
+ throw new Error(`${origin}: unknown paired-stage metric "${metric}" (expected one of: ${PAIR_DELTA_METRICS.filter((m) => !NEUTRAL_PAIR_METRICS.has(m)).join(', ')}).`);
92
+ }
93
+ if (NEUTRAL_PAIR_METRICS.has(metric)) {
94
+ throw new Error(`${origin}: "${metric}" measures workload volume, not performance, so it has no regression direction to budget.`);
95
+ }
96
+ }
97
+
98
+ /** One `--stage-regression-budget` value, `<metric>:<pct>`, over the paired-stage metrics. */
99
+ export function parseStageRegressionBudgetFlag(spec ) {
100
+ const origin = `--stage-regression-budget "${spec}"`;
101
+ const colon = spec.indexOf(':');
102
+ if (colon === -1) throw new Error(`${origin}: expected <metric>:<pct>.`);
103
+ const metric = spec.slice(0, colon);
104
+ const pct = spec.slice(colon + 1);
105
+ assertStageBudgetMetric(metric, origin);
106
+ if (!PCT_PATTERN.test(pct)) throw new Error(`${origin}: "${pct}" is not a non-negative percentage.`);
107
+ return { metric, maxPct: Number(pct) };
108
+ }
109
+
110
+ /** The pair qualities a stage budget reads, from a comma list such as `exact,structural`. */
111
+ export function parseStageQualities(list , origin ) {
112
+ const qualities = list.split(',').map((q) => q.trim()).filter(Boolean);
113
+ const bad = qualities.find((q) => !(STAGE_QUALITIES ).includes(q));
114
+ if (qualities.length === 0 || bad !== undefined) {
115
+ throw new Error(`${origin}: ${bad === undefined ? 'no quality given' : `unknown quality "${bad}"`} (expected a list from: ${STAGE_QUALITIES.join(', ')}).`);
116
+ }
117
+ return qualities ;
118
+ }
@@ -11,7 +11,7 @@ import { redactComparison } from './redact.js';
11
11
 
12
12
 
13
13
 
14
-
14
+
15
15
 
16
16
 
17
17
  /** `full` carries every field (the CLI); `summary` leaves out `stagePairs` (MCP, which returns them
@@ -33,6 +33,7 @@ export function comparisonOutput(comparison , view
33
33
  unmatched: comparison.unmatched,
34
34
  replanned: comparison.replanned,
35
35
  bookkeepingStageIds: comparison.bookkeepingStageIds,
36
+ executionAlignment: comparison.executionAlignment,
36
37
  };
37
38
  }
38
39
 
@@ -1 +1 @@
1
- 6e0ac29f74ae092b14c5700dfa86d3273fce7d1af59005c6e2e450a91da92d5e
1
+ 7aaca3af812e0e5a4f4632ad7577fc438b589520a27422ce8c986b156de82260