sparkforensics-server 0.6.0 → 0.8.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 (410) hide show
  1. package/index.js +16 -7
  2. package/package.json +1 -1
  3. package/public/assets/AutoscalingChurn-DenWKTv-.js +1 -0
  4. package/public/assets/CacheUtilization-DLz7NdjV.js +1 -0
  5. package/public/assets/CachingOpportunity-zpQIqhzm.js +1 -0
  6. package/public/assets/ChartTheme-DAiARbLP.js +8 -0
  7. package/public/assets/ColdStart-C-9HjxWl.js +1 -0
  8. package/public/assets/ConfigAudit-D0V9tcrJ.js +1 -0
  9. package/public/assets/CoreUsageArea-yeerrljd.js +1 -0
  10. package/public/assets/Dashboard-DkUPrHKV.js +1 -0
  11. package/public/assets/DisclosureContext-DK1_iz6J.js +1 -0
  12. package/public/assets/DuplicatePlanSubtree-xDNDbCd3.js +1 -0
  13. package/public/assets/DurationHistogram-B6jsE0Ov.js +1 -0
  14. package/public/assets/EmptyState-C1uL40VU.js +1 -0
  15. package/public/assets/ExecutorUtilization-DXDZKmSc.js +1 -0
  16. package/public/assets/GcPressure-MRf7I4IG.js +1 -0
  17. package/public/assets/ImpactEstimate-Dm3xWbzV.js +1 -0
  18. package/public/assets/IncompleteRun-CaLOhMtf.js +1 -0
  19. package/public/assets/JobFailures-CzmzlvhO.js +1 -0
  20. package/public/assets/MemoryUtilization-AWUMUjCH.js +1 -0
  21. package/public/assets/NestedLoopJoin-C9sNr6Ss.js +1 -0
  22. package/public/assets/OverBroadcast-BOmRE6ca.js +1 -0
  23. package/public/assets/PartitionHint-BL2EmJfE.js +1 -0
  24. package/public/assets/PartitionSizing-BzBySvUq.js +1 -0
  25. package/public/assets/PlanExplorer-DdiQRcJU.js +1 -0
  26. package/public/assets/PlanGraphRoute-CIH9gBuR.js +1 -0
  27. package/public/assets/PlanView-B1hxzFRs.js +2 -0
  28. package/public/assets/PythonUdf-DvgCCueD.js +1 -0
  29. package/public/assets/RetryWaste-uXXu_CyM.js +1 -0
  30. package/public/assets/RowPagination-C-LzWkI5.js +1 -0
  31. package/public/assets/RowStatusCluster-Dbtmte7l.js +1 -0
  32. package/public/assets/ShuffleIO-nALwzC1_.js +1 -0
  33. package/public/assets/Skew-CHMEKgvk.js +1 -0
  34. package/public/assets/SlowHost-yyvjpHk6.js +1 -0
  35. package/public/assets/SmallFiles-y6odJ97n.js +1 -0
  36. package/public/assets/SpeculationWaste-C-SD2LOt.js +1 -0
  37. package/public/assets/Spill-DHTMmOup.js +1 -0
  38. package/public/assets/StageFailed-CQ3DxC9R.js +1 -0
  39. package/public/assets/StageFindingGroup-BgYLyj5q.js +1 -0
  40. package/public/assets/StageHeader-CIETkjVZ.js +1 -0
  41. package/public/assets/StagePill--P7PDd3s.js +1 -0
  42. package/public/assets/StageShape-BIv8vx_o.js +1 -0
  43. package/public/assets/StageSlowness-MMqzERxN.js +1 -0
  44. package/public/assets/Straggler-BgrY7AyW.js +1 -0
  45. package/public/assets/TaskFailures-BHHyKYTs.js +1 -0
  46. package/public/assets/TinyTask-DDScNhCl.js +1 -0
  47. package/public/assets/UnderBroadcast-CGIC04TI.js +1 -0
  48. package/public/assets/WidgetLeadSummary-CXFZP00C.js +1 -0
  49. package/public/assets/button-DbpTz2yk.js +1 -0
  50. package/public/assets/charts-vendor-CtTxwTeu.js +59 -0
  51. package/public/assets/composite-C9Nu2EI9.js +1 -0
  52. package/public/assets/d3-vendor-DNlD2-cz.js +1 -0
  53. package/public/assets/detector-registry-Bp8hrLua.js +2 -0
  54. package/public/assets/downsample-WmNM1qc9.js +1 -0
  55. package/public/assets/finding-anchor-Dr40LkS9.js +1 -0
  56. package/public/assets/findings-of-type-BL2u3RGZ.js +1 -0
  57. package/public/assets/impact-sort-mC5ISgX8.js +1 -0
  58. package/public/assets/inbox-KPU1Gppm.js +1 -0
  59. package/public/assets/index-CaRVPvAT.css +2 -0
  60. package/public/assets/index-fDiwfuoO.js +14 -0
  61. package/public/assets/inertValue-L9VxV0VE.js +1 -0
  62. package/public/assets/layout.worker-CLcBHxVp.js +3 -0
  63. package/public/assets/{parser-worker-CA5RxqKc.js → parser-worker-CRIOPMOW.js} +8 -8
  64. package/public/assets/plan-graph-vendor-Ce6d4HU6.js +7 -0
  65. package/public/assets/react-vendor-C6KcUD36.js +9 -0
  66. package/public/assets/select-BHqOqun5.js +1 -0
  67. package/public/assets/toggle-DPeN6aNa.js +1 -0
  68. package/public/assets/toggle-group-Kl3uyHSr.js +1 -0
  69. package/public/assets/usePagedRows-BafD6bEN.js +1 -0
  70. package/public/assets/useRecentFiles-DzJZAoOT.js +3 -0
  71. package/public/assets/useRegisterFieldControl-pQv-ZDoU.js +1 -0
  72. package/public/assets/useScrollLock-BCxn_dMp.js +1 -0
  73. package/public/assets/useSortMode-DjrAN-z4.js +1 -0
  74. package/public/docs/404.html +3 -3
  75. package/public/docs/assets/{app.CnWL6bKV.js → app.1gvzd5Zb.js} +1 -1
  76. package/public/docs/assets/chunks/@localSearchIndexroot.DS2TQP5_.js +1 -0
  77. package/public/docs/assets/chunks/{VPLocalSearchBox.C0IBKeRQ.js → VPLocalSearchBox.CNxT3s0I.js} +1 -1
  78. package/public/docs/assets/chunks/{theme.BH-2HuRf.js → theme.C5lLqUv7.js} +2 -2
  79. package/public/docs/assets/contributor-guide_architecture_board-widgets.md.CqKM_jEG.js +1 -0
  80. package/public/docs/assets/contributor-guide_architecture_board-widgets.md.CqKM_jEG.lean.js +1 -0
  81. package/public/docs/assets/{contributor-guide_architecture_board-widgets_caching-and-memory.md.CQgn1t_M.js → contributor-guide_architecture_board-widgets_caching-and-memory.md.BYDo3ClR.js} +1 -1
  82. package/public/docs/assets/contributor-guide_architecture_board-widgets_plan-advisor.md.ajUAVdbc.js +1 -0
  83. package/public/docs/assets/{contributor-guide_architecture_board-widgets_visual-system-and-templating.md.wulOueeB.js → contributor-guide_architecture_board-widgets_visual-system-and-templating.md.WqY74T0T.js} +1 -1
  84. package/public/docs/assets/contributor-guide_architecture_board-widgets_visual-system-and-templating.md.WqY74T0T.lean.js +1 -0
  85. package/public/docs/assets/contributor-guide_architecture_detector-contract.md.CHHgywoP.js +1 -0
  86. package/public/docs/assets/{contributor-guide_architecture_detector-contract.md.N4hhenrA.lean.js → contributor-guide_architecture_detector-contract.md.CHHgywoP.lean.js} +1 -1
  87. package/public/docs/assets/contributor-guide_architecture_detector-contract_plan-attribution.md.DIz3_oz9.js +1 -0
  88. package/public/docs/assets/contributor-guide_architecture_detector-contract_plan-attribution.md.DIz3_oz9.lean.js +1 -0
  89. package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.WH4tRHPL.js +1 -0
  90. package/public/docs/assets/{contributor-guide_architecture_detector-contract_threshold-tables.md.Dp3c40rx.lean.js → contributor-guide_architecture_detector-contract_threshold-tables.md.WH4tRHPL.lean.js} +1 -1
  91. package/public/docs/assets/{contributor-guide_architecture_drill-down.md.CqZPrdo9.js → contributor-guide_architecture_drill-down.md.yB-Hz2XT.js} +1 -1
  92. package/public/docs/assets/{contributor-guide_architecture_drill-down.md.CqZPrdo9.lean.js → contributor-guide_architecture_drill-down.md.yB-Hz2XT.lean.js} +1 -1
  93. package/public/docs/assets/contributor-guide_architecture_drill-down_plan-graph.md.BQZE04sw.js +1 -0
  94. package/public/docs/assets/{contributor-guide_architecture_drill-down_plan-graph.md.DwOozIEs.lean.js → contributor-guide_architecture_drill-down_plan-graph.md.BQZE04sw.lean.js} +1 -1
  95. package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.K71YB-Rr.js +1 -0
  96. package/public/docs/assets/{contributor-guide_architecture_impact-estimation.md.DfvbqEUu.lean.js → contributor-guide_architecture_impact-estimation.md.K71YB-Rr.lean.js} +1 -1
  97. package/public/docs/assets/contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.DLCAffwn.js +1 -0
  98. package/public/docs/assets/{contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.Cwm1jHmn.lean.js → contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.DLCAffwn.lean.js} +1 -1
  99. package/public/docs/assets/contributor-guide_architecture_impact-estimation_stage-union-rollup.md.JFrzDMwo.js +1 -0
  100. package/public/docs/assets/contributor-guide_architecture_impact-estimation_stage-union-rollup.md.JFrzDMwo.lean.js +1 -0
  101. package/public/docs/assets/contributor-guide_architecture_overview.md.nxkpOhgf.js +1 -0
  102. package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.U-9KMDFK.js +1 -0
  103. package/public/docs/assets/{contributor-guide_architecture_state-and-history_run-comparison-internals.md.CZA0JLtV.lean.js → contributor-guide_architecture_state-and-history_run-comparison-internals.md.U-9KMDFK.lean.js} +1 -1
  104. package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.BrSz8Ezh.js +1 -0
  105. package/public/docs/assets/contributor-guide_architecture_widget-rendering_findings-tab.md.B7rjSF_h.js +1 -0
  106. package/public/docs/assets/contributor-guide_architecture_widget-rendering_findings-tab.md.B7rjSF_h.lean.js +1 -0
  107. package/public/docs/assets/{contributor-guide_architecture_widget-rendering_list-sort-mode.md.BuHKgH0C.js → contributor-guide_architecture_widget-rendering_list-sort-mode.md.37hcZJ7q.js} +1 -1
  108. package/public/docs/assets/{contributor-guide_architecture_widget-rendering_row-expand-contract.md.ZT89u9f1.js → contributor-guide_architecture_widget-rendering_row-expand-contract.md.mK_VmAWk.js} +1 -1
  109. package/public/docs/assets/{contributor-guide_architecture_worker-protocol.md.BAnhi2-d.js → contributor-guide_architecture_worker-protocol.md.Br6OE7RW.js} +1 -1
  110. package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.Br6OE7RW.lean.js +1 -0
  111. package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.IVyaXrGe.js +1 -0
  112. package/public/docs/assets/{contributor-guide_architecture_worker-protocol_cli-and-mcp.md.BX1XnFbq.lean.js → contributor-guide_architecture_worker-protocol_cli-and-mcp.md.IVyaXrGe.lean.js} +1 -1
  113. package/public/docs/assets/{contributor-guide_architecture_worker-protocol_core-and-event-validation.md.DSVFPpWb.js → contributor-guide_architecture_worker-protocol_core-and-event-validation.md.DVSXn-bb.js} +1 -1
  114. package/public/docs/assets/{contributor-guide_architecture_worker-protocol_evidence-availability.md.CjimIDBY.js → contributor-guide_architecture_worker-protocol_evidence-availability.md.D3S5cyOW.js} +2 -2
  115. package/public/docs/assets/{contributor-guide_architecture_worker-protocol_evidence-availability.md.CjimIDBY.lean.js → contributor-guide_architecture_worker-protocol_evidence-availability.md.D3S5cyOW.lean.js} +1 -1
  116. package/public/docs/assets/contributor-guide_architecture_worker-protocol_evidence-report.md.CtmCU57I.js +1 -0
  117. package/public/docs/assets/{contributor-guide_architecture_worker-protocol_evidence-report.md.CiQl1MLb.lean.js → contributor-guide_architecture_worker-protocol_evidence-report.md.CtmCU57I.lean.js} +1 -1
  118. package/public/docs/assets/contributor-guide_contributing.md.DBW4drtZ.js +1 -0
  119. package/public/docs/assets/contributor-guide_development-setup.md.B21ZTDhn.js +15 -0
  120. package/public/docs/assets/contributor-guide_development-setup.md.B21ZTDhn.lean.js +1 -0
  121. package/public/docs/assets/contributor-guide_testing.md.DpkozNoG.js +4 -0
  122. package/public/docs/assets/{contributor-guide_testing.md.By6XGR9a.lean.js → contributor-guide_testing.md.DpkozNoG.lean.js} +1 -1
  123. package/public/docs/assets/{index.md.CHJVslga.js → index.md.lMm7SUMz.js} +1 -1
  124. package/public/docs/assets/{index.md.CHJVslga.lean.js → index.md.lMm7SUMz.lean.js} +1 -1
  125. package/public/docs/assets/{style.CyK9Ss73.css → style.Cfxqlj0y.css} +1 -1
  126. package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.C0pcQoNc.js +29 -0
  127. package/public/docs/assets/{user-guide_getting-started_ci-and-automation.md.BEepbDDL.lean.js → user-guide_getting-started_ci-and-automation.md.C0pcQoNc.lean.js} +1 -1
  128. package/public/docs/assets/user-guide_getting-started_reading-the-dashboard.md.CGaxPX_1.js +1 -0
  129. package/public/docs/assets/{user-guide_getting-started_tuning-thresholds.md.B_MyjyZm.js → user-guide_getting-started_tuning-thresholds.md.BGVE-wrc.js} +2 -2
  130. package/public/docs/assets/{user-guide_getting-started_tuning-thresholds.md.B_MyjyZm.lean.js → user-guide_getting-started_tuning-thresholds.md.BGVE-wrc.lean.js} +1 -1
  131. package/public/docs/assets/{user-guide_mcp-tools.md.C65SjWUL.js → user-guide_mcp-tools.md.49r3L2YE.js} +1 -1
  132. package/public/docs/assets/{user-guide_mcp-tools_compare-runs.md.Cc_gLoix.js → user-guide_mcp-tools_compare-runs.md.BQ_kIptC.js} +11 -6
  133. package/public/docs/assets/user-guide_mcp-tools_compare-runs.md.BQ_kIptC.lean.js +1 -0
  134. package/public/docs/assets/user-guide_mcp-tools_diagnose-run.md.azCZXkH8.js +44 -0
  135. package/public/docs/assets/{user-guide_mcp-tools_diagnose-run.md.BP1vnHA5.lean.js → user-guide_mcp-tools_diagnose-run.md.azCZXkH8.lean.js} +1 -1
  136. package/public/docs/assets/{user-guide_mcp-tools_evaluate-budgets.md.BB0nDY3Z.js → user-guide_mcp-tools_evaluate-budgets.md.dpQpRQ7z.js} +2 -2
  137. package/public/docs/assets/{user-guide_mcp-tools_evaluate-budgets.md.BB0nDY3Z.lean.js → user-guide_mcp-tools_evaluate-budgets.md.dpQpRQ7z.lean.js} +1 -1
  138. package/public/docs/assets/{user-guide_mcp-tools_get-finding-evidence.md.rKyu4wts.js → user-guide_mcp-tools_get-finding-evidence.md.CH6FkNLX.js} +2 -1
  139. package/public/docs/assets/{user-guide_mcp-tools_get-finding-evidence.md.rKyu4wts.lean.js → user-guide_mcp-tools_get-finding-evidence.md.CH6FkNLX.lean.js} +1 -1
  140. package/public/docs/assets/user-guide_run-comparison.md.B_LOkdBf.js +1 -0
  141. package/public/docs/assets/user-guide_run-comparison.md.B_LOkdBf.lean.js +1 -0
  142. package/public/docs/assets/user-guide_understanding-findings.md.C4riKCKl.js +1 -0
  143. package/public/docs/assets/user-guide_understanding-findings.md.C4riKCKl.lean.js +1 -0
  144. package/public/docs/contributor-guide/architecture/board-widgets/caching-and-memory.html +6 -6
  145. package/public/docs/contributor-guide/architecture/board-widgets/plan-advisor.html +6 -6
  146. package/public/docs/contributor-guide/architecture/board-widgets/visual-system-and-templating.html +6 -6
  147. package/public/docs/contributor-guide/architecture/board-widgets.html +6 -6
  148. package/public/docs/contributor-guide/architecture/detector-contract/plan-attribution.html +6 -6
  149. package/public/docs/contributor-guide/architecture/detector-contract/threshold-tables.html +6 -6
  150. package/public/docs/contributor-guide/architecture/detector-contract.html +6 -6
  151. package/public/docs/contributor-guide/architecture/drill-down/plan-graph.html +6 -6
  152. package/public/docs/contributor-guide/architecture/drill-down.html +6 -6
  153. package/public/docs/contributor-guide/architecture/impact-estimation/caveats-tuning-and-coverage.html +6 -6
  154. package/public/docs/contributor-guide/architecture/impact-estimation/stage-union-rollup.html +6 -6
  155. package/public/docs/contributor-guide/architecture/impact-estimation.html +6 -6
  156. package/public/docs/contributor-guide/architecture/index.html +5 -5
  157. package/public/docs/contributor-guide/architecture/overview.html +6 -6
  158. package/public/docs/contributor-guide/architecture/state-and-history/history-server-intake.html +5 -5
  159. package/public/docs/contributor-guide/architecture/state-and-history/run-comparison-internals.html +6 -6
  160. package/public/docs/contributor-guide/architecture/state-and-history.html +5 -5
  161. package/public/docs/contributor-guide/architecture/widget-rendering/findings-tab.html +6 -6
  162. package/public/docs/contributor-guide/architecture/widget-rendering/investigation-routing.html +5 -5
  163. package/public/docs/contributor-guide/architecture/widget-rendering/list-sort-mode.html +6 -6
  164. package/public/docs/contributor-guide/architecture/widget-rendering/reference-section.html +5 -5
  165. package/public/docs/contributor-guide/architecture/widget-rendering/row-expand-contract.html +6 -6
  166. package/public/docs/contributor-guide/architecture/widget-rendering.html +6 -6
  167. package/public/docs/contributor-guide/architecture/worker-protocol/cli-and-mcp.html +6 -6
  168. package/public/docs/contributor-guide/architecture/worker-protocol/core-and-event-validation.html +6 -6
  169. package/public/docs/contributor-guide/architecture/worker-protocol/evidence-availability.html +7 -7
  170. package/public/docs/contributor-guide/architecture/worker-protocol/evidence-report.html +6 -6
  171. package/public/docs/contributor-guide/architecture/worker-protocol.html +6 -6
  172. package/public/docs/contributor-guide/contributing.html +6 -6
  173. package/public/docs/contributor-guide/development-setup.html +8 -8
  174. package/public/docs/contributor-guide/testing.html +9 -6
  175. package/public/docs/hashmap.json +1 -1
  176. package/public/docs/index.html +6 -6
  177. package/public/docs/logo-dark.svg +1 -0
  178. package/public/docs/logo-light.svg +1 -0
  179. package/public/docs/tuning-reference/anti-patterns.html +5 -5
  180. package/public/docs/tuning-reference/aqe.html +5 -5
  181. package/public/docs/tuning-reference/bottleneck-broadcast-sizing.html +5 -5
  182. package/public/docs/tuning-reference/bottleneck-cold-start.html +5 -5
  183. package/public/docs/tuning-reference/bottleneck-duplicate-plan-subtree.html +5 -5
  184. package/public/docs/tuning-reference/bottleneck-failures.html +5 -5
  185. package/public/docs/tuning-reference/bottleneck-gc.html +5 -5
  186. package/public/docs/tuning-reference/bottleneck-job-failure-rate.html +5 -5
  187. package/public/docs/tuning-reference/bottleneck-memory-utilization.html +5 -5
  188. package/public/docs/tuning-reference/bottleneck-retry-waste.html +5 -5
  189. package/public/docs/tuning-reference/bottleneck-shuffle.html +5 -5
  190. package/public/docs/tuning-reference/bottleneck-skew.html +5 -5
  191. package/public/docs/tuning-reference/bottleneck-slow-host.html +5 -5
  192. package/public/docs/tuning-reference/bottleneck-small-files.html +5 -5
  193. package/public/docs/tuning-reference/bottleneck-spill.html +5 -5
  194. package/public/docs/tuning-reference/bottleneck-straggler.html +5 -5
  195. package/public/docs/tuning-reference/bottleneck-tiny-tasks.html +5 -5
  196. package/public/docs/tuning-reference/bottleneck-utilization.html +5 -5
  197. package/public/docs/tuning-reference/caching.html +5 -5
  198. package/public/docs/tuning-reference/cluster-config.html +5 -5
  199. package/public/docs/tuning-reference/config.html +5 -5
  200. package/public/docs/tuning-reference/data-formats.html +5 -5
  201. package/public/docs/tuning-reference/index.html +5 -5
  202. package/public/docs/tuning-reference/intro.html +5 -5
  203. package/public/docs/tuning-reference/joins.html +5 -5
  204. package/public/docs/tuning-reference/memory-model.html +5 -5
  205. package/public/docs/tuning-reference/metrics.html +5 -5
  206. package/public/docs/tuning-reference/partitioning.html +5 -5
  207. package/public/docs/tuning-reference/pyspark.html +5 -5
  208. package/public/docs/tuning-reference/shuffle.html +5 -5
  209. package/public/docs/tuning-reference/spark-architecture.html +5 -5
  210. package/public/docs/tuning-reference/table-formats.html +5 -5
  211. package/public/docs/user-guide/alternative-log-retrieval/managed-platforms.html +5 -5
  212. package/public/docs/user-guide/alternative-log-retrieval/ssh-bastion.html +5 -5
  213. package/public/docs/user-guide/alternative-log-retrieval.html +5 -5
  214. package/public/docs/user-guide/getting-started/ci-and-automation.html +16 -11
  215. package/public/docs/user-guide/getting-started/reading-the-dashboard.html +6 -6
  216. package/public/docs/user-guide/getting-started/regression-metric-keys.html +5 -5
  217. package/public/docs/user-guide/getting-started/tuning-thresholds.html +7 -7
  218. package/public/docs/user-guide/getting-started.html +5 -5
  219. package/public/docs/user-guide/mcp-tools/compare-runs.html +16 -11
  220. package/public/docs/user-guide/mcp-tools/diagnose-run.html +28 -7
  221. package/public/docs/user-guide/mcp-tools/evaluate-budgets.html +6 -6
  222. package/public/docs/user-guide/mcp-tools/get-finding-documentation.html +5 -5
  223. package/public/docs/user-guide/mcp-tools/get-finding-evidence.html +7 -6
  224. package/public/docs/user-guide/mcp-tools/get-reference-doc.html +5 -5
  225. package/public/docs/user-guide/mcp-tools/get-run-summary.html +5 -5
  226. package/public/docs/user-guide/mcp-tools/list-runs.html +5 -5
  227. package/public/docs/user-guide/mcp-tools.html +6 -6
  228. package/public/docs/user-guide/run-comparison/how-stages-are-matched.html +5 -5
  229. package/public/docs/user-guide/run-comparison.html +6 -6
  230. package/public/docs/user-guide/understanding-findings.html +6 -6
  231. package/public/export-template.html +45 -45
  232. package/public/index.html +8 -7
  233. package/public/sample-runs/sample-run.ndjson.gz +0 -0
  234. package/vendor-core/allocation.js +25 -23
  235. package/vendor-core/analyzer.js +85 -58
  236. package/vendor-core/api-merge-writes.js +2 -0
  237. package/vendor-core/aqe-skew.js +347 -0
  238. package/vendor-core/cli/budgets.js +6 -4
  239. package/vendor-core/cli/collect-run-pair.js +89 -0
  240. package/vendor-core/cli/collect-run-worker.js +16 -0
  241. package/vendor-core/cli/collect-run.js +1 -2
  242. package/vendor-core/cli/native-zstd.js +12 -16
  243. package/vendor-core/comparability.js +234 -0
  244. package/vendor-core/comparison-output.js +15 -7
  245. package/vendor-core/comparison-verdict.js +105 -25
  246. package/vendor-core/core-source-hash.txt +1 -1
  247. package/vendor-core/core-time-series.js +0 -1
  248. package/vendor-core/core-usage-locality.js +1 -1
  249. package/vendor-core/detectors.js +1007 -380
  250. package/vendor-core/docs-config.js +0 -4
  251. package/vendor-core/docs-content/detection/cache.md +2 -1
  252. package/vendor-core/docs-content/detection/cfg.md +19 -11
  253. package/vendor-core/docs-content/detection/cold.md +2 -1
  254. package/vendor-core/docs-content/detection/gc.md +7 -3
  255. package/vendor-core/docs-content/detection/host.md +11 -1
  256. package/vendor-core/docs-content/detection/mem.md +17 -6
  257. package/vendor-core/docs-content/detection/part.md +3 -1
  258. package/vendor-core/docs-content/detection/plan.md +55 -13
  259. package/vendor-core/docs-content/detection/shfl.md +11 -5
  260. package/vendor-core/docs-content/detection/skew.md +47 -1
  261. package/vendor-core/docs-content/detection/slow.md +4 -2
  262. package/vendor-core/docs-content/detection/spec.md +6 -1
  263. package/vendor-core/docs-content/detection/spill.md +6 -3
  264. package/vendor-core/docs-content/detection/strag.md +19 -6
  265. package/vendor-core/docs-content/detection/tiny.md +9 -3
  266. package/vendor-core/effective-conf.js +7 -10
  267. package/vendor-core/efficiency-model.js +2 -2
  268. package/vendor-core/etl-phases.js +1 -2
  269. package/vendor-core/event-handlers.js +175 -57
  270. package/vendor-core/event-schemas.js +21 -16
  271. package/vendor-core/evidence-availability.js +9 -2
  272. package/vendor-core/evidence-report.js +118 -35
  273. package/vendor-core/executor-peaks.js +41 -0
  274. package/vendor-core/export-data.js +5 -5
  275. package/vendor-core/finding-identity.js +78 -0
  276. package/vendor-core/finding-presentation.js +116 -33
  277. package/vendor-core/finding-types.js +88 -21
  278. package/vendor-core/format-utils.js +14 -15
  279. package/vendor-core/html-export.js +1 -1
  280. package/vendor-core/impact-band.js +2 -2
  281. package/vendor-core/impact-format.js +47 -19
  282. package/vendor-core/impact-model.js +26 -15
  283. package/vendor-core/job-groups.js +4 -4
  284. package/vendor-core/mcp-server-factory.js +48 -12
  285. package/vendor-core/mcp-tools.js +179 -24
  286. package/vendor-core/median.js +2 -2
  287. package/vendor-core/nested-loop-join.js +73 -0
  288. package/vendor-core/occupancy.js +1 -2
  289. package/vendor-core/parser-worker.js +1 -1
  290. package/vendor-core/plan-graph-model.js +14 -22
  291. package/vendor-core/plan-node-detail.js +2 -3
  292. package/vendor-core/plan-summary.js +24 -19
  293. package/vendor-core/plan-tree-walk.js +6 -8
  294. package/vendor-core/python-stage.js +12 -1
  295. package/vendor-core/recommendation-rollup.js +13 -12
  296. package/vendor-core/redact.js +52 -46
  297. package/vendor-core/run-aggregates.js +6 -8
  298. package/vendor-core/run-comparison.js +226 -66
  299. package/vendor-core/run-identity.js +12 -0
  300. package/vendor-core/run-interpretation.js +21 -0
  301. package/vendor-core/run-metrics.js +2 -2
  302. package/vendor-core/run-output.js +32 -10
  303. package/vendor-core/run-totals.js +12 -0
  304. package/vendor-core/run-verdict.js +76 -3
  305. package/vendor-core/scaling-sim.js +3 -4
  306. package/vendor-core/snappy-block.js +2 -2
  307. package/vendor-core/spark-conf.js +168 -0
  308. package/vendor-core/sql-stages.js +2 -2
  309. package/vendor-core/stage-alignment.js +2 -12
  310. package/vendor-core/stage-identity.js +58 -3
  311. package/vendor-core/stage-label.js +14 -0
  312. package/vendor-core/stage-plan-nodes.js +17 -3
  313. package/vendor-core/stage-quantiles.js +141 -9
  314. package/vendor-core/stage-structure.js +17 -3
  315. package/vendor-core/threshold-overrides.js +9 -3
  316. package/vendor-core/types.js +58 -21
  317. package/vendor-core/wasted-core-hours.js +0 -1
  318. package/vendor-core/write-targets.js +114 -14
  319. package/vendor-core/zstd-worker-client.js +1 -1
  320. package/vendor-core/zstd-worker.js +1 -1
  321. package/public/assets/AutoscalingChurn-CRA2Mb7F.js +0 -1
  322. package/public/assets/CacheUtilization-Bh9OY_V9.js +0 -1
  323. package/public/assets/CachingOpportunity-oCiQwmTJ.js +0 -1
  324. package/public/assets/ColdStart-BJrtjfmn.js +0 -1
  325. package/public/assets/ConfigAudit-DMvKxySP.js +0 -1
  326. package/public/assets/CoreUsageArea-REC2zsKw.js +0 -1
  327. package/public/assets/Dashboard-BHZ76JCu.js +0 -2
  328. package/public/assets/DisclosureContext-OO7X1HAx.js +0 -1
  329. package/public/assets/DuplicatePlanSubtree-BgG-L-cK.js +0 -1
  330. package/public/assets/ExecutorUtilization-D2KhSVbY.js +0 -1
  331. package/public/assets/GcPressure-D1BTE8qh.js +0 -1
  332. package/public/assets/IncompleteRun-DD40xUpy.js +0 -1
  333. package/public/assets/JobFailures-YwNzCF19.js +0 -1
  334. package/public/assets/MemoryUtilization-9b8ee_3_.js +0 -1
  335. package/public/assets/OverBroadcast-CfctS8a8.js +0 -1
  336. package/public/assets/PartitionHint-CUvbnnxy.js +0 -1
  337. package/public/assets/PartitionSizing-VG8kBgG5.js +0 -1
  338. package/public/assets/PlanExplorer-DmubTAOP.js +0 -1
  339. package/public/assets/PlanGraphRoute-DctPZjS0.js +0 -1
  340. package/public/assets/RetryWaste-CnpM8bB7.js +0 -1
  341. package/public/assets/ShuffleIO-D13vGmas.js +0 -1
  342. package/public/assets/Skew-DlagstYP.js +0 -1
  343. package/public/assets/SlowHost-cSkLRZ_e.js +0 -1
  344. package/public/assets/SmallFiles-B-kERMcq.js +0 -1
  345. package/public/assets/SpeculationWaste-DaysAzXO.js +0 -1
  346. package/public/assets/Spill-H44Gz4US.js +0 -1
  347. package/public/assets/StageFailed-DCfii9h_.js +0 -1
  348. package/public/assets/StageFindingGroup-BXJ0Mj5n.js +0 -1
  349. package/public/assets/StageHeader--oN2J-oD.js +0 -1
  350. package/public/assets/StageShape-IZ80CZiJ.js +0 -1
  351. package/public/assets/StageSlowness--BRIaBc1.js +0 -1
  352. package/public/assets/Straggler-Ctq0Df_o.js +0 -1
  353. package/public/assets/TaskFailures-Bks_gqhL.js +0 -1
  354. package/public/assets/TinyTask-BssfzGFB.js +0 -1
  355. package/public/assets/UnderBroadcast-DsScrxc-.js +0 -1
  356. package/public/assets/charts-vendor-0VMaLVLN.js +0 -59
  357. package/public/assets/finding-anchor-BYW1oz5R.js +0 -1
  358. package/public/assets/impact-sort-xzy0g-Vh.js +0 -1
  359. package/public/assets/index-DhpRGMZc.css +0 -2
  360. package/public/assets/index-DwqkFUFk.js +0 -14
  361. package/public/assets/plan-graph-vendor-BYl-AzN5.js +0 -7
  362. package/public/assets/react-vendor-DfpuPQZf.js +0 -9
  363. package/public/assets/select-C8edjYTq.js +0 -1
  364. package/public/assets/usePagedRows-CsuUFeYx.js +0 -1
  365. package/public/assets/useRecentFiles-BvkKo3JN.js +0 -11
  366. package/public/assets/useSortMode-joX71BZN.js +0 -1
  367. package/public/docs/assets/chunks/@localSearchIndexroot.tE5pz54z.js +0 -1
  368. package/public/docs/assets/contributor-guide_architecture_board-widgets.md.YQcj5EI4.js +0 -1
  369. package/public/docs/assets/contributor-guide_architecture_board-widgets.md.YQcj5EI4.lean.js +0 -1
  370. package/public/docs/assets/contributor-guide_architecture_board-widgets_plan-advisor.md.RlzvUmNm.js +0 -1
  371. package/public/docs/assets/contributor-guide_architecture_board-widgets_visual-system-and-templating.md.wulOueeB.lean.js +0 -1
  372. package/public/docs/assets/contributor-guide_architecture_detector-contract.md.N4hhenrA.js +0 -1
  373. package/public/docs/assets/contributor-guide_architecture_detector-contract_plan-attribution.md.yNex4bys.js +0 -1
  374. package/public/docs/assets/contributor-guide_architecture_detector-contract_plan-attribution.md.yNex4bys.lean.js +0 -1
  375. package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.Dp3c40rx.js +0 -1
  376. package/public/docs/assets/contributor-guide_architecture_drill-down_plan-graph.md.DwOozIEs.js +0 -1
  377. package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.DfvbqEUu.js +0 -1
  378. package/public/docs/assets/contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.Cwm1jHmn.js +0 -1
  379. package/public/docs/assets/contributor-guide_architecture_impact-estimation_stage-union-rollup.md.DdCGtAGu.js +0 -1
  380. package/public/docs/assets/contributor-guide_architecture_impact-estimation_stage-union-rollup.md.DdCGtAGu.lean.js +0 -1
  381. package/public/docs/assets/contributor-guide_architecture_overview.md.C1sSvm9i.js +0 -1
  382. package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.CZA0JLtV.js +0 -1
  383. package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.BOZ1azXG.js +0 -1
  384. package/public/docs/assets/contributor-guide_architecture_widget-rendering_findings-tab.md.BQXycK6G.js +0 -1
  385. package/public/docs/assets/contributor-guide_architecture_widget-rendering_findings-tab.md.BQXycK6G.lean.js +0 -1
  386. package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.BAnhi2-d.lean.js +0 -1
  387. package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.BX1XnFbq.js +0 -1
  388. package/public/docs/assets/contributor-guide_architecture_worker-protocol_evidence-report.md.CiQl1MLb.js +0 -1
  389. package/public/docs/assets/contributor-guide_contributing.md.CQMvGwFt.js +0 -1
  390. package/public/docs/assets/contributor-guide_development-setup.md.DQdzera7.js +0 -15
  391. package/public/docs/assets/contributor-guide_development-setup.md.DQdzera7.lean.js +0 -1
  392. package/public/docs/assets/contributor-guide_testing.md.By6XGR9a.js +0 -1
  393. package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.BEepbDDL.js +0 -24
  394. package/public/docs/assets/user-guide_getting-started_reading-the-dashboard.md.Obf3W3__.js +0 -1
  395. package/public/docs/assets/user-guide_mcp-tools_compare-runs.md.Cc_gLoix.lean.js +0 -1
  396. package/public/docs/assets/user-guide_mcp-tools_diagnose-run.md.BP1vnHA5.js +0 -23
  397. package/public/docs/assets/user-guide_run-comparison.md.BUALkohi.js +0 -1
  398. package/public/docs/assets/user-guide_run-comparison.md.BUALkohi.lean.js +0 -1
  399. package/public/docs/assets/user-guide_understanding-findings.md.3C7KCEZO.js +0 -1
  400. package/public/docs/assets/user-guide_understanding-findings.md.3C7KCEZO.lean.js +0 -1
  401. /package/public/docs/assets/{contributor-guide_architecture_board-widgets_caching-and-memory.md.CQgn1t_M.lean.js → contributor-guide_architecture_board-widgets_caching-and-memory.md.BYDo3ClR.lean.js} +0 -0
  402. /package/public/docs/assets/{contributor-guide_architecture_board-widgets_plan-advisor.md.RlzvUmNm.lean.js → contributor-guide_architecture_board-widgets_plan-advisor.md.ajUAVdbc.lean.js} +0 -0
  403. /package/public/docs/assets/{contributor-guide_architecture_overview.md.C1sSvm9i.lean.js → contributor-guide_architecture_overview.md.nxkpOhgf.lean.js} +0 -0
  404. /package/public/docs/assets/{contributor-guide_architecture_widget-rendering.md.BOZ1azXG.lean.js → contributor-guide_architecture_widget-rendering.md.BrSz8Ezh.lean.js} +0 -0
  405. /package/public/docs/assets/{contributor-guide_architecture_widget-rendering_list-sort-mode.md.BuHKgH0C.lean.js → contributor-guide_architecture_widget-rendering_list-sort-mode.md.37hcZJ7q.lean.js} +0 -0
  406. /package/public/docs/assets/{contributor-guide_architecture_widget-rendering_row-expand-contract.md.ZT89u9f1.lean.js → contributor-guide_architecture_widget-rendering_row-expand-contract.md.mK_VmAWk.lean.js} +0 -0
  407. /package/public/docs/assets/{contributor-guide_architecture_worker-protocol_core-and-event-validation.md.DSVFPpWb.lean.js → contributor-guide_architecture_worker-protocol_core-and-event-validation.md.DVSXn-bb.lean.js} +0 -0
  408. /package/public/docs/assets/{contributor-guide_contributing.md.CQMvGwFt.lean.js → contributor-guide_contributing.md.DBW4drtZ.lean.js} +0 -0
  409. /package/public/docs/assets/{user-guide_getting-started_reading-the-dashboard.md.Obf3W3__.lean.js → user-guide_getting-started_reading-the-dashboard.md.CGaxPX_1.lean.js} +0 -0
  410. /package/public/docs/assets/{user-guide_mcp-tools.md.C65SjWUL.lean.js → user-guide_mcp-tools.md.49r3L2YE.lean.js} +0 -0
@@ -0,0 +1,78 @@
1
+
2
+
3
+ /** Maps a finding's location in one run to a label shared by both runs. A stage or execution that
4
+ * pairs with one in the other run gets the same label on both sides; one that pairs with nothing
5
+ * gets a label no other run's location can equal. */
6
+
7
+
8
+
9
+
10
+
11
+ /** The finding's rule, or its type when the detector emits no rule. */
12
+ export function findingRule(finding ) {
13
+ return 'rule' in finding && typeof finding.rule === 'string' ? finding.rule : finding.type;
14
+ }
15
+
16
+ /** The finding's band, `unknown` when the impact estimate could not grade it. */
17
+ export function findingBand(finding ) {
18
+ return finding.impactBand ?? 'unknown';
19
+ }
20
+
21
+ // Location: stage, else SQL execution, else audited config property, else the whole run.
22
+ function findingLocation(finding , locator ) {
23
+ if (finding.stageId != null) return `stage:${locator.stage(finding.stageId)}`;
24
+ if ('executionId' in finding && typeof finding.executionId === 'number') return `execution:${locator.execution(finding.executionId)}`;
25
+ if (finding.type === 'configAudit') return `property:${finding.property}`;
26
+ return 'run';
27
+ }
28
+
29
+ /** The identity of a finding across two runs: its type, rule and location. Never its value or its
30
+ * impact band, which both move from run to run for the same problem (`findingId` hashes the value,
31
+ * so it matches only within one run). */
32
+ export function findingKey(finding , locator ) {
33
+ return `${finding.type}|${findingRule(finding)}|${findingLocation(finding, locator)}`;
34
+ }
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+ function groupByKey(findings , locator ) {
46
+ const groups = new Map ();
47
+ for (const finding of findings) {
48
+ const key = findingKey(finding, locator);
49
+ const group = groups.get(key);
50
+ if (group) group.push(finding); else groups.set(key, [finding]);
51
+ }
52
+ return groups;
53
+ }
54
+
55
+ /** Pairs two runs' findings by `findingKey`. A key several findings share (one detector firing per
56
+ * host on one run-level key, say) pairs one-to-one: same band first, then in catalog order, so only
57
+ * a count difference or a band no counterpart shares reads as a change. One diff feeds every
58
+ * comparison consumer: the verdict, the budgets and the dashboard. */
59
+ export function diffFindings(
60
+ baseline , candidate , baselineLocator , candidateLocator ,
61
+ ) {
62
+ const base = groupByKey(baseline, baselineLocator), cand = groupByKey(candidate, candidateLocator);
63
+ const diff = { introduced: [], resolved: [], changed: [] };
64
+ for (const key of new Set([...base.keys(), ...cand.keys()])) {
65
+ const unpairedBase = [...(base.get(key) ?? [])], unpairedCand = [...(cand.get(key) ?? [])];
66
+ for (let i = unpairedBase.length - 1; i >= 0; i--) {
67
+ const j = unpairedCand.findIndex((f) => findingBand(f) === findingBand(unpairedBase[i]));
68
+ if (j < 0) continue;
69
+ unpairedBase.splice(i, 1);
70
+ unpairedCand.splice(j, 1);
71
+ }
72
+ const paired = Math.min(unpairedBase.length, unpairedCand.length);
73
+ for (let i = 0; i < paired; i++) diff.changed.push({ baseline: unpairedBase[i], candidate: unpairedCand[i] });
74
+ diff.resolved.push(...unpairedBase.slice(paired));
75
+ diff.introduced.push(...unpairedCand.slice(paired));
76
+ }
77
+ return diff;
78
+ }
@@ -8,7 +8,7 @@
8
8
  // detectors.ts is type-only, so it is erased at build time. A detector's scope, order and emits
9
9
  // list stay on its DETECTORS entry; renderers get them through detectorInfoByType().
10
10
 
11
-
11
+
12
12
 
13
13
 
14
14
 
@@ -30,6 +30,12 @@
30
30
  /** A share threshold as captions and caveats state it: 0.005 -> "0.5%", never float noise like 7.000000000000001%. */
31
31
  export const shareLabel = (share ) => `${Math.round(share * 1e6) / 1e4}%`;
32
32
 
33
+ /** `n.toLocaleString('en-US')` for a whole number, without loading ICU (about 10 ms the first time it is
34
+ * used), which keeps the CLI's startup off that cost. Other values take the locale path. */
35
+ function groupThousands(n ) {
36
+ return Number.isSafeInteger(n) ? String(n).replace(/\B(?=(\d{3})+$)/g, ',') : n.toLocaleString('en-US');
37
+ }
38
+
33
39
  // Whether the detector found `key` already logged on for this run. Its switchFix (detectors.ts)
34
40
  // then worded the row's own text for that case and left the property out of the remediation, so
35
41
  // a generic line reads the same decision and never recommends a switch the row says is on.
@@ -38,6 +44,13 @@ function switchAlreadyOn(finding , key )
38
44
  return finding.remediation != null && !finding.remediation.some((r) => r.kind === 'conf' && r.key === key);
39
45
  }
40
46
 
47
+ // The broadcast threshold properties a broadcast-sizing row's remediation names: the adaptive one when
48
+ // it governed the query, else (and for a row with none) spark.sql.autoBroadcastJoinThreshold.
49
+ function broadcastThresholdKeyOf(f ) {
50
+ const keys = (f.remediation ?? []).filter((r) => r.kind === 'conf').map((r) => r.key);
51
+ return keys.length ? keys.join(' or ') : 'spark.sql.autoBroadcastJoinThreshold';
52
+ }
53
+
41
54
  // The properties an idle-capacity finding lowers once dynamic allocation is on, worded as its row
42
55
  // does: the executor cap, and the floor when the row's remediation lowers it too.
43
56
  function idleCapacityLowering(f ) {
@@ -45,28 +58,65 @@ function idleCapacityLowering(f ) {
45
58
  return ` by lowering spark.dynamicAllocation.maxExecutors${lowersFloor ? ' and spark.dynamicAllocation.minExecutors' : ''}`;
46
59
  }
47
60
 
61
+ // What a shuffle-reading stage's partition-count advice names, read from the row's remediation: the
62
+ // property ('property', also for a finding with no remediation), AQE's coalescing settings ('aqe',
63
+ // which leave spark.sql.shuffle.partitions out), or the stage's own repartition(n) ('code').
64
+ function partitionAdviceKind(f ) {
65
+ if (f.remediation == null || f.remediation.some((r) => r.kind === 'conf' && r.key === 'spark.sql.shuffle.partitions')) return 'property';
66
+ if (f.remediation.some((r) => r.kind === 'conf' && r.key.startsWith('spark.sql.adaptive.'))) return 'aqe';
67
+ return f.remediation.some((r) => r.kind === 'code') ? 'code' : 'property';
68
+ }
69
+
70
+ // The skew-join line per AQE case, with no instance data (numbers, config values).
71
+ const AQE_SKEW_GENERIC = {
72
+ split: 'AQE skew-join handling already split the skewed partitions, so the imbalance that remains is not join skew: look for a GC pause, a slow host or an expensive key.',
73
+ evenReads: 'The shuffle reads are even, so the slow tail is not partition-size skew and AQE has nothing to split: look for a GC pause, a slow host or an expensive key.',
74
+ belowThreshold: 'The largest partition is under what AQE treats as skewed: lower the skew threshold or factor for this query.',
75
+ planShape: 'An operator sits between the join and its shuffle, so AQE skew-join handling cannot apply: salt the key or repartition on a better key.',
76
+ userRepartition: 'An explicit repartition feeds the join, so AQE leaves its shuffle alone: drop the repartition, or salt the key.',
77
+ joinType: 'The join type does not let AQE split the skewed side: put the skewed table on a splittable side, or salt the key.',
78
+ extraShuffle: 'AQE skipped the split because it would add a shuffle: set spark.sql.adaptive.forceOptimizeSkewedJoin to true if that shuffle is cheaper than the tail, or salt the key.',
79
+ notSplit: 'AQE did not split the skewed partition and the log does not say why: salt the key or repartition on a better key.',
80
+ };
81
+ // Before Spark 3.3 there is no forceOptimizeSkewedJoin, so an extraShuffle row carries a code fix.
82
+ const AQE_SKEW_EXTRA_SHUFFLE_NO_FORCE = 'AQE skipped the split because it would add a shuffle: salt the key or repartition on a better key.';
83
+
48
84
  const SKEW_JOIN_KEY = 'spark.sql.adaptive.skewJoin.enabled';
49
85
  const SKEW_JOIN_ALREADY_ON = 'AQE skew-join handling is already on, so salt the key or repartition on a better key.';
50
86
  const SKEW_JOIN_AQE_OFF = 'AQE is off, so enable it (spark.sql.adaptive.enabled) for skew-join handling to apply; otherwise salt the key or repartition on a better key.';
51
87
 
52
88
  // The skew-join generic line, worded per the row's remediation: AQE logged off, switch already on, or neither.
53
- function skewJoinGeneric(f , unset ) {
89
+ function skewJoinGeneric(f , unset ) {
54
90
  if (f.origin === 'inputScan') return 'Uneven input files: compact small files or split large ones (lower spark.sql.files.maxPartitionBytes).';
55
91
  if (f.origin === 'other') return 'Work is uneven across tasks: salt the key or repartition on a better key.';
92
+ if (f.aqeSkew === 'extraShuffle' && !f.remediation?.some((r) => r.kind === 'conf')) return AQE_SKEW_EXTRA_SHUFFLE_NO_FORCE;
93
+ if (f.aqeSkew != null) return AQE_SKEW_GENERIC[f.aqeSkew];
56
94
  if (f.remediation?.some((r) => r.kind === 'conf' && r.key === 'spark.sql.adaptive.enabled')) return SKEW_JOIN_AQE_OFF;
57
95
  return switchAlreadyOn(f, SKEW_JOIN_KEY) ? SKEW_JOIN_ALREADY_ON : unset;
58
96
  }
97
+ /** One line on what a straggler finding's slow tasks lost their time to, for the widget row; null
98
+ * when the log gave no cause (the finding then only knows their duration). */
99
+ export function stragglerCauseSummary(f ) {
100
+ const share = f.causeSharePct != null ? ` (${f.causeSharePct}% of their extra time)` : '';
101
+ switch (f.cause) {
102
+ case 'data': return `The slow tasks read more data${share}`;
103
+ case 'gc': return `GC${share}`;
104
+ case 'fetchWait': return `Waiting on shuffle fetches${share}`;
105
+ case 'host': return `Tasks piled on ${f.host ?? 'one host'}${share}`;
106
+ case 'unexplained': return 'Not data volume, GC, fetch wait or one host';
107
+ default: return null;
108
+ }
109
+ }
110
+
59
111
  const DYNAMIC_ALLOCATION_KEY = 'spark.dynamicAllocation.enabled';
60
112
 
61
- // The four configAudit DETECTORS entries share this row, one per audited property.
113
+ // The three configAudit DETECTORS entries share this row, one per audited property.
62
114
  const CONFIG_AUDIT_PRESENTATION = {
63
115
  name: 'config audit',
64
116
  tag: 'CFG',
65
117
  thresholdSummary: () => 'a Spark conf value outside the recommended range',
66
118
  actionLabel(f) {
67
119
  switch (f.property) {
68
- case 'spark.shuffle.service.enabled': return 'Enable shuffle service';
69
- case 'spark.dynamicAllocation.minExecutors': return 'Fix autoscaling bounds';
70
120
  case 'spark.dynamicAllocation.maxExecutors': return 'Set max executors';
71
121
  case 'spark.serializer': return 'Switch to Kryo';
72
122
  case 'spark.executor.memoryOverhead': return 'Raise memory overhead';
@@ -75,11 +125,9 @@ const CONFIG_AUDIT_PRESENTATION = {
75
125
  },
76
126
  genericRecommendation(f) {
77
127
  switch (f.property) {
78
- case 'spark.shuffle.service.enabled': return 'Set spark.shuffle.service.enabled=true so shuffle data survives executor removal.';
79
- case 'spark.dynamicAllocation.minExecutors': return 'Set the minimum executor bound at or below the maximum.';
80
128
  case 'spark.dynamicAllocation.maxExecutors': return 'Set spark.dynamicAllocation.maxExecutors to cap cluster growth.';
81
129
  case 'spark.serializer': return 'Consider spark.serializer=org.apache.spark.serializer.KryoSerializer for faster, smaller buffers.';
82
- case 'spark.executor.memoryOverhead': return 'Raise executor memoryOverhead above Spark\'s default floor to avoid off-heap OOM-kills.';
130
+ case 'spark.executor.memoryOverhead': return 'Raise executor memoryOverhead above the default Spark would have computed to avoid off-heap OOM-kills.';
83
131
  }
84
132
  return undefined;
85
133
  },
@@ -98,7 +146,7 @@ export const FINDING_PRESENTATION
98
146
  skew: {
99
147
  name: 'task skew',
100
148
  tag: 'SKEW',
101
- thresholdSummary: (t) => `P95 task time over ${t.ratioWarn}× the median (the longest task on stages under ${t.minTasksForP95} tasks)`,
149
+ thresholdSummary: (t) => `P95 task time over ${t.ratioWarn}× the median (the longest task on stages under ${t.minTasksForP95} tasks), and the slow tasks read correspondingly more data`,
102
150
  actionLabel: () => 'Fix task skew',
103
151
  genericRecommendation: (f) => skewJoinGeneric(f,
104
152
  'For join-driven skew, enable AQE skew-join handling (spark.sql.adaptive.skewJoin.enabled); otherwise salt the key or repartition on a better key.'),
@@ -129,9 +177,14 @@ export const FINDING_PRESENTATION
129
177
  tag: 'TINY',
130
178
  thresholdSummary: (t) => `${t.minTasks}+ tasks with a median of ${t.maxP50}ms or less and a P95 of ${t.maxP95}ms or less`,
131
179
  actionLabel: () => 'Coalesce small tasks',
132
- genericRecommendation: (f) => (f.reads != null && f.reads !== 'shuffle'
133
- ? 'Scheduler overhead may dominate: coalesce down to fewer, larger tasks.'
134
- : 'Scheduler overhead may dominate: lower spark.sql.shuffle.partitions, or coalesce down to fewer, larger tasks.'),
180
+ genericRecommendation(f) {
181
+ if (f.reads != null && f.reads !== 'shuffle') return 'Scheduler overhead may dominate: coalesce down to fewer, larger tasks.';
182
+ switch (partitionAdviceKind(f)) {
183
+ case 'aqe': return 'Scheduler overhead may dominate: AQE already coalesced the shuffle, so set spark.sql.adaptive.coalescePartitions.parallelismFirst to false or raise spark.sql.adaptive.advisoryPartitionSizeInBytes, or coalesce down to fewer, larger tasks.';
184
+ case 'code': return "Scheduler overhead may dominate: this stage's own repartition(n) or RDD parallelism sized it, so lower that count or coalesce down to fewer, larger tasks.";
185
+ default: return 'Scheduler overhead may dominate: lower spark.sql.shuffle.partitions, or coalesce down to fewer, larger tasks.';
186
+ }
187
+ },
135
188
  },
136
189
 
137
190
  shuffle: {
@@ -180,9 +233,15 @@ export const FINDING_PRESENTATION
180
233
  thresholdSummary: (t) => `single-task disk spill above ${t.singleTaskDiskGiB} GiB`,
181
234
  actionLabel: () => 'Reduce spill',
182
235
  // The skew/volume classification isn't a Finding field, so one sentence covers both.
183
- genericRecommendation: (f) => (f.reads != null && f.reads !== 'shuffle'
184
- ? 'If the spill is skew-driven, fix task skew first: adding memory will not help. Otherwise increase executor memory or process less data per task.'
185
- : 'If the spill is skew-driven, fix task skew first: adding memory will not help. Otherwise raise spark.sql.shuffle.partitions or increase executor memory.'),
236
+ genericRecommendation(f) {
237
+ const skewFirst = 'If the spill is skew-driven, fix task skew first: adding memory will not help. Otherwise';
238
+ if (f.reads != null && f.reads !== 'shuffle') return `${skewFirst} increase executor memory or process less data per task.`;
239
+ switch (partitionAdviceKind(f)) {
240
+ case 'aqe': return `${skewFirst} lower spark.sql.adaptive.advisoryPartitionSizeInBytes so AQE's merged partitions are smaller, or increase executor memory.`;
241
+ case 'code': return `${skewFirst} raise this stage's own partition count (its repartition(n) or RDD parallelism), or increase executor memory.`;
242
+ default: return `${skewFirst} raise spark.sql.shuffle.partitions or increase executor memory.`;
243
+ }
244
+ },
186
245
  },
187
246
 
188
247
  gc: {
@@ -220,7 +279,7 @@ export const FINDING_PRESENTATION
220
279
  slowHost: {
221
280
  name: 'slow executor host',
222
281
  tag: 'HOST',
223
- thresholdSummary: (t) => `a host ${t.ratioWarn}× slower than its peers by mean task time (per-executor figures from ${t.ratioTiers[0]}×)`,
282
+ thresholdSummary: (t) => `a host ${t.ratioWarn}× slower than its peers by mean task time (per-executor figures from ${t.ratioTiers[0]}×, and only when the stage's own task-time spread cannot explain the gap)`,
224
283
  actionLabel(f) {
225
284
  if (f.variant === 'durationShare') return 'Fix data locality';
226
285
  if (f.variant === 'multiDim') return 'Investigate degraded executor';
@@ -240,18 +299,28 @@ export const FINDING_PRESENTATION
240
299
  tag: 'SLOW',
241
300
  thresholdSummary: () => 'a stage running far longer than its peers, not attributable to a single slow host',
242
301
  actionLabel: () => 'Profile slow stage',
243
- genericRecommendation: (f) => f.reads === 'input'
244
- ? 'Often too few or too uneven input partitions: check input file sizes and lower spark.sql.files.maxPartitionBytes, or look for a large per-task data volume driving heavy spill.'
245
- : f.reads === 'other'
246
- ? 'Check what the stage computes and for a large per-task data volume driving heavy spill.'
247
- : 'Often a partition-count problem: raise parallelism via spark.sql.shuffle.partitions or spark.default.parallelism, or check for a large per-task data volume driving heavy shuffle and spill.',
302
+ genericRecommendation(f) {
303
+ if (f.reads === 'input') return 'Often too few or too uneven input partitions: check input file sizes and lower spark.sql.files.maxPartitionBytes, or look for a large per-task data volume driving heavy spill.';
304
+ if (f.reads === 'other') return 'Check what the stage computes and for a large per-task data volume driving heavy spill.';
305
+ const spill = 'or check for a large per-task data volume driving heavy shuffle and spill.';
306
+ switch (partitionAdviceKind(f)) {
307
+ case 'aqe': return `Often a partition-count problem: AQE coalesced the shuffle, so lower spark.sql.adaptive.advisoryPartitionSizeInBytes to get more tasks, ${spill}`;
308
+ case 'code': return `Often a partition-count problem: raise this stage's own partition count (its repartition(n) or RDD parallelism), ${spill}`;
309
+ default: return `Often a partition-count problem: raise spark.sql.shuffle.partitions, ${spill}`;
310
+ }
311
+ },
248
312
  },
249
313
  straggler: {
250
314
  name: 'straggling task',
251
315
  tag: 'STRAG',
252
- thresholdSummary: () => 'one or more tasks finishing far after the rest of their stage',
316
+ thresholdSummary: () => 'one or more tasks finishing far after the rest of their stage, for a reason other than reading more data',
253
317
  actionLabel: () => 'Fix stragglers',
254
- genericRecommendation: (f) => `Rule out a GC pause or a slow shuffle fetch before assuming a hardware issue. If uneven data is the cause: ${skewJoinGeneric(f, 'for join-driven skew, enable AQE skew-join handling (spark.sql.adaptive.skewJoin.enabled); otherwise salt the key or repartition on a better key.')}`,
318
+ genericRecommendation: (f) => f.cause === 'data' ? `Uneven data volume drives the slow tasks: ${skewJoinGeneric(f, 'for join-driven skew, enable AQE skew-join handling (spark.sql.adaptive.skewJoin.enabled); otherwise salt the key or repartition on a better key.')}`
319
+ : f.cause === 'gc' ? 'GC accounts for most of the slow tasks\' extra time: reduce object creation, use primitive types, avoid UDFs, increase executor memory.'
320
+ : f.cause === 'fetchWait' ? 'Waiting on shuffle fetches accounts for most of the slow tasks\' extra time: look for a slow or overloaded node serving shuffle blocks, executors lost mid-stage, or reducers fetching many small blocks.'
321
+ : f.cause === 'host' ? 'Most slow tasks ran on one host: check what it was running, and consider enabling spark.speculation to relaunch a lagging task automatically.'
322
+ : f.cause === 'unexplained' ? 'The slow tasks read no more data than the median task, and GC, shuffle fetch wait and one slow host do not account for their time: look at per-record cost (UDFs, regular expressions, a call out per row).'
323
+ : 'Nothing in the log attributes the slow tasks to data volume, GC, shuffle fetch wait or one host: check their input sizes, GC time and hosts before choosing a fix.',
255
324
  },
256
325
  speculationWaste: {
257
326
  name: 'speculation waste',
@@ -279,8 +348,8 @@ export const FINDING_PRESENTATION
279
348
  case 'idleCores': return 'Reduce idle cores';
280
349
  case 'wasteModel': return 'Right-size executor memory';
281
350
  case 'memoryBand':
282
- if (f.dataUnavailable) return 'Enable memory metrics';
283
- return f.rule === 'heapNearCapacity' ? 'Increase executor memory' : 'Reduce executor memory';
351
+ if (f.dataUnavailable) return 'Executor heap peaks unavailable';
352
+ return 'Reduce executor memory';
284
353
  }
285
354
  return undefined;
286
355
  },
@@ -292,9 +361,7 @@ export const FINDING_PRESENTATION
292
361
  case 'wasteModel': return 'Review spark.executor.memory and executor count.';
293
362
  case 'memoryBand':
294
363
  if (f.dataUnavailable) return undefined;
295
- return f.rule === 'heapNearCapacity'
296
- ? 'Memory may be too small: raise spark.executor.memory to avoid OOM/spill.'
297
- : 'Memory may be over-provisioned: consider reducing spark.executor.memory for cost savings.';
364
+ return 'Memory may be over-provisioned: consider reducing spark.executor.memory for cost savings.';
298
365
  }
299
366
  return undefined;
300
367
  },
@@ -373,14 +440,30 @@ export const FINDING_PRESENTATION
373
440
  ? 'Repartition or coalesce before writing to raise the average file size.'
374
441
  : 'Compact the upstream output so fewer, larger files are produced.'),
375
442
  },
443
+ pythonUdf: {
444
+ name: 'row-at-a-time Python UDF',
445
+ tag: 'PLAN',
446
+ thresholdSummary: (t) => `over ${t.minBytesSent / 1048576} MiB sent to Python workers by stages running over ${t.minStageMs / 1000} s`,
447
+ actionLabel: () => 'Vectorize Python UDF',
448
+ genericRecommendation: () => 'Use Arrow-optimized Python UDFs (spark.sql.execution.pythonUDF.arrow.enabled, Spark 3.4 and later) or a pandas UDF instead of a row-at-a-time Python UDF.',
449
+ },
450
+ nestedLoopJoin: {
451
+ name: 'nested loop join',
452
+ tag: 'PLAN',
453
+ thresholdSummary: (t) => `a nested-loop or cartesian join with over ${groupThousands(t.minOutputRows)} output rows, at least ${t.minExpansion}x its larger input`,
454
+ actionLabel: (f) => (f.condition == null ? 'Confirm cross join' : 'Add an equi-join key'),
455
+ genericRecommendation: (f) => (f.condition == null
456
+ ? 'Confirm the cross join is intended, or add a join key so the rows are matched instead of multiplied.'
457
+ : 'Add an equi-join key so Spark can use a hash or sort-merge join; for a range condition, bucket the range and join on the bucket as well.'),
458
+ },
376
459
  underBroadcast: {
377
460
  name: 'missed broadcast join',
378
461
  tag: 'PLAN',
379
- thresholdSummary: () => 'a join below the configured size floor that skipped broadcast',
462
+ thresholdSummary: (t) => `a join side its type can broadcast, between ${t.minSmallerSideBytes / 1048576} MiB and ${t.overBroadcastBytes / 1073741824} GiB, that skipped broadcast`,
380
463
  actionLabel: () => 'Use broadcast join',
381
464
  genericRecommendation: (f) => (f.broadcastThreshold === 'notLimiting'
382
- ? 'The threshold already admits the smaller side, so it is not what stopped the broadcast: consider a broadcast() hint or collecting table statistics.'
383
- : 'This could have been a broadcast join: consider a broadcast() hint or raising spark.sql.autoBroadcastJoinThreshold.'),
465
+ ? 'The threshold already admits the build side, so it is not what stopped the broadcast: consider a broadcast() hint or collecting table statistics.'
466
+ : `This could have been a broadcast join: consider a broadcast() hint or raising ${broadcastThresholdKeyOf(f)}.`),
384
467
  },
385
468
  overBroadcast: {
386
469
  name: 'oversized broadcast join',
@@ -389,9 +472,9 @@ export const FINDING_PRESENTATION
389
472
  actionLabel: () => 'Fix oversized broadcast',
390
473
  genericRecommendation: (f) => (f.broadcastThreshold === 'notLimiting'
391
474
  ? 'The configured threshold is below this broadcast, so remove the broadcast() hint that forced it.'
392
- : switchAlreadyOn(f, 'spark.sql.autoBroadcastJoinThreshold')
475
+ : f.broadcastThreshold === 'disabled'
393
476
  ? 'Automatic broadcast is already disabled, so remove the broadcast() hint that forced it.'
394
- : 'Check for a misapplied broadcast hint or a misconfigured spark.sql.autoBroadcastJoinThreshold.'),
477
+ : `Check for a misapplied broadcast hint or a misconfigured ${broadcastThresholdKeyOf(f)}.`),
395
478
  },
396
479
  };
397
480
 
@@ -88,12 +88,20 @@
88
88
 
89
89
 
90
90
 
91
- // ── Per-stage detectors ─────────────────────────────────────────────────────
92
-
93
91
  /** What a skewed stage reads, which decides the fix: 'shuffleJoin' (a shuffle feeding a join, where
94
92
  * AQE skew-join handling applies), 'inputScan' (uneven input files) or 'other'. */
95
93
 
96
94
 
95
+ /** What AQE skew-join handling did for a 'shuffleJoin' stage, read from the final plan and the
96
+ * effective conf: 'split' (it split the skewed partition, so the rest is not join skew),
97
+ * 'evenReads' (the shuffle reads are even, so no partition is skewed for AQE to split),
98
+ * 'belowThreshold' (the partition is under the effective skew threshold or factor), 'planShape' (an
99
+ * operator sits between the join and its shuffle), 'userRepartition' (the shuffle is an explicit
100
+ * repartition), 'joinType' (the join type does not let AQE split the skewed side),
101
+ * 'extraShuffle' (splitting would add a shuffle for the operator above the join) or 'notSplit'
102
+ * (nothing in the log explains it). */
103
+
104
+
97
105
  /** What a stage reads, by the dominant side: a shuffle, input files, or neither. */
98
106
 
99
107
 
@@ -103,8 +111,23 @@
103
111
  * enough, so the stage's own repartition(n) or RDD parallelism is). */
104
112
 
105
113
 
114
+ /** Why a stage's slow tail is slow (attributeTail in stage-quantiles.ts). 'data': the slow tasks
115
+ * read far more than the median task, so run time follows data volume. 'gc', 'fetchWait' and
116
+ * 'host': the tail's extra time is GC, shuffle fetch wait, or tasks piled on one host.
117
+ * 'unexplained': the slow tasks read no more than the median and none of those accounts for their
118
+ * time. 'unattributed': the stage has no data volume to compare and what data, GC, fetch wait and
119
+ * host do not explain is the largest share (or the stage has no tail attribution), so only the
120
+ * duration is known. */
121
+
122
+
106
123
 
107
124
 
125
+
126
+
127
+
128
+
129
+
130
+
108
131
 
109
132
 
110
133
 
@@ -123,8 +146,9 @@
123
146
 
124
147
 
125
148
 
126
-
149
+
127
150
 
151
+
128
152
 
129
153
 
130
154
 
@@ -155,6 +179,10 @@
155
179
 
156
180
 
157
181
 
182
+
183
+
184
+
185
+
158
186
 
159
187
 
160
188
 
@@ -185,11 +213,23 @@
185
213
 
186
214
 
187
215
 
188
-
216
+
217
+
189
218
 
190
219
 
191
-
220
+
221
+
192
222
 
223
+
224
+
225
+
226
+
227
+
228
+
229
+
230
+
231
+
232
+
193
233
 
194
234
 
195
235
 
@@ -211,8 +251,6 @@
211
251
 
212
252
 
213
253
 
214
-
215
- // ── App-level detectors ─────────────────────────────────────────────────────
216
254
 
217
255
 
218
256
  // valueText is always 'missing': the ApplicationEnd event the log lacks.
@@ -225,7 +263,7 @@
225
263
 
226
264
 
227
265
 
228
-
266
+
229
267
 
230
268
 
231
269
 
@@ -239,21 +277,24 @@
239
277
 
240
278
 
241
279
 
242
-
243
-
280
+
281
+
282
+
244
283
 
245
-
284
+
246
285
 
247
286
 
248
287
 
249
288
 
250
289
 
251
290
 
252
-
291
+
253
292
 
254
293
 
255
-
256
294
 
295
+
296
+
297
+
257
298
 
258
299
 
259
300
 
@@ -304,17 +345,13 @@
304
345
 
305
346
 
306
347
 
307
-
308
- // ── Config-scope detectors ──────────────────────────────────────────────────
309
348
 
310
349
 
311
350
 
312
351
 
313
- // valueText is the audited property's current value, or a note that it is unset or inverted.
352
+ // valueText is the audited property's current value, or a note that it is unset.
314
353
 
315
354
 
316
- // ── SQL-scope (Plan Advisor) detectors ──────────────────────────────────────
317
-
318
355
 
319
356
 
320
357
 
@@ -344,13 +381,41 @@
344
381
 
345
382
 
346
383
 
347
- /** How the effective spark.sql.autoBroadcastJoinThreshold relates to the finding: 'limits' (the
384
+
385
+
386
+
387
+
388
+
389
+
390
+
391
+
392
+
393
+ /** A BroadcastNestedLoopJoin or CartesianProduct whose output is far larger than both inputs.
394
+ * `nodeName` is the operator, `joinType` the Spark join type (null when the plan line has none)
395
+ * and `condition` the join condition with expression ids removed (null for a join without one).
396
+ * `leftRows` and `rightRows` are the input row counts the executors reported, null for a
397
+ * CartesianProduct, which re-reads its inputs so their counts are not row counts. */
398
+
399
+
400
+
401
+
402
+
403
+
404
+
405
+
406
+
407
+
408
+ /** How the effective broadcast threshold (spark.sql.autoBroadcastJoinThreshold, or its adaptive counterpart under AQE) relates to the finding: 'limits' (the
348
409
  * property decided it, or is unknown), 'notLimiting' (already admits, or already below, the
349
410
  * broadcast, so something else decided it) or 'disabled' (-1). */
350
411
 
351
412
 
352
413
 
414
+
353
415
 
416
+
417
+
418
+
354
419
 
355
420
 
356
421
 
@@ -360,8 +425,6 @@
360
425
 
361
426
 
362
427
 
363
- // ── Unions ──────────────────────────────────────────────────────────────────
364
-
365
428
  /** Public evidence fields per finding type: what a report row's `evidence` carries. */
366
429
 
367
430
 
@@ -390,6 +453,8 @@
390
453
 
391
454
 
392
455
 
456
+
457
+
393
458
 
394
459
 
395
460
 
@@ -421,6 +486,8 @@
421
486
 
422
487
 
423
488
 
489
+
490
+
424
491
 
425
492
 
426
493
 
@@ -14,9 +14,9 @@ const BOTTLENECK_WIDGET = {
14
14
  coldStart: 'executor-timeline', utilization: 'executor-timeline', speculationWaste: 'executor-timeline',
15
15
  } ;
16
16
 
17
- // Cross-widget stage recurrence: how many distinct board widgets (skew+straggler on one stage
18
- // still count as one, task-skew) flag a stage. Computed once from the full catalog each widget
19
- // already receives.
17
+ // Cross-widget stage recurrence: how many distinct widget groups (skew, slowHost and straggler
18
+ // share task-skew) flag a stage. No production code calls it, and the group names in
19
+ // BOTTLENECK_WIDGET are labels, not ids of rendered widgets.
20
20
  // `stageId` may be number|null|undefined: sql/config-scope findings never set it; `stageId == null`
21
21
  // below treats both the same.
22
22
  export function stageWidgetFrequency(catalog ) {
@@ -69,9 +69,7 @@ export function formatStageIdsLabel(stageIds , maxShown = DEFAU
69
69
  }
70
70
 
71
71
  /** Worst (lowest IMPACT_BAND_ORDER) impact band across findings, undefined when empty. Shared by
72
- * every widget that badges/colors a group of findings.
73
- * @param {{ impactBand: 'critical' | 'warning' | 'info' }[]} findings
74
- * @returns {'critical' | 'warning' | 'info' | undefined} */
72
+ * every widget that badges/colors a group of findings. */
75
73
  export function worstImpactBand(
76
74
  findings ,
77
75
  ) {
@@ -285,15 +283,16 @@ export function formatRawWaste(rawWaste ) {
285
283
  }
286
284
  }
287
285
 
288
- // formatRawWaste/fmtMs round to a fixed precision (one decimal place, or
289
- // whole milliseconds below 1s), which can collapse a small but genuinely
290
- // nonzero value down to a formatted string that reads exactly like zero
291
- // (0.04 coreHours -> "0.0 core-h"). Checking the raw value against 0 misses
292
- // this; checking the *formatted* text against zero's own formatted text
293
- // catches it regardless of unit. The fractional group must consume the
294
- // entire decimal part (all zeros) before the terminator: otherwise a real
295
- // value like "0.5 core-h" leaves the "." unconsumed, and the terminator
296
- // char class excludes ".", so it correctly fails to match.
286
+ /** A cost-only figure as core time. Task time summed over tasks (`ms`) is core time, and printing
287
+ * it as a duration beside wall-clock savings reads as run time it does not shorten. */
288
+ export function asCoreTime(rawWaste ) {
289
+ return rawWaste.unit === 'ms' ? { ...rawWaste, unit: 'coreMs' } : rawWaste;
290
+ }
291
+
292
+ // True when a formatted figure reads as zero, including a small nonzero value the fixed rounding
293
+ // collapsed (0.04 coreHours -> "0.0 core-h"). It tests the formatted text, so it holds for any unit.
294
+ // The fractional group must consume the whole decimal part before the terminator, so "0.5 core-h"
295
+ // does not match.
297
296
  export function readsAsZero(formatted ) {
298
297
  return /^0(\.0+)?(?:[^0-9.]|$)/.test(formatted);
299
298
  }