sparkforensics-server 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (382) hide show
  1. package/index.js +3 -0
  2. package/package.json +3 -3
  3. package/public/assets/AutoscalingChurn-5hDGIXi4.js +1 -0
  4. package/public/assets/CacheUtilization-CT2SCnJJ.js +1 -0
  5. package/public/assets/CachingOpportunity-D_O_WkbH.js +1 -0
  6. package/public/assets/ColdStart-B38OiAOp.js +1 -0
  7. package/public/assets/ConfigAudit-BoplryCH.js +1 -0
  8. package/public/assets/CoreUsageArea-csgwNxkl.js +1 -0
  9. package/public/assets/Dashboard-BgKodJl2.js +2 -0
  10. package/public/assets/DisclosureContext-B0siMVdg.js +1 -0
  11. package/public/assets/DuplicatePlanSubtree-BqS2A2j4.js +1 -0
  12. package/public/assets/ExecutorUtilization-Dp52QGWo.js +1 -0
  13. package/public/assets/GcPressure-CT1yzR_s.js +1 -0
  14. package/public/assets/IncompleteRun-C7Qkjtjn.js +1 -0
  15. package/public/assets/JobFailures-WHl3ELfh.js +1 -0
  16. package/public/assets/MemoryUtilization-D8IUDHJI.js +1 -0
  17. package/public/assets/OverBroadcast-6oSSAQ_R.js +1 -0
  18. package/public/assets/PartitionHint-CUvbnnxy.js +1 -0
  19. package/public/assets/PartitionSizing-BwdFL-mw.js +1 -0
  20. package/public/assets/{PlanExplorer-DdmPkKO1.js → PlanExplorer-DcxmiQWL.js} +1 -1
  21. package/public/assets/PlanGraphRoute-pqnHTt4i.js +1 -0
  22. package/public/assets/RetryWaste-BTbeHkJc.js +1 -0
  23. package/public/assets/ShuffleIO-CRhrYNE0.js +1 -0
  24. package/public/assets/Skew-DSNXRG9V.js +1 -0
  25. package/public/assets/SlowHost-B7Lwu2EU.js +1 -0
  26. package/public/assets/SmallFiles-C082c2rd.js +1 -0
  27. package/public/assets/SpeculationWaste-BS2vpadr.js +1 -0
  28. package/public/assets/Spill-B4DVKHXx.js +1 -0
  29. package/public/assets/StageFailed-D3ac9Sou.js +1 -0
  30. package/public/assets/StageFindingGroup-D9naK109.js +1 -0
  31. package/public/assets/{StageHeader-BSV8ALhT.js → StageHeader-pI1nTwVf.js} +1 -1
  32. package/public/assets/StageShape-eO5le8iG.js +1 -0
  33. package/public/assets/StageSlowness-BaNyfNvL.js +1 -0
  34. package/public/assets/Straggler-D5Fnok_y.js +1 -0
  35. package/public/assets/TaskFailures-BWnC-K_G.js +1 -0
  36. package/public/assets/{TinyTask-CIPRxWfT.js → TinyTask-8_5lpq_2.js} +1 -1
  37. package/public/assets/UnderBroadcast-DJ0XSwth.js +1 -0
  38. package/public/assets/{finding-anchor-DF1cSHO1.js → finding-anchor-Cys8Z6Yn.js} +1 -1
  39. package/public/assets/{impact-sort-7z0ZcBZf.js → impact-sort-BTQndbof.js} +1 -1
  40. package/public/assets/index-DCeWge12.js +13 -0
  41. package/public/assets/parser-worker-DmeCnLWU.js +122 -0
  42. package/public/assets/{plan-graph-vendor-B0sEms9Z.js → plan-graph-vendor-BYl-AzN5.js} +1 -1
  43. package/public/assets/{react-vendor-BAKt3vny.js → react-vendor-DEdfrRTJ.js} +1 -1
  44. package/public/assets/{select-Cy5pmbrh.js → select-DIYN6D3y.js} +1 -1
  45. package/public/assets/{usePagedRows-BjPOpUU8.js → usePagedRows-BxvdFnPk.js} +1 -1
  46. package/public/assets/useRecentFiles-C3Ov47U7.js +11 -0
  47. package/public/assets/{useSortMode-CU0vzThu.js → useSortMode-B8SRfAoG.js} +1 -1
  48. package/public/docs/404.html +4 -3
  49. package/public/docs/assets/{app.C6w47a9W.js → app.D2GqAkhy.js} +1 -1
  50. package/public/docs/assets/chunks/@localSearchIndexroot.D4OdkQjy.js +1 -0
  51. package/public/docs/assets/chunks/{VPLocalSearchBox.BcTNvT55.js → VPLocalSearchBox.BS4cSmtW.js} +1 -1
  52. package/public/docs/assets/chunks/{theme.D-mE6j02.js → theme.BuxVpb0H.js} +2 -2
  53. package/public/docs/assets/contributor-guide_architecture_board-widgets.md.YQcj5EI4.js +1 -0
  54. package/public/docs/assets/contributor-guide_architecture_board-widgets.md.YQcj5EI4.lean.js +1 -0
  55. package/public/docs/assets/contributor-guide_architecture_board-widgets_caching-and-memory.md.8bH1HnZp.js +1 -0
  56. package/public/docs/assets/contributor-guide_architecture_board-widgets_caching-and-memory.md.8bH1HnZp.lean.js +1 -0
  57. package/public/docs/assets/contributor-guide_architecture_board-widgets_plan-advisor.md.RlzvUmNm.js +1 -0
  58. package/public/docs/assets/contributor-guide_architecture_board-widgets_plan-advisor.md.RlzvUmNm.lean.js +1 -0
  59. package/public/docs/assets/contributor-guide_architecture_board-widgets_visual-system-and-templating.md.DYgUpIJB.js +1 -0
  60. package/public/docs/assets/contributor-guide_architecture_board-widgets_visual-system-and-templating.md.DYgUpIJB.lean.js +1 -0
  61. package/public/docs/assets/contributor-guide_architecture_detector-contract.md.wj6Et6UD.js +1 -0
  62. package/public/docs/assets/contributor-guide_architecture_detector-contract.md.wj6Et6UD.lean.js +1 -0
  63. package/public/docs/assets/contributor-guide_architecture_detector-contract_plan-attribution.md.yNex4bys.js +1 -0
  64. package/public/docs/assets/contributor-guide_architecture_detector-contract_plan-attribution.md.yNex4bys.lean.js +1 -0
  65. package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.fkPBXsU2.js +1 -0
  66. package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.fkPBXsU2.lean.js +1 -0
  67. package/public/docs/assets/contributor-guide_architecture_drill-down.md.CqZPrdo9.js +1 -0
  68. package/public/docs/assets/contributor-guide_architecture_drill-down.md.CqZPrdo9.lean.js +1 -0
  69. package/public/docs/assets/contributor-guide_architecture_drill-down_plan-graph.md.DwOozIEs.js +1 -0
  70. package/public/docs/assets/contributor-guide_architecture_drill-down_plan-graph.md.DwOozIEs.lean.js +1 -0
  71. package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.CZv_ZGOp.js +1 -0
  72. package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.CZv_ZGOp.lean.js +1 -0
  73. package/public/docs/assets/contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.BUucRc0A.js +1 -0
  74. package/public/docs/assets/contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.BUucRc0A.lean.js +1 -0
  75. package/public/docs/assets/contributor-guide_architecture_impact-estimation_stage-union-rollup.md.DdCGtAGu.js +1 -0
  76. package/public/docs/assets/contributor-guide_architecture_impact-estimation_stage-union-rollup.md.DdCGtAGu.lean.js +1 -0
  77. package/public/docs/assets/contributor-guide_architecture_index.md.tymxf1Fh.js +1 -0
  78. package/public/docs/assets/contributor-guide_architecture_index.md.tymxf1Fh.lean.js +1 -0
  79. package/public/docs/assets/contributor-guide_architecture_overview.md.C1sSvm9i.js +1 -0
  80. package/public/docs/assets/contributor-guide_architecture_overview.md.C1sSvm9i.lean.js +1 -0
  81. package/public/docs/assets/contributor-guide_architecture_state-and-history.md.DyCBdjuM.js +1 -0
  82. package/public/docs/assets/{contributor-guide_architecture_state-and-history.md.DOTVHjTV.lean.js → contributor-guide_architecture_state-and-history.md.DyCBdjuM.lean.js} +1 -1
  83. package/public/docs/assets/contributor-guide_architecture_state-and-history_history-server-intake.md.C298hrHZ.js +1 -0
  84. package/public/docs/assets/contributor-guide_architecture_state-and-history_history-server-intake.md.C298hrHZ.lean.js +1 -0
  85. package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.uuAKpY0Y.js +1 -0
  86. package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.uuAKpY0Y.lean.js +1 -0
  87. package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.BOZ1azXG.js +1 -0
  88. package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.BOZ1azXG.lean.js +1 -0
  89. package/public/docs/assets/contributor-guide_architecture_widget-rendering_findings-tab.md.BQXycK6G.js +1 -0
  90. package/public/docs/assets/contributor-guide_architecture_widget-rendering_findings-tab.md.BQXycK6G.lean.js +1 -0
  91. package/public/docs/assets/contributor-guide_architecture_widget-rendering_investigation-routing.md.BGdiyElu.js +1 -0
  92. package/public/docs/assets/contributor-guide_architecture_widget-rendering_investigation-routing.md.BGdiyElu.lean.js +1 -0
  93. package/public/docs/assets/contributor-guide_architecture_widget-rendering_list-sort-mode.md.BuHKgH0C.js +1 -0
  94. package/public/docs/assets/contributor-guide_architecture_widget-rendering_list-sort-mode.md.BuHKgH0C.lean.js +1 -0
  95. package/public/docs/assets/contributor-guide_architecture_widget-rendering_reference-section.md.DWqC80bN.js +1 -0
  96. package/public/docs/assets/contributor-guide_architecture_widget-rendering_reference-section.md.DWqC80bN.lean.js +1 -0
  97. package/public/docs/assets/contributor-guide_architecture_widget-rendering_row-expand-contract.md.ZT89u9f1.js +1 -0
  98. package/public/docs/assets/contributor-guide_architecture_widget-rendering_row-expand-contract.md.ZT89u9f1.lean.js +1 -0
  99. package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.BAnhi2-d.js +1 -0
  100. package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.BAnhi2-d.lean.js +1 -0
  101. package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.Dk08VS7n.js +1 -0
  102. package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.Dk08VS7n.lean.js +1 -0
  103. package/public/docs/assets/contributor-guide_architecture_worker-protocol_core-and-event-validation.md.DSVFPpWb.js +1 -0
  104. package/public/docs/assets/contributor-guide_architecture_worker-protocol_core-and-event-validation.md.DSVFPpWb.lean.js +1 -0
  105. package/public/docs/assets/contributor-guide_architecture_worker-protocol_evidence-availability.md.CjimIDBY.js +6 -0
  106. package/public/docs/assets/contributor-guide_architecture_worker-protocol_evidence-availability.md.CjimIDBY.lean.js +1 -0
  107. package/public/docs/assets/contributor-guide_architecture_worker-protocol_evidence-report.md.DVGZOWvW.js +1 -0
  108. package/public/docs/assets/contributor-guide_architecture_worker-protocol_evidence-report.md.DVGZOWvW.lean.js +1 -0
  109. package/public/docs/assets/contributor-guide_contributing.md.CQMvGwFt.js +1 -0
  110. package/public/docs/assets/{contributor-guide_contributing.md.B2E9ni79.lean.js → contributor-guide_contributing.md.CQMvGwFt.lean.js} +1 -1
  111. package/public/docs/assets/contributor-guide_development-setup.md.D9bu4j_c.js +15 -0
  112. package/public/docs/assets/contributor-guide_development-setup.md.D9bu4j_c.lean.js +1 -0
  113. package/public/docs/assets/contributor-guide_testing.md.By6XGR9a.js +1 -0
  114. package/public/docs/assets/{contributor-guide_testing.md.DTwhpoN2.lean.js → contributor-guide_testing.md.By6XGR9a.lean.js} +1 -1
  115. package/public/docs/assets/{style.DSixAiZE.css → style.tmDhD3c8.css} +1 -1
  116. package/public/docs/assets/user-guide_alternative-log-retrieval.md.CJpYvHGo.js +4 -0
  117. package/public/docs/assets/user-guide_alternative-log-retrieval.md.CJpYvHGo.lean.js +1 -0
  118. package/public/docs/assets/user-guide_alternative-log-retrieval_managed-platforms.md.DvDwHNtC.js +4 -0
  119. package/public/docs/assets/user-guide_alternative-log-retrieval_managed-platforms.md.DvDwHNtC.lean.js +1 -0
  120. package/public/docs/assets/user-guide_alternative-log-retrieval_ssh-bastion.md.Gszzlplv.js +1 -0
  121. package/public/docs/assets/user-guide_alternative-log-retrieval_ssh-bastion.md.Gszzlplv.lean.js +1 -0
  122. package/public/docs/assets/user-guide_getting-started.md.DYHHHkQR.js +1 -0
  123. package/public/docs/assets/user-guide_getting-started.md.DYHHHkQR.lean.js +1 -0
  124. package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.DpnfKz3x.js +22 -0
  125. package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.DpnfKz3x.lean.js +1 -0
  126. package/public/docs/assets/user-guide_getting-started_reading-the-dashboard.md.Obf3W3__.js +1 -0
  127. package/public/docs/assets/user-guide_getting-started_reading-the-dashboard.md.Obf3W3__.lean.js +1 -0
  128. package/public/docs/assets/user-guide_getting-started_regression-metric-keys.md.B_gzr2Mw.js +1 -0
  129. package/public/docs/assets/user-guide_getting-started_regression-metric-keys.md.B_gzr2Mw.lean.js +1 -0
  130. package/public/docs/assets/user-guide_getting-started_tuning-thresholds.md.B_MyjyZm.js +4 -0
  131. package/public/docs/assets/user-guide_getting-started_tuning-thresholds.md.B_MyjyZm.lean.js +1 -0
  132. package/public/docs/assets/user-guide_mcp-tools.md.C65SjWUL.js +16 -0
  133. package/public/docs/assets/user-guide_mcp-tools.md.C65SjWUL.lean.js +1 -0
  134. package/public/docs/assets/user-guide_mcp-tools_compare-runs.md.CPowlb2q.js +55 -0
  135. package/public/docs/assets/user-guide_mcp-tools_compare-runs.md.CPowlb2q.lean.js +1 -0
  136. package/public/docs/assets/user-guide_mcp-tools_diagnose-run.md.BP1vnHA5.js +23 -0
  137. package/public/docs/assets/user-guide_mcp-tools_diagnose-run.md.BP1vnHA5.lean.js +1 -0
  138. package/public/docs/assets/user-guide_mcp-tools_evaluate-budgets.md.Dq5Azk8G.js +26 -0
  139. package/public/docs/assets/user-guide_mcp-tools_evaluate-budgets.md.Dq5Azk8G.lean.js +1 -0
  140. package/public/docs/assets/user-guide_mcp-tools_get-finding-documentation.md.BtTTHNV6.js +14 -0
  141. package/public/docs/assets/user-guide_mcp-tools_get-finding-documentation.md.BtTTHNV6.lean.js +1 -0
  142. package/public/docs/assets/user-guide_mcp-tools_get-finding-evidence.md.rKyu4wts.js +20 -0
  143. package/public/docs/assets/user-guide_mcp-tools_get-finding-evidence.md.rKyu4wts.lean.js +1 -0
  144. package/public/docs/assets/user-guide_mcp-tools_get-reference-doc.md.Yac4tH3u.js +5 -0
  145. package/public/docs/assets/user-guide_mcp-tools_get-reference-doc.md.Yac4tH3u.lean.js +1 -0
  146. package/public/docs/assets/user-guide_mcp-tools_get-run-summary.md.DpxZ4ZNp.js +15 -0
  147. package/public/docs/assets/user-guide_mcp-tools_get-run-summary.md.DpxZ4ZNp.lean.js +1 -0
  148. package/public/docs/assets/user-guide_mcp-tools_list-runs.md.DpQPeCM3.js +12 -0
  149. package/public/docs/assets/user-guide_mcp-tools_list-runs.md.DpQPeCM3.lean.js +1 -0
  150. package/public/docs/assets/user-guide_run-comparison.md.B6aNtDYF.js +1 -0
  151. package/public/docs/assets/user-guide_run-comparison.md.B6aNtDYF.lean.js +1 -0
  152. package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.XBzQ1oEU.js +1 -0
  153. package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.XBzQ1oEU.lean.js +1 -0
  154. package/public/docs/assets/user-guide_understanding-findings.md.Dqt2H_C1.js +1 -0
  155. package/public/docs/contributor-guide/architecture/board-widgets/caching-and-memory.html +30 -0
  156. package/public/docs/contributor-guide/architecture/board-widgets/plan-advisor.html +30 -0
  157. package/public/docs/contributor-guide/architecture/board-widgets/visual-system-and-templating.html +30 -0
  158. package/public/docs/contributor-guide/architecture/board-widgets.html +7 -6
  159. package/public/docs/contributor-guide/architecture/detector-contract/plan-attribution.html +30 -0
  160. package/public/docs/contributor-guide/architecture/detector-contract/threshold-tables.html +30 -0
  161. package/public/docs/contributor-guide/architecture/detector-contract.html +7 -6
  162. package/public/docs/contributor-guide/architecture/drill-down/plan-graph.html +30 -0
  163. package/public/docs/contributor-guide/architecture/drill-down.html +7 -6
  164. package/public/docs/contributor-guide/architecture/impact-estimation/caveats-tuning-and-coverage.html +30 -0
  165. package/public/docs/contributor-guide/architecture/impact-estimation/stage-union-rollup.html +30 -0
  166. package/public/docs/contributor-guide/architecture/impact-estimation.html +7 -6
  167. package/public/docs/contributor-guide/architecture/index.html +7 -6
  168. package/public/docs/contributor-guide/architecture/overview.html +7 -6
  169. package/public/docs/contributor-guide/architecture/state-and-history/history-server-intake.html +30 -0
  170. package/public/docs/contributor-guide/architecture/state-and-history/run-comparison-internals.html +30 -0
  171. package/public/docs/contributor-guide/architecture/state-and-history.html +7 -6
  172. package/public/docs/contributor-guide/architecture/widget-rendering/findings-tab.html +30 -0
  173. package/public/docs/contributor-guide/architecture/widget-rendering/investigation-routing.html +30 -0
  174. package/public/docs/contributor-guide/architecture/widget-rendering/list-sort-mode.html +30 -0
  175. package/public/docs/contributor-guide/architecture/widget-rendering/reference-section.html +30 -0
  176. package/public/docs/contributor-guide/architecture/widget-rendering/row-expand-contract.html +30 -0
  177. package/public/docs/contributor-guide/architecture/widget-rendering.html +7 -6
  178. package/public/docs/contributor-guide/architecture/worker-protocol/cli-and-mcp.html +30 -0
  179. package/public/docs/contributor-guide/architecture/worker-protocol/core-and-event-validation.html +30 -0
  180. package/public/docs/contributor-guide/architecture/worker-protocol/evidence-availability.html +35 -0
  181. package/public/docs/contributor-guide/architecture/worker-protocol/evidence-report.html +30 -0
  182. package/public/docs/contributor-guide/architecture/worker-protocol.html +7 -11
  183. package/public/docs/contributor-guide/contributing.html +7 -6
  184. package/public/docs/contributor-guide/development-setup.html +11 -10
  185. package/public/docs/contributor-guide/testing.html +7 -6
  186. package/public/docs/hashmap.json +1 -1
  187. package/public/docs/index.html +5 -4
  188. package/public/docs/tuning-reference/anti-patterns.html +5 -4
  189. package/public/docs/tuning-reference/aqe.html +5 -4
  190. package/public/docs/tuning-reference/bottleneck-broadcast-sizing.html +5 -4
  191. package/public/docs/tuning-reference/bottleneck-cold-start.html +5 -4
  192. package/public/docs/tuning-reference/bottleneck-duplicate-plan-subtree.html +5 -4
  193. package/public/docs/tuning-reference/bottleneck-failures.html +5 -4
  194. package/public/docs/tuning-reference/bottleneck-gc.html +5 -4
  195. package/public/docs/tuning-reference/bottleneck-job-failure-rate.html +5 -4
  196. package/public/docs/tuning-reference/bottleneck-memory-utilization.html +5 -4
  197. package/public/docs/tuning-reference/bottleneck-retry-waste.html +5 -4
  198. package/public/docs/tuning-reference/bottleneck-shuffle.html +5 -4
  199. package/public/docs/tuning-reference/bottleneck-skew.html +5 -4
  200. package/public/docs/tuning-reference/bottleneck-slow-host.html +5 -4
  201. package/public/docs/tuning-reference/bottleneck-small-files.html +5 -4
  202. package/public/docs/tuning-reference/bottleneck-spill.html +5 -4
  203. package/public/docs/tuning-reference/bottleneck-straggler.html +5 -4
  204. package/public/docs/tuning-reference/bottleneck-tiny-tasks.html +5 -4
  205. package/public/docs/tuning-reference/bottleneck-utilization.html +5 -4
  206. package/public/docs/tuning-reference/caching.html +5 -4
  207. package/public/docs/tuning-reference/cluster-config.html +5 -4
  208. package/public/docs/tuning-reference/config.html +5 -4
  209. package/public/docs/tuning-reference/data-formats.html +5 -4
  210. package/public/docs/tuning-reference/index.html +5 -4
  211. package/public/docs/tuning-reference/intro.html +5 -4
  212. package/public/docs/tuning-reference/joins.html +5 -4
  213. package/public/docs/tuning-reference/memory-model.html +5 -4
  214. package/public/docs/tuning-reference/metrics.html +5 -4
  215. package/public/docs/tuning-reference/partitioning.html +5 -4
  216. package/public/docs/tuning-reference/pyspark.html +5 -4
  217. package/public/docs/tuning-reference/shuffle.html +5 -4
  218. package/public/docs/tuning-reference/spark-architecture.html +5 -4
  219. package/public/docs/tuning-reference/table-formats.html +5 -4
  220. package/public/docs/user-guide/alternative-log-retrieval/managed-platforms.html +33 -0
  221. package/public/docs/user-guide/alternative-log-retrieval/ssh-bastion.html +30 -0
  222. package/public/docs/user-guide/alternative-log-retrieval.html +11 -7
  223. package/public/docs/user-guide/getting-started/ci-and-automation.html +51 -0
  224. package/public/docs/user-guide/getting-started/reading-the-dashboard.html +30 -0
  225. package/public/docs/user-guide/getting-started/regression-metric-keys.html +30 -0
  226. package/public/docs/user-guide/getting-started/tuning-thresholds.html +33 -0
  227. package/public/docs/user-guide/getting-started.html +7 -11
  228. package/public/docs/user-guide/mcp-tools/compare-runs.html +84 -0
  229. package/public/docs/user-guide/mcp-tools/diagnose-run.html +52 -0
  230. package/public/docs/user-guide/mcp-tools/evaluate-budgets.html +55 -0
  231. package/public/docs/user-guide/mcp-tools/get-finding-documentation.html +43 -0
  232. package/public/docs/user-guide/mcp-tools/get-finding-evidence.html +49 -0
  233. package/public/docs/user-guide/mcp-tools/get-reference-doc.html +34 -0
  234. package/public/docs/user-guide/mcp-tools/get-run-summary.html +44 -0
  235. package/public/docs/user-guide/mcp-tools/list-runs.html +41 -0
  236. package/public/docs/user-guide/mcp-tools.html +12 -126
  237. package/public/docs/user-guide/run-comparison/how-stages-are-matched.html +30 -0
  238. package/public/docs/user-guide/run-comparison.html +7 -6
  239. package/public/docs/user-guide/understanding-findings.html +7 -6
  240. package/public/export-template.html +44 -44
  241. package/public/index.html +4 -4
  242. package/vendor-core/allocation.js +106 -0
  243. package/vendor-core/analyzer.js +13 -13
  244. package/vendor-core/api-merge-writes.js +80 -0
  245. package/vendor-core/cli/budgets.js +23 -9
  246. package/vendor-core/cli/collect-run.js +11 -4
  247. package/vendor-core/cli/regression-budgets.js +83 -0
  248. package/vendor-core/comparison-output.js +49 -0
  249. package/vendor-core/comparison-verdict.js +22 -22
  250. package/vendor-core/core-source-hash.txt +1 -1
  251. package/vendor-core/delta-commands.js +13 -0
  252. package/vendor-core/delta-log-paths.js +42 -0
  253. package/vendor-core/delta-targets.js +127 -0
  254. package/vendor-core/detectors.js +202 -68
  255. package/vendor-core/docs-content/detection/cache.md +3 -2
  256. package/vendor-core/docs-content/detection/cfg.md +9 -8
  257. package/vendor-core/docs-content/detection/chrn.md +1 -2
  258. package/vendor-core/docs-content/detection/cold.md +4 -2
  259. package/vendor-core/docs-content/detection/fail.md +3 -2
  260. package/vendor-core/docs-content/detection/gc.md +3 -2
  261. package/vendor-core/docs-content/detection/host.md +2 -1
  262. package/vendor-core/docs-content/detection/local.md +1 -1
  263. package/vendor-core/docs-content/detection/mem.md +5 -2
  264. package/vendor-core/docs-content/detection/plan.md +2 -1
  265. package/vendor-core/docs-content/detection/sfail.md +2 -1
  266. package/vendor-core/docs-content/detection/shape.md +5 -4
  267. package/vendor-core/docs-content/detection/skew.md +3 -1
  268. package/vendor-core/docs-content/detection/slow.md +2 -2
  269. package/vendor-core/docs-content/detection/spec.md +2 -3
  270. package/vendor-core/docs-content/detection/spill.md +1 -1
  271. package/vendor-core/docs-site-config.js +1 -1
  272. package/vendor-core/effective-conf.js +107 -0
  273. package/vendor-core/efficiency-model.js +8 -6
  274. package/vendor-core/event-handlers.js +255 -56
  275. package/vendor-core/event-schemas.js +5 -1
  276. package/vendor-core/evidence-report.js +31 -14
  277. package/vendor-core/finding-generic-recommendation.js +20 -1
  278. package/vendor-core/finding-names.js +7 -0
  279. package/vendor-core/finding-presentation.js +61 -26
  280. package/vendor-core/finding-tag-help.js +1 -1
  281. package/vendor-core/finding-types.js +12 -0
  282. package/vendor-core/format-utils.js +4 -3
  283. package/vendor-core/impact-estimator.js +20 -2
  284. package/vendor-core/impact-format.js +14 -13
  285. package/vendor-core/impact-model.js +27 -5
  286. package/vendor-core/ingest.js +4 -2
  287. package/vendor-core/list-runs.js +5 -2
  288. package/vendor-core/mcp-server-factory.js +13 -6
  289. package/vendor-core/mcp-tools.js +43 -34
  290. package/vendor-core/model-assembler.js +23 -1
  291. package/vendor-core/parser-worker.js +1 -1
  292. package/vendor-core/plan-graph-model.js +1 -1
  293. package/vendor-core/plan-summary.js +13 -10
  294. package/vendor-core/proxy.js +3 -1
  295. package/vendor-core/python-stage.js +25 -0
  296. package/vendor-core/recommendation-rollup.js +16 -9
  297. package/vendor-core/redact.js +51 -10
  298. package/vendor-core/remediation.js +20 -0
  299. package/vendor-core/run-comparison.js +118 -140
  300. package/vendor-core/run-interpretation.js +2 -1
  301. package/vendor-core/run-metrics.js +198 -0
  302. package/vendor-core/run-output.js +26 -0
  303. package/vendor-core/run-totals.js +24 -0
  304. package/vendor-core/run-verdict.js +3 -4
  305. package/vendor-core/scorecard-estimates.js +1 -0
  306. package/vendor-core/session-snapshot.js +7 -0
  307. package/vendor-core/shs-schemas.js +2 -2
  308. package/vendor-core/spark-memory.js +17 -0
  309. package/vendor-core/stage-alignment.js +262 -0
  310. package/vendor-core/stage-identity.js +112 -0
  311. package/vendor-core/stage-plan-nodes.js +18 -0
  312. package/vendor-core/stage-quantiles.js +4 -0
  313. package/vendor-core/types.js +54 -1
  314. package/vendor-core/wasted-core-hours.js +10 -7
  315. package/vendor-core/write-target-args.js +48 -0
  316. package/vendor-core/write-targets.js +295 -0
  317. package/public/assets/AutoscalingChurn-CUSIvvab.js +0 -1
  318. package/public/assets/CacheUtilization-Ce0K6_X2.js +0 -1
  319. package/public/assets/CachingOpportunity-Cn-LmRA4.js +0 -1
  320. package/public/assets/ColdStart-BoiYzEyV.js +0 -1
  321. package/public/assets/ConfigAudit-BoJc6PAw.js +0 -1
  322. package/public/assets/CoreUsageArea-BFfXKu7K.js +0 -1
  323. package/public/assets/Dashboard-Bo_1jGqH.js +0 -2
  324. package/public/assets/DisclosureContext-LYzHroHO.js +0 -1
  325. package/public/assets/DuplicatePlanSubtree-BaAuGAoo.js +0 -1
  326. package/public/assets/ExecutorUtilization-OE6VMFkG.js +0 -1
  327. package/public/assets/GcPressure-AoL_ZNrV.js +0 -1
  328. package/public/assets/IncompleteRun-Bv6IzgsQ.js +0 -1
  329. package/public/assets/JobFailures-DF3U2z1f.js +0 -1
  330. package/public/assets/MemoryUtilization-B65OVZlq.js +0 -1
  331. package/public/assets/OverBroadcast-DXbJREDl.js +0 -1
  332. package/public/assets/PartitionSizing-DZLnjy0d.js +0 -1
  333. package/public/assets/PlanGraphRoute-C-BGmC6j.js +0 -1
  334. package/public/assets/RetryWaste-BbEPsdMw.js +0 -1
  335. package/public/assets/ShuffleIO-wZB94LS8.js +0 -1
  336. package/public/assets/Skew-SpQE7zeJ.js +0 -1
  337. package/public/assets/SlowHost-DwDBCTvr.js +0 -1
  338. package/public/assets/SmallFiles-yjY_6sT1.js +0 -1
  339. package/public/assets/SpeculationWaste-73PDmj0c.js +0 -1
  340. package/public/assets/Spill-DEw0Kd1i.js +0 -1
  341. package/public/assets/StageFailed-BEfXdEJc.js +0 -1
  342. package/public/assets/StageFindingGroup-Bzm2TY03.js +0 -1
  343. package/public/assets/StageShape-CzR4jHOt.js +0 -1
  344. package/public/assets/StageSlowness-vMPsnAaa.js +0 -1
  345. package/public/assets/Straggler-Cf__OGl5.js +0 -1
  346. package/public/assets/TaskFailures-BZ8kLZiO.js +0 -1
  347. package/public/assets/UnderBroadcast-CBm8ZPoS.js +0 -1
  348. package/public/assets/index-CEM9bz_x.js +0 -13
  349. package/public/assets/parser-worker-Q5KCPdKT.js +0 -122
  350. package/public/assets/useRecentFiles-BrBl-AP1.js +0 -11
  351. package/public/docs/assets/chunks/@localSearchIndexroot.DbZy5EtM.js +0 -1
  352. package/public/docs/assets/contributor-guide_architecture_board-widgets.md.VTcYTH4d.js +0 -1
  353. package/public/docs/assets/contributor-guide_architecture_board-widgets.md.VTcYTH4d.lean.js +0 -1
  354. package/public/docs/assets/contributor-guide_architecture_detector-contract.md.CsqZHz_T.js +0 -1
  355. package/public/docs/assets/contributor-guide_architecture_detector-contract.md.CsqZHz_T.lean.js +0 -1
  356. package/public/docs/assets/contributor-guide_architecture_drill-down.md.CFNCE8JB.js +0 -1
  357. package/public/docs/assets/contributor-guide_architecture_drill-down.md.CFNCE8JB.lean.js +0 -1
  358. package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.HT9IJGIl.js +0 -1
  359. package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.HT9IJGIl.lean.js +0 -1
  360. package/public/docs/assets/contributor-guide_architecture_index.md.3TO9ic6w.js +0 -1
  361. package/public/docs/assets/contributor-guide_architecture_index.md.3TO9ic6w.lean.js +0 -1
  362. package/public/docs/assets/contributor-guide_architecture_overview.md.CTEqVeV4.js +0 -1
  363. package/public/docs/assets/contributor-guide_architecture_overview.md.CTEqVeV4.lean.js +0 -1
  364. package/public/docs/assets/contributor-guide_architecture_state-and-history.md.DOTVHjTV.js +0 -1
  365. package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.BmpARZve.js +0 -1
  366. package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.BmpARZve.lean.js +0 -1
  367. package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.21pTYhFF.js +0 -6
  368. package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.21pTYhFF.lean.js +0 -1
  369. package/public/docs/assets/contributor-guide_contributing.md.B2E9ni79.js +0 -1
  370. package/public/docs/assets/contributor-guide_development-setup.md.CjgZJwjK.js +0 -15
  371. package/public/docs/assets/contributor-guide_development-setup.md.CjgZJwjK.lean.js +0 -1
  372. package/public/docs/assets/contributor-guide_testing.md.DTwhpoN2.js +0 -1
  373. package/public/docs/assets/user-guide_alternative-log-retrieval.md.ZHPSbBkF.js +0 -1
  374. package/public/docs/assets/user-guide_alternative-log-retrieval.md.ZHPSbBkF.lean.js +0 -1
  375. package/public/docs/assets/user-guide_getting-started.md.CEa2ZX-l.js +0 -6
  376. package/public/docs/assets/user-guide_getting-started.md.CEa2ZX-l.lean.js +0 -1
  377. package/public/docs/assets/user-guide_mcp-tools.md.dQwIvdTF.js +0 -131
  378. package/public/docs/assets/user-guide_mcp-tools.md.dQwIvdTF.lean.js +0 -1
  379. package/public/docs/assets/user-guide_run-comparison.md.ChLI3MK0.js +0 -1
  380. package/public/docs/assets/user-guide_run-comparison.md.ChLI3MK0.lean.js +0 -1
  381. package/public/docs/assets/user-guide_understanding-findings.md.D1ccs60C.js +0 -1
  382. /package/public/docs/assets/{user-guide_understanding-findings.md.D1ccs60C.lean.js → user-guide_understanding-findings.md.Dqt2H_C1.lean.js} +0 -0
@@ -0,0 +1,20 @@
1
+ // Constructors for the structured `remediation` a detector attaches next to its prose
2
+ // `recommendation`: only where the detector already names the Spark property, never an invented one.
3
+
4
+
5
+
6
+
7
+ /** The property should be raised; `suggested` is the value when the detector computed one. */
8
+ export function increaseConf(key , suggested = null) {
9
+ return { kind: 'conf', key, direction: 'increase', suggested };
10
+ }
11
+
12
+ /** The property should be lowered; `suggested` is the value when the detector computed one. */
13
+ export function decreaseConf(key , suggested = null) {
14
+ return { kind: 'conf', key, direction: 'decrease', suggested };
15
+ }
16
+
17
+ /** The property should take a specific value (a switch or a class name), not move up or down. */
18
+ export function setConf(key , suggested = null) {
19
+ return { kind: 'conf', key, direction: 'set', suggested };
20
+ }
@@ -1,9 +1,12 @@
1
1
  import { computeWallClock } from './wall-clock.js';
2
2
  import { normalizeDetail } from './detectors.js';
3
- import { cyrb53 } from './string-hash.js';
3
+ import { normalizeStageName, stageIdentityWith, identityIndexWith, pairEqualCounts } from './stage-identity.js';
4
+ import { computeAllocation } from './allocation.js';
5
+ import { totalExecutorCpuMs, withEarlierAttempts } from './run-totals.js';
4
6
  import { captureSnapshot } from './session-snapshot.js';
7
+ import { alignStages, compileNormalizePatterns, COMPARISON_SCHEMA_VERSION, } from './stage-alignment.js';
5
8
  import { tunedRunNote } from './threshold-overrides.js';
6
-
9
+
7
10
 
8
11
 
9
12
  import { isIncompleteRun } from './check-coverage.js';
@@ -18,13 +21,26 @@ import { summarizeRunOutcome } from './run-outcome.js';
18
21
 
19
22
 
20
23
 
24
+
25
+
26
+
27
+
21
28
 
22
29
 
23
-
30
+
31
+
32
+
33
+
24
34
 
35
+
36
+
37
+
38
+
39
+
40
+
25
41
 
26
42
 
27
-
43
+
28
44
 
29
45
 
30
46
 
@@ -38,73 +54,10 @@ function jobOutcome(snapshot ) {
38
54
  return { failedJobs, totalJobs, incomplete: isIncompleteRun(snapshot.catalog) };
39
55
  }
40
56
 
41
- // Replace run-varying tokens (digit runs, long hex ids) with a stable marker so
42
- // the same logical stage across two runs normalizes to one identity.
43
- export function normalizeStageName(name ) {
44
- return String(name)
45
- .toLowerCase()
46
- .replace(/\b[0-9a-f]{8,}\b/g, '#') // hex ids/uuids first (they contain digits)
47
- .replace(/\d+/g, '#')
48
- .replace(/\s+/g, ' ')
49
- .trim();
50
- }
51
-
52
- // Bottom-up, order-independent structural identity of a resolved plan tree:
53
- // each node folds its normalized name/detail with its children's digests
54
- // (children sorted, so AQE picking a different broadcast side still matches),
55
- // so two plans collide only when their whole shape and every node's detail
56
- // agree. `normalizeDetail` strips the run-to-run noise (expr ids, `plan_id=`,
57
- // codegen numbers, AQE build-side choice, commutative-operand order). Each
58
- // node folds to a fixed-length cyrb53 digest (JSON.stringify-encoded, so
59
- // detail text containing `<`/`>`/`{`/`,` can't collide two different plans)
60
- // instead of embedding full child identity strings, which would re-escape
61
- // every level below and blow identity size up to ~2^depth on the 20-60+
62
- // operator-deep plans real Spark produces.
63
- function planTreeIdentity(root ) {
64
- if (!root) return null;
65
- function visit(node ) {
66
- const childDigests = (node.children ?? []).map(visit).sort();
67
- return cyrb53(JSON.stringify([normalizeStageName(node.name ?? ''), normalizeDetail(node.detail ?? ''), childDigests]));
68
- }
69
- return visit(root);
70
- }
71
-
72
- // Plan identity for the stage's SQL execution, scoped to only the plan nodes
73
- // this stage actually ran (`node.stageIds`), not the whole tree: two stages
74
- // sharing one SQL execution (e.g. a self-join's two Exchange stages) otherwise
75
- // collapse onto one identity regardless of which part of the plan each ran.
76
- // Falls back to the coarser whole-tree identity when the stage has no
77
- // attributed nodes (hand-built snapshots without `stageIds`, or unmatched
78
- // accumulables).
79
- function sqlNodeIdentity(stage , snapshot ) {
80
- const execId = stage.sqlExecutionId;
81
- if (execId == null) return '';
82
- const root = snapshot.sql.get(execId)?.planTree ?? null;
83
- if (!root) return '';
84
- const fingerprints = [];
85
- (function collect(node ) {
86
- if (node.stageIds?.includes(stage.id)) {
87
- fingerprints.push(JSON.stringify([normalizeStageName(node.name ?? ''), normalizeDetail(node.detail ?? '')]));
88
- }
89
- for (const child of node.children ?? []) collect(child);
90
- })(root);
91
- if (fingerprints.length === 0) return planTreeIdentity(root) ?? '';
92
- return cyrb53(JSON.stringify(fingerprints.sort()));
93
- }
94
-
95
- export function stageIdentity(stage , snapshot ) {
96
- return normalizeStageName(stage.name ?? '') + '§' + sqlNodeIdentity(stage, snapshot);
97
- }
57
+ export { normalizeStageName };
98
58
 
99
- function identityIndex(snapshot ) {
100
- const byIdentity = new Map ();
101
- for (const [id, stage] of snapshot.stages) {
102
- const key = stageIdentity(stage, snapshot);
103
- const ids = byIdentity.get(key);
104
- if (ids) ids.push(id);
105
- else byIdentity.set(key, [id]);
106
- }
107
- return byIdentity;
59
+ export function stageIdentity(stage , snapshot ) {
60
+ return stageIdentityWith(stage, snapshot, normalizeDetail);
108
61
  }
109
62
 
110
63
  export function matchStages(baseSnap , candSnap )
@@ -113,10 +66,8 @@ export function matchStages(baseSnap , candSnap
113
66
 
114
67
 
115
68
  {
116
- const baseIdx = identityIndex(baseSnap);
117
- const candIdx = identityIndex(candSnap);
118
- const pairs = [];
119
- const matchedIdentities = new Set ();
69
+ const baseIdx = identityIndexWith(baseSnap, (stage) => stageIdentity(stage, baseSnap));
70
+ const candIdx = identityIndexWith(candSnap, (stage) => stageIdentity(stage, candSnap));
120
71
  const collisionIdentities = new Set ();
121
72
  // A collision is a single-run property: more than one stage in the SAME run
122
73
  // shares an identity. Recorded per-run regardless of whether the other run
@@ -124,26 +75,7 @@ export function matchStages(baseSnap , candSnap
124
75
  // don't, so this set means "was ambiguous", not "stayed unpaired".
125
76
  for (const idx of [baseIdx, candIdx])
126
77
  for (const [identity, ids] of idx) if (ids.length > 1) collisionIdentities.add(identity);
127
- // An identity colliding equally on both sides has no genuine ambiguity about
128
- // *count*, so pair its stages off positionally by sorted id rather than
129
- // dropping them. This is exact when the identity actually distinguishes
130
- // stages (e.g. self-comparing a run: every stage matches itself). It's a
131
- // best-effort guess when the identity is coarse (no SQL/attribution) and
132
- // the two sides are genuinely different runs -- two unrelated same-named
133
- // stages could get cross-paired. Accepted tradeoff: dropping them instead
134
- // would also sacrifice the exact-self-comparison case, which matters more.
135
- for (const identity of [...baseIdx.keys()].sort()) {
136
- const candIds = candIdx.get(identity);
137
- if (!candIds) continue;
138
- const baseIds = baseIdx.get(identity) ;
139
- if (baseIds.length !== candIds.length) continue;
140
- const sortedBaseIds = [...baseIds].sort((a, b) => a - b);
141
- const sortedCandIds = [...candIds].sort((a, b) => a - b);
142
- for (let i = 0; i < sortedBaseIds.length; i++) {
143
- pairs.push({ identity, baseId: sortedBaseIds[i], candId: sortedCandIds[i] });
144
- }
145
- matchedIdentities.add(identity);
146
- }
78
+ const { pairs, matchedIdentities } = pairEqualCounts(baseIdx, candIdx);
147
79
  const total = baseSnap.stages.size + candSnap.stages.size;
148
80
  // Both runs stage-less: nothing to compare, not "nothing matched".
149
81
  const coverage = total === 0 ? 1 : (2 * pairs.length) / total;
@@ -199,6 +131,7 @@ function skewRatios(stages ) {
199
131
  export const COMPARISON_METRIC_KEYS = [
200
132
  'wallClock', 'shuffleSpill', 'taskSkew', 'failedTaskRate', 'diskSpill', 'gcTime',
201
133
  'inputBytes', 'outputBytes', 'executorRunTime', 'taskCount', 'executorsAdded',
134
+ 'executorCpuTime', 'allocatedCoreHours',
202
135
  ];
203
136
 
204
137
  // Volume/count metrics, not cost metrics: more or less input/output data, or
@@ -228,20 +161,24 @@ function metric(
228
161
 
229
162
  export function metricDeltas(baseSnap , candSnap ) {
230
163
  const out = [];
164
+ // Run totals count every task attempt, failed and speculative ones too (run-totals.ts), the
165
+ // same sums the CLI metrics block reports; skew stays on each stage's latest attempt.
166
+ const attemptsOf = (snap ) => withEarlierAttempts([...snap.stages.values()]);
231
167
 
232
168
  // Wall-clock: always computable (computeWallClock tolerates a null app).
233
169
  out.push(metric('wallClock', 'Wall-clock duration',
234
170
  computeWallClock(baseSnap.app, baseSnap.stages).total,
235
171
  computeWallClock(candSnap.app, candSnap.stages).total));
236
172
 
237
- // Shuffle spill over the whole run: a sum needs no stage matching, and
238
- // matching is unreliable on real logs (see plan rationale), so scope it to
239
- // all stages exactly like task-skew and failed-rate below.
240
- const bSpill = sumField([...baseSnap.stages.values()], 'memoryBytesSpilled');
241
- const cSpill = sumField([...candSnap.stages.values()], 'memoryBytesSpilled');
242
- out.push(metric('shuffleSpill', 'Shuffle spill',
173
+ // Memory spill (Spark's memoryBytesSpilled) over the whole run: a sum needs no
174
+ // stage matching, and matching is unreliable on real logs, so scope it to all
175
+ // stages exactly like task-skew and failed-rate below. The key stays
176
+ // `shuffleSpill` so existing --regression-metric callers keep working.
177
+ const bSpill = sumField(attemptsOf(baseSnap), 'memoryBytesSpilled');
178
+ const cSpill = sumField(attemptsOf(candSnap), 'memoryBytesSpilled');
179
+ out.push(metric('shuffleSpill', 'Memory spill',
243
180
  bSpill.present ? bSpill.sum : null, cSpill.present ? cSpill.sum : null,
244
- { unavailableReason: bSpill.present && cSpill.present ? undefined : 'No shuffle-spill data recorded for a run' }));
181
+ { unavailableReason: bSpill.present && cSpill.present ? undefined : 'No memory-spill data recorded for a run' }));
245
182
 
246
183
  // Task skew: p95 of per-stage ratio across the whole run.
247
184
  const bSkew = p95(skewRatios([...baseSnap.stages.values()]));
@@ -250,10 +187,10 @@ export function metricDeltas(baseSnap , candSnap
250
187
  { unavailableReason: bSkew != null && cSkew != null ? undefined : 'No stage had measurable duration for a run' }));
251
188
 
252
189
  // Failed-task rate: Σ failedTasks / Σ taskCount.
253
- const bTasks = sumField([...baseSnap.stages.values()], 'taskCount');
254
- const cTasks = sumField([...candSnap.stages.values()], 'taskCount');
255
- const bFailed = sumField([...baseSnap.stages.values()], 'failedTasks');
256
- const cFailed = sumField([...candSnap.stages.values()], 'failedTasks');
190
+ const bTasks = sumField(attemptsOf(baseSnap), 'taskCount');
191
+ const cTasks = sumField(attemptsOf(candSnap), 'taskCount');
192
+ const bFailed = sumField(attemptsOf(baseSnap), 'failedTasks');
193
+ const cFailed = sumField(attemptsOf(candSnap), 'failedTasks');
257
194
  // Guard on BOTH inputs: a missing `failedTasks` field must render Unavailable,
258
195
  // not a false 0% rate (dividing an absent-and-therefore-0 numerator).
259
196
  const bRate = bTasks.present && bTasks.sum > 0 && bFailed.present ? bFailed.sum / bTasks.sum : null;
@@ -265,8 +202,8 @@ export function metricDeltas(baseSnap , candSnap
265
202
  // carries (set in finalizeStage). Correct at any match coverage, like the
266
203
  // sums above; no parser or detector change.
267
204
  const sumMetric = (key , label , field , reason ) => {
268
- const b = sumField([...baseSnap.stages.values()], field);
269
- const c = sumField([...candSnap.stages.values()], field);
205
+ const b = sumField(attemptsOf(baseSnap), field);
206
+ const c = sumField(attemptsOf(candSnap), field);
270
207
  out.push(metric(key, label, b.present ? b.sum : null, c.present ? c.sum : null,
271
208
  { unavailableReason: b.present && c.present ? undefined : reason }));
272
209
  };
@@ -285,6 +222,18 @@ export function metricDeltas(baseSnap , candSnap
285
222
  out.push(metric('executorsAdded', 'Executors added', bExec, cExec,
286
223
  { unavailableReason: bExec != null && cExec != null ? undefined : 'No executor events recorded for a run' }));
287
224
 
225
+ // Executor CPU time (ms) and allocated core-hours cost resources, so less is better. CPU time is
226
+ // null, not 0, on a run whose log never recorded it (older Spark).
227
+ const cpuMs = (snap ) => totalExecutorCpuMs(attemptsOf(snap));
228
+ const bCpu = cpuMs(baseSnap), cCpu = cpuMs(candSnap);
229
+ out.push(metric('executorCpuTime', 'Executor CPU time', bCpu, cCpu,
230
+ { unavailableReason: bCpu != null && cCpu != null ? undefined : 'No executor CPU time recorded for a run' }));
231
+ const coreHours = (snap ) =>
232
+ (snap.executors ? computeAllocation(snap).coreHours : null);
233
+ const bCore = coreHours(baseSnap), cCore = coreHours(candSnap);
234
+ out.push(metric('allocatedCoreHours', 'Allocated core-hours', bCore, cCore,
235
+ { unavailableReason: bCore != null && cCore != null ? undefined : 'No executor lifecycle or core count recorded for a run' }));
236
+
288
237
  return out;
289
238
  }
290
239
 
@@ -333,19 +282,19 @@ function namesConflict(a , b )
333
282
  }
334
283
 
335
284
  // A pair's skew ratio is null when the stage's duration wasn't measurable (see
336
- // `stageSkewRatio`). `baseId`/`candId` are carried through so a consumer can
337
- // key rows uniquely: two pairs can share one `identity` (a same-run collision
338
- // resolved positionally in matchStages), but never the same baseId+candId.
339
- function stageSkewDeltas(
340
- baseSnap ,
341
- candSnap ,
342
- match ,
343
- ) {
344
- return match.pairs.map((p) => {
345
- const b = stageSkewRatio(baseSnap.stages.get(p.baseId) );
346
- const c = stageSkewRatio(candSnap.stages.get(p.candId) );
347
- return { identity: p.identity, baseId: p.baseId, candId: p.candId, baseline: b, candidate: c, delta: b != null && c != null ? c - b : null };
348
- }).sort((x, y) => x.identity < y.identity ? -1 : x.identity > y.identity ? 1 : 0);
285
+ // `stageSkewRatio`). Rows come from the aligner's `stagePairs`, so the table and the coverage
286
+ // banner agree. A pair covers one stage per side here; `pairId` is the unique row key.
287
+ function stageSkewDeltas(baseSnap , candSnap , stagePairs ) {
288
+ return stagePairs.map((p) => {
289
+ const baseId = p.baseStageIds[0], candId = p.candStageIds[0];
290
+ const baseStage = baseSnap.stages.get(baseId) ;
291
+ const b = stageSkewRatio(baseStage);
292
+ const c = stageSkewRatio(candSnap.stages.get(candId) );
293
+ return {
294
+ pairId: p.pairId, name: normalizeStageName(baseStage.name ?? ''), baseId, candId,
295
+ baseline: b, candidate: c, delta: b != null && c != null ? c - b : null,
296
+ };
297
+ }).sort((x, y) => x.name < y.name ? -1 : x.name > y.name ? 1 : x.baseId - y.baseId);
349
298
  }
350
299
 
351
300
  // Compact, view-friendly per-stage record for the manual stage-pinning panel.
@@ -387,6 +336,14 @@ function stageList(snap )
387
336
  // caller's reference, so one shared empty Map is safe here.
388
337
  const EMPTY_TASK_DATA = new Map ();
389
338
 
339
+
340
+
341
+
342
+
343
+
344
+
345
+
346
+
390
347
  // Wraps the analyze()-to-compareRuns() snapshot-building sequence shared by
391
348
  // the CLI's --baseline path and mcp-tools.ts's compareRuns tool: both need
392
349
  // captureSnapshot (with an empty taskDataCache, the interactive drill-down
@@ -395,50 +352,65 @@ const EMPTY_TASK_DATA = new Map ();
395
352
  export function buildComparison(
396
353
  baseline ,
397
354
  candidate ,
355
+ options ,
398
356
  ) {
399
357
  return compareRuns(
400
358
  { label: baseline.label, snapshot: captureSnapshot(baseline.appModel, baseline.catalog, EMPTY_TASK_DATA) },
401
359
  { label: candidate.label, snapshot: captureSnapshot(candidate.appModel, candidate.catalog, EMPTY_TASK_DATA) },
360
+ options,
402
361
  );
403
362
  }
404
363
 
405
- // matchStages' coverage is a Dice coefficient: (2 * pairs.length) / (baseCount
406
- // + candCount). Below 0.5, more than half of each run's stages went unpaired,
407
- // so the stage-level rows (stageSkew, baseStages/candStages) mostly show
408
- // unrelated work side by side rather than the same stage before/after -- the
409
- // comparison is dominated by guesswork, not genuine pairing. 0.5 is thus the
410
- // natural midpoint for "more matched than not," not an arbitrary tuning knob.
411
- const LOW_COVERAGE_THRESHOLD = 0.5;
364
+ // `ok` needs this share of both runs' executor run time to sit in paired stages. Run time, not
365
+ // stage count: the stages a run spends its time in decide whether a per-stage delta compares the
366
+ // same work, and a count lets many small matched stages hide an unmatched heavy one.
367
+ const RUNTIME_COVERAGE_THRESHOLD = 0.9;
412
368
 
413
369
  export function compareRuns(
414
370
  baseline ,
415
371
  candidate ,
372
+ options = {},
416
373
  ) {
417
374
  const baseSnap = baseline.snapshot, candSnap = candidate.snapshot;
375
+ // The old exact matcher stays the source of `matchedCoverage`; pairs and the gate come from the aligner.
418
376
  const match = matchStages(baseSnap, candSnap);
377
+ const alignment = alignStages(baseSnap, candSnap, { normalizePath: compileNormalizePatterns(options.normalizePath) });
378
+ const runtimeCoverage = alignment.runtimeCoverage;
419
379
  // Name equality is a weak confidence signal, not a hard gate: renaming a job
420
380
  // is the normal way to label an A/B experiment, so a mismatch must not block
421
381
  // the (matching-free, name-independent) deltas. Surface it as `low` instead.
422
382
  const namesDiffer = namesConflict(baseSnap.app, candSnap.app);
423
- // Coverage is the other half of the signal: identical names on two runs that
424
- // barely share any stages are just as misleading as differing names on two
425
- // runs that match well, so either condition alone drops confidence to `low`.
426
- const lowCoverage = match.coverage < LOW_COVERAGE_THRESHOLD;
427
- const reason = namesDiffer && lowCoverage
428
- ? `Run names differ and only ${(match.coverage * 100).toFixed(0)}% of stages matched, so deltas may compare different work.`
383
+ // Runtime coverage is the other half of the signal: identical names on two runs whose heavy
384
+ // stages do not pair are just as misleading as differing names on two runs that pair well, so
385
+ // either condition alone drops confidence to `low`. No run time at all is `insufficient`.
386
+ const insufficient = runtimeCoverage === null;
387
+ const lowCoverage = runtimeCoverage !== null && runtimeCoverage < RUNTIME_COVERAGE_THRESHOLD;
388
+ const confidence = insufficient ? 'insufficient' : namesDiffer || lowCoverage ? 'low' : 'ok';
389
+ // Rounded down, so a share just under the gate never reads as 90%.
390
+ const share = runtimeCoverage === null ? '' : `${Math.floor(runtimeCoverage * 100)}%`;
391
+ const reason = insufficient
392
+ ? `${namesDiffer ? 'Run names differ and no' : 'No'} executor run time was recorded in either run, so there is no work to compare.`
393
+ : namesDiffer && lowCoverage
394
+ ? `Run names differ and only ${share} of executor run time is in matched stages, so deltas may compare different work.`
429
395
  : namesDiffer
430
396
  ? 'Run names differ, so deltas may compare different work.'
431
397
  : lowCoverage
432
- ? `Only ${(match.coverage * 100).toFixed(0)}% of stages matched between runs, so per-stage rows mostly compare unrelated work.`
398
+ ? `Only ${share} of executor run time is in matched stages, so per-stage rows mostly compare unrelated work.`
433
399
  : null;
434
400
  return {
435
401
  baselineLabel: baseline.label, candidateLabel: candidate.label,
436
- confidence: namesDiffer || lowCoverage ? 'low' : 'ok',
402
+ confidence,
437
403
  reason,
438
404
  matchedCoverage: match.coverage,
405
+ comparisonSchemaVersion: COMPARISON_SCHEMA_VERSION,
406
+ runtimeCoverage,
407
+ stagePairs: alignment.pairs,
408
+ unmatched: alignment.unmatched,
409
+ replanned: alignment.replanned,
410
+ bookkeepingStageIds: alignment.bookkeepingStageIds,
439
411
  metrics: metricDeltas(baseSnap, candSnap),
440
412
  findings: findingsDelta(baseSnap, candSnap),
441
- stageSkew: stageSkewDeltas(baseSnap, candSnap, match),
413
+ stageSkew: stageSkewDeltas(baseSnap, candSnap, alignment.pairs),
442
414
  baseStages: stageList(baseSnap),
443
415
  candStages: stageList(candSnap),
444
416
  jobOutcomes: { baseline: jobOutcome(baseSnap), candidate: jobOutcome(candSnap) },
@@ -467,16 +439,22 @@ export function renderComparisonMarkdown(
467
439
  const lines = ['', '## Comparison to baseline', ''];
468
440
  if (tuned) lines.push(`- Tuned thresholds (both runs): ${tunedRunNote(tuned)}`, '');
469
441
  if (verdict) {
470
- // The dashboard comparison page's headline: run A is the baseline, run B the candidate.
471
- lines.push(`Run A: ${comparison.baselineLabel} · Run B: ${comparison.candidateLabel}`, '', verdict.title);
442
+ // Names each run by its label (MCP's run IDs), unless the labels are just the role names the
443
+ // CLI passes, where the verdict below already says baseline and candidate.
444
+ if (comparison.baselineLabel !== 'baseline' || comparison.candidateLabel !== 'candidate') {
445
+ lines.push(`Baseline: ${comparison.baselineLabel} · Candidate: ${comparison.candidateLabel}`, '');
446
+ }
447
+ lines.push(verdict.title);
472
448
  if (verdict.sentences.length > 0) lines.push('', verdict.sentences.join(' '));
473
449
  lines.push('');
474
450
  }
475
- if (comparison.confidence === 'low') {
476
- lines.push(`- confidence: low, ${comparison.reason}`);
451
+ if (comparison.confidence !== 'ok') {
452
+ lines.push(`- confidence: ${comparison.confidence}, ${comparison.reason}`);
477
453
  lines.push('');
478
454
  }
479
455
  lines.push(`- matched stage coverage: ${(comparison.matchedCoverage * 100).toFixed(1)}%`);
456
+ lines.push(`- runtime coverage: ${comparison.runtimeCoverage === null ? 'n/a' : `${(comparison.runtimeCoverage * 100).toFixed(1)}%`}`);
457
+ lines.push(`- stage pairs: ${comparison.stagePairs.length}, unmatched: ${comparison.unmatched.baseStageIds.length} baseline / ${comparison.unmatched.candStageIds.length} candidate, Delta bookkeeping: ${comparison.bookkeepingStageIds.baseStageIds.length} baseline / ${comparison.bookkeepingStageIds.candStageIds.length} candidate`);
480
458
  lines.push('');
481
459
  lines.push('### Metric deltas');
482
460
  lines.push('');
@@ -192,6 +192,7 @@ function interpretEfficiency(appModel ) {
192
192
  app: appModel.app,
193
193
  stages: appModel.stages,
194
194
  executorsAdded: appModel.executors.added,
195
+ executorsRemoved: appModel.executors.removed,
195
196
  runAggregates: appModel.runAggregates,
196
197
  });
197
198
  }
@@ -270,7 +271,7 @@ export function interpretRun(appModel , catalog , configFindi
270
271
  coverage: interpretCoverage(appModel, allFindings),
271
272
  runShape: interpretRunShape(appModel),
272
273
  wallClock: computeWallClock(appModel.app, appModel.stages),
273
- wastedCoreHours: computeWastedCoreHours(appModel.app, appModel.executors.added, appModel.runAggregates),
274
+ wastedCoreHours: computeWastedCoreHours(appModel.app, appModel.executors.added, appModel.runAggregates, appModel.executors.removed),
274
275
  efficiency: interpretEfficiency(appModel),
275
276
  wallClockReliable: checkConcurrentJobGroups(appModel.jobs).wallClockReliable,
276
277
  etlPhases: attributeEtlPhases(appModel.stages),
@@ -0,0 +1,198 @@
1
+ // The machine-readable metrics block of the CLI's JSON output: run-level totals and per-stage rows
2
+ // an automated tuning loop can compare across runs without reading a report. Its schemaVersion
3
+ // moves independently of the evidence report's. A figure the log cannot provide is null, never 0.
4
+ import { computeAllocation, } from './allocation.js';
5
+ import { computeSkewRatio, ENTRY_BY_TYPE, } from './detectors.js';
6
+ import { totalExecutorCpuMs, withEarlierAttempts } from './run-totals.js';
7
+ import { isPythonStage } from './python-stage.js';
8
+ import { stageIdentity } from './run-comparison.js';
9
+ import { hasCompleteApplicationInterval } from './scorecard-estimates.js';
10
+ import { effectiveThresholds } from './threshold-overrides.js';
11
+ import { computeWallClock } from './wall-clock.js';
12
+
13
+
14
+ export const METRICS_SCHEMA_VERSION = 1;
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+
50
+
51
+
52
+
53
+
54
+
55
+
56
+
57
+
58
+
59
+
60
+
61
+
62
+
63
+
64
+
65
+
66
+
67
+
68
+
69
+
70
+
71
+
72
+ const finite = (v ) => typeof v === 'number' && Number.isFinite(v);
73
+
74
+ // Σ of a field over the stages that carry a finite value; null when none does.
75
+ function sumOf(stages , pick ) {
76
+ let sum = 0, present = false;
77
+ for (const s of stages) {
78
+ const v = pick(s);
79
+ if (finite(v)) { sum += v; present = true; }
80
+ }
81
+ return present ? sum : null;
82
+ }
83
+
84
+ // 0 means either no execution memory used or not recorded; only a positive peak is reported.
85
+ function peakExecutionMemory(stages ) {
86
+ const peaks = stages.map((s) => s.peakExecutionMemoryMax).filter((v) => finite(v) && v > 0);
87
+ return peaks.length > 0 ? Math.max(...peaks) : null;
88
+ }
89
+
90
+ function maxSkew(stages , minTasksForP95 ) {
91
+ const ratios = stages
92
+ .map((s) => computeSkewRatio(s, minTasksForP95)?.ratio)
93
+ .filter((r) => finite(r));
94
+ return ratios.length > 0 ? Math.max(...ratios) : null;
95
+ }
96
+
97
+ // Failed stage attempts and stages submitted more than once, from the parser's per-stage attempt
98
+ // counts; null when any stage record lacks them.
99
+ function stageAttempts(stages ) {
100
+ let failed = 0, retried = 0;
101
+ for (const s of stages) {
102
+ if (!finite(s.stageAttempts) || !finite(s.failedStageAttempts)) return null;
103
+ failed += s.failedStageAttempts;
104
+ if (s.stageAttempts > 1) retried++;
105
+ }
106
+ return { failed, retried };
107
+ }
108
+
109
+ // Metrics of a set of stages, every attempt included; a row folds the stages sharing one
110
+ // fingerprint. Skew describes the latest attempt's task durations.
111
+ function stageMetrics(stages , minTasksForP95 ) {
112
+ const attempts = withEarlierAttempts(stages);
113
+ // Task-derived figures are null for stages that never finished (no task records).
114
+ // Superseded attempts carry work but no task count of their own, so they stay in once any
115
+ // attempt finished a task.
116
+ const finished = attempts.some((s) => (s.taskCount ?? 0) > 0) ? attempts : [];
117
+ const tasks = sumOf(attempts, (s) => s.taskCount);
118
+ const fromTasks = (pick ) => (finished.length > 0 ? sumOf(finished, pick) : null);
119
+ const durations = attempts.filter((s) => finite(s.submittedAt) && finite(s.completedAt) && (s.completedAt ) >= (s.submittedAt ));
120
+ return {
121
+ durationMs: durations.length > 0 ? sumOf(durations, (s) => (s.completedAt ) - (s.submittedAt )) : null,
122
+ executorCpuTimeMs: totalExecutorCpuMs(finished),
123
+ executorRunTimeMs: fromTasks((s) => s.executorRunTime),
124
+ gcTimeMs: fromTasks((s) => s.jvmGCTime),
125
+ memorySpillBytes: fromTasks((s) => s.memoryBytesSpilled),
126
+ diskSpillBytes: fromTasks((s) => s.diskBytesSpilled),
127
+ shuffleReadBytes: fromTasks((s) => s.shuffleReadBytes),
128
+ shuffleWriteBytes: fromTasks((s) => s.shuffleWriteBytes),
129
+ inputBytes: fromTasks((s) => s.inputBytes),
130
+ outputBytes: fromTasks((s) => s.outputBytes),
131
+ outputRows: sumOf(finished, (s) => s.outputRecords),
132
+ peakExecutionMemoryBytes: peakExecutionMemory(finished),
133
+ taskCount: tasks != null && tasks > 0 ? tasks : null,
134
+ failedTasks: fromTasks((s) => s.failedTasks),
135
+ retriedTasks: fromTasks((s) => (s.wastedAttempts ) ?? 0),
136
+ skew: maxSkew(stages.filter((s) => (s.taskCount ?? 0) > 0), minTasksForP95),
137
+ };
138
+ }
139
+
140
+ /** The run's metrics block. `thresholds` only moves the skew ratio's P95 cutoff, as for the
141
+ * --max-skew budget. */
142
+ export function computeRunMetrics(appModel , thresholds ) {
143
+ const stageList = [...appModel.stages.values()];
144
+ const minTasksForP95 = effectiveThresholds(ENTRY_BY_TYPE.get('skew') , thresholds).minTasksForP95 ;
145
+ const all = stageMetrics(stageList, minTasksForP95);
146
+ const python = stageList.filter((s) => isPythonStage(s, appModel.sql));
147
+ const pythonRunTimeMs = sumOf(withEarlierAttempts(python), (s) => s.executorRunTime);
148
+
149
+ const byFingerprint = new Map ();
150
+ for (const s of stageList) {
151
+ const key = stageIdentity(s, appModel);
152
+ const group = byFingerprint.get(key);
153
+ if (group) group.push(s); else byFingerprint.set(key, [s]);
154
+ }
155
+ const stages = {};
156
+ for (const [key, group] of byFingerprint) {
157
+ const attempts = stageAttempts(group);
158
+ stages[key] = {
159
+ stageIds: group.map((s) => s.id).sort((a, b) => a - b),
160
+ failed: attempts ? attempts.failed > 0 : null,
161
+ retried: attempts ? attempts.retried > 0 : null,
162
+ python: group.some((s) => isPythonStage(s, appModel.sql)),
163
+ ...stageMetrics(group, minTasksForP95),
164
+ };
165
+ }
166
+
167
+ const attempts = stageAttempts(stageList);
168
+ return {
169
+ schemaVersion: METRICS_SCHEMA_VERSION,
170
+ runComplete: appModel.app?.endTime != null,
171
+ time: {
172
+ wallClockMs: hasCompleteApplicationInterval(appModel.app) ? computeWallClock(appModel.app, appModel.stages).total : null,
173
+ executorCpuTimeMs: all.executorCpuTimeMs,
174
+ executorRunTimeMs: all.executorRunTimeMs,
175
+ gcTimeMs: all.gcTimeMs,
176
+ },
177
+ data: {
178
+ memorySpillBytes: all.memorySpillBytes, diskSpillBytes: all.diskSpillBytes,
179
+ shuffleReadBytes: all.shuffleReadBytes, shuffleWriteBytes: all.shuffleWriteBytes,
180
+ inputBytes: all.inputBytes, outputBytes: all.outputBytes, outputRows: all.outputRows,
181
+ peakExecutionMemoryBytes: all.peakExecutionMemoryBytes,
182
+ },
183
+ shape: {
184
+ taskCount: all.taskCount,
185
+ stageCount: stageList.length,
186
+ failedStageAttempts: attempts?.failed ?? null,
187
+ retriedStages: attempts?.retried ?? null,
188
+ failedTasks: all.failedTasks,
189
+ retriedTasks: all.retriedTasks,
190
+ maxSkew: all.skew,
191
+ },
192
+ allocation: computeAllocation(appModel),
193
+ python: {
194
+ shareOfTaskRunTime: all.executorRunTimeMs != null && all.executorRunTimeMs > 0 ? (pythonRunTimeMs ?? 0) / all.executorRunTimeMs : null,
195
+ },
196
+ stages,
197
+ };
198
+ }