sparkforensics-server 0.5.2 → 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 (445) 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/instrument-sans-latin-ext-wdth-normal-C5E2Gvlv.woff2 +0 -0
  63. package/public/assets/instrument-sans-latin-wdth-normal-BVScPF0l.woff2 +0 -0
  64. package/public/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  65. package/public/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  66. package/public/assets/layout.worker-CLcBHxVp.js +3 -0
  67. package/public/assets/{parser-worker-CA5RxqKc.js → parser-worker-CRIOPMOW.js} +8 -8
  68. package/public/assets/plan-graph-vendor-Ce6d4HU6.js +7 -0
  69. package/public/assets/react-vendor-C6KcUD36.js +9 -0
  70. package/public/assets/select-BHqOqun5.js +1 -0
  71. package/public/assets/toggle-DPeN6aNa.js +1 -0
  72. package/public/assets/toggle-group-Kl3uyHSr.js +1 -0
  73. package/public/assets/usePagedRows-BafD6bEN.js +1 -0
  74. package/public/assets/useRecentFiles-DzJZAoOT.js +3 -0
  75. package/public/assets/useRegisterFieldControl-pQv-ZDoU.js +1 -0
  76. package/public/assets/useScrollLock-BCxn_dMp.js +1 -0
  77. package/public/assets/useSortMode-DjrAN-z4.js +1 -0
  78. package/public/docs/404.html +3 -7
  79. package/public/docs/assets/{app.eYTPqTeq.js → app.1gvzd5Zb.js} +1 -1
  80. package/public/docs/assets/chunks/@localSearchIndexroot.DS2TQP5_.js +1 -0
  81. package/public/docs/assets/chunks/{VPLocalSearchBox.DsZrQKQR.js → VPLocalSearchBox.CNxT3s0I.js} +1 -1
  82. package/public/docs/assets/chunks/{theme.DsTNE2pE.js → theme.C5lLqUv7.js} +2 -2
  83. package/public/docs/assets/contributor-guide_architecture_board-widgets.md.CqKM_jEG.js +1 -0
  84. package/public/docs/assets/contributor-guide_architecture_board-widgets.md.CqKM_jEG.lean.js +1 -0
  85. package/public/docs/assets/{contributor-guide_architecture_board-widgets_caching-and-memory.md.8bH1HnZp.js → contributor-guide_architecture_board-widgets_caching-and-memory.md.BYDo3ClR.js} +1 -1
  86. package/public/docs/assets/contributor-guide_architecture_board-widgets_plan-advisor.md.ajUAVdbc.js +1 -0
  87. package/public/docs/assets/contributor-guide_architecture_board-widgets_visual-system-and-templating.md.WqY74T0T.js +1 -0
  88. package/public/docs/assets/contributor-guide_architecture_board-widgets_visual-system-and-templating.md.WqY74T0T.lean.js +1 -0
  89. package/public/docs/assets/contributor-guide_architecture_detector-contract.md.CHHgywoP.js +1 -0
  90. package/public/docs/assets/{contributor-guide_architecture_detector-contract.md.wj6Et6UD.lean.js → contributor-guide_architecture_detector-contract.md.CHHgywoP.lean.js} +1 -1
  91. package/public/docs/assets/contributor-guide_architecture_detector-contract_plan-attribution.md.DIz3_oz9.js +1 -0
  92. package/public/docs/assets/contributor-guide_architecture_detector-contract_plan-attribution.md.DIz3_oz9.lean.js +1 -0
  93. package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.WH4tRHPL.js +1 -0
  94. package/public/docs/assets/{contributor-guide_architecture_detector-contract_threshold-tables.md.CKjWG56Y.lean.js → contributor-guide_architecture_detector-contract_threshold-tables.md.WH4tRHPL.lean.js} +1 -1
  95. package/public/docs/assets/{contributor-guide_architecture_drill-down.md.CqZPrdo9.js → contributor-guide_architecture_drill-down.md.yB-Hz2XT.js} +1 -1
  96. 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
  97. package/public/docs/assets/contributor-guide_architecture_drill-down_plan-graph.md.BQZE04sw.js +1 -0
  98. 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
  99. package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.K71YB-Rr.js +1 -0
  100. package/public/docs/assets/{contributor-guide_architecture_impact-estimation.md.CZv_ZGOp.lean.js → contributor-guide_architecture_impact-estimation.md.K71YB-Rr.lean.js} +1 -1
  101. package/public/docs/assets/contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.DLCAffwn.js +1 -0
  102. package/public/docs/assets/{contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.BUucRc0A.lean.js → contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.DLCAffwn.lean.js} +1 -1
  103. package/public/docs/assets/contributor-guide_architecture_impact-estimation_stage-union-rollup.md.JFrzDMwo.js +1 -0
  104. package/public/docs/assets/contributor-guide_architecture_impact-estimation_stage-union-rollup.md.JFrzDMwo.lean.js +1 -0
  105. package/public/docs/assets/contributor-guide_architecture_overview.md.nxkpOhgf.js +1 -0
  106. package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.U-9KMDFK.js +1 -0
  107. 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
  108. package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.BrSz8Ezh.js +1 -0
  109. package/public/docs/assets/contributor-guide_architecture_widget-rendering_findings-tab.md.B7rjSF_h.js +1 -0
  110. package/public/docs/assets/contributor-guide_architecture_widget-rendering_findings-tab.md.B7rjSF_h.lean.js +1 -0
  111. 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
  112. 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
  113. package/public/docs/assets/{contributor-guide_architecture_worker-protocol.md.BAnhi2-d.js → contributor-guide_architecture_worker-protocol.md.Br6OE7RW.js} +1 -1
  114. package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.Br6OE7RW.lean.js +1 -0
  115. package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.IVyaXrGe.js +1 -0
  116. package/public/docs/assets/{contributor-guide_architecture_worker-protocol_cli-and-mcp.md.DC1YqXBp.lean.js → contributor-guide_architecture_worker-protocol_cli-and-mcp.md.IVyaXrGe.lean.js} +1 -1
  117. 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
  118. 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
  119. 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
  120. package/public/docs/assets/contributor-guide_architecture_worker-protocol_evidence-report.md.CtmCU57I.js +1 -0
  121. package/public/docs/assets/{contributor-guide_architecture_worker-protocol_evidence-report.md.DVGZOWvW.lean.js → contributor-guide_architecture_worker-protocol_evidence-report.md.CtmCU57I.lean.js} +1 -1
  122. package/public/docs/assets/contributor-guide_contributing.md.DBW4drtZ.js +1 -0
  123. package/public/docs/assets/{contributor-guide_development-setup.md.D9bu4j_c.js → contributor-guide_development-setup.md.B21ZTDhn.js} +3 -3
  124. package/public/docs/assets/contributor-guide_development-setup.md.B21ZTDhn.lean.js +1 -0
  125. package/public/docs/assets/contributor-guide_testing.md.DpkozNoG.js +4 -0
  126. package/public/docs/assets/{contributor-guide_testing.md.By6XGR9a.lean.js → contributor-guide_testing.md.DpkozNoG.lean.js} +1 -1
  127. package/public/docs/assets/{index.md.CHJVslga.js → index.md.lMm7SUMz.js} +1 -1
  128. package/public/docs/assets/{index.md.CHJVslga.lean.js → index.md.lMm7SUMz.lean.js} +1 -1
  129. package/public/docs/assets/instrument-sans-latin-ext-wdth-normal.C5E2Gvlv.woff2 +0 -0
  130. package/public/docs/assets/instrument-sans-latin-wdth-normal.BVScPF0l.woff2 +0 -0
  131. package/public/docs/assets/jetbrains-mono-cyrillic-wght-normal.D73BlboJ.woff2 +0 -0
  132. package/public/docs/assets/jetbrains-mono-greek-wght-normal.Bw9x6K1M.woff2 +0 -0
  133. package/public/docs/assets/jetbrains-mono-latin-ext-wght-normal.DBQx-q_a.woff2 +0 -0
  134. package/public/docs/assets/jetbrains-mono-latin-wght-normal.B9CIFXIH.woff2 +0 -0
  135. package/public/docs/assets/jetbrains-mono-vietnamese-wght-normal.Bt-aOZkq.woff2 +0 -0
  136. package/public/docs/assets/style.Cfxqlj0y.css +1 -0
  137. package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.C0pcQoNc.js +29 -0
  138. package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.C0pcQoNc.lean.js +1 -0
  139. package/public/docs/assets/user-guide_getting-started_reading-the-dashboard.md.CGaxPX_1.js +1 -0
  140. 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
  141. 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
  142. package/public/docs/assets/{user-guide_mcp-tools.md.C65SjWUL.js → user-guide_mcp-tools.md.49r3L2YE.js} +1 -1
  143. 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
  144. package/public/docs/assets/user-guide_mcp-tools_compare-runs.md.BQ_kIptC.lean.js +1 -0
  145. package/public/docs/assets/user-guide_mcp-tools_diagnose-run.md.azCZXkH8.js +44 -0
  146. 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
  147. package/public/docs/assets/{user-guide_mcp-tools_evaluate-budgets.md.BB0nDY3Z.js → user-guide_mcp-tools_evaluate-budgets.md.dpQpRQ7z.js} +2 -2
  148. 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
  149. 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
  150. 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
  151. package/public/docs/assets/{user-guide_mcp-tools_get-run-summary.md.DpxZ4ZNp.js → user-guide_mcp-tools_get-run-summary.md.EhaIsele.js} +1 -1
  152. package/public/docs/assets/user-guide_run-comparison.md.B_LOkdBf.js +1 -0
  153. package/public/docs/assets/user-guide_run-comparison.md.B_LOkdBf.lean.js +1 -0
  154. package/public/docs/assets/user-guide_understanding-findings.md.C4riKCKl.js +1 -0
  155. package/public/docs/assets/user-guide_understanding-findings.md.C4riKCKl.lean.js +1 -0
  156. package/public/docs/contributor-guide/architecture/board-widgets/caching-and-memory.html +6 -10
  157. package/public/docs/contributor-guide/architecture/board-widgets/plan-advisor.html +6 -10
  158. package/public/docs/contributor-guide/architecture/board-widgets/visual-system-and-templating.html +6 -10
  159. package/public/docs/contributor-guide/architecture/board-widgets.html +6 -10
  160. package/public/docs/contributor-guide/architecture/detector-contract/plan-attribution.html +6 -10
  161. package/public/docs/contributor-guide/architecture/detector-contract/threshold-tables.html +6 -10
  162. package/public/docs/contributor-guide/architecture/detector-contract.html +6 -10
  163. package/public/docs/contributor-guide/architecture/drill-down/plan-graph.html +6 -10
  164. package/public/docs/contributor-guide/architecture/drill-down.html +6 -10
  165. package/public/docs/contributor-guide/architecture/impact-estimation/caveats-tuning-and-coverage.html +6 -10
  166. package/public/docs/contributor-guide/architecture/impact-estimation/stage-union-rollup.html +6 -10
  167. package/public/docs/contributor-guide/architecture/impact-estimation.html +6 -10
  168. package/public/docs/contributor-guide/architecture/index.html +5 -9
  169. package/public/docs/contributor-guide/architecture/overview.html +6 -10
  170. package/public/docs/contributor-guide/architecture/state-and-history/history-server-intake.html +5 -9
  171. package/public/docs/contributor-guide/architecture/state-and-history/run-comparison-internals.html +6 -10
  172. package/public/docs/contributor-guide/architecture/state-and-history.html +5 -9
  173. package/public/docs/contributor-guide/architecture/widget-rendering/findings-tab.html +6 -10
  174. package/public/docs/contributor-guide/architecture/widget-rendering/investigation-routing.html +5 -9
  175. package/public/docs/contributor-guide/architecture/widget-rendering/list-sort-mode.html +6 -10
  176. package/public/docs/contributor-guide/architecture/widget-rendering/reference-section.html +5 -9
  177. package/public/docs/contributor-guide/architecture/widget-rendering/row-expand-contract.html +6 -10
  178. package/public/docs/contributor-guide/architecture/widget-rendering.html +6 -10
  179. package/public/docs/contributor-guide/architecture/worker-protocol/cli-and-mcp.html +6 -10
  180. package/public/docs/contributor-guide/architecture/worker-protocol/core-and-event-validation.html +6 -10
  181. package/public/docs/contributor-guide/architecture/worker-protocol/evidence-availability.html +7 -11
  182. package/public/docs/contributor-guide/architecture/worker-protocol/evidence-report.html +6 -10
  183. package/public/docs/contributor-guide/architecture/worker-protocol.html +6 -10
  184. package/public/docs/contributor-guide/contributing.html +6 -10
  185. package/public/docs/contributor-guide/development-setup.html +8 -12
  186. package/public/docs/contributor-guide/testing.html +9 -10
  187. package/public/docs/favicon.svg +2 -2
  188. package/public/docs/hashmap.json +1 -1
  189. package/public/docs/index.html +6 -10
  190. package/public/docs/logo-dark.svg +1 -0
  191. package/public/docs/logo-light.svg +1 -0
  192. package/public/docs/tuning-reference/anti-patterns.html +5 -9
  193. package/public/docs/tuning-reference/aqe.html +5 -9
  194. package/public/docs/tuning-reference/bottleneck-broadcast-sizing.html +5 -9
  195. package/public/docs/tuning-reference/bottleneck-cold-start.html +5 -9
  196. package/public/docs/tuning-reference/bottleneck-duplicate-plan-subtree.html +5 -9
  197. package/public/docs/tuning-reference/bottleneck-failures.html +5 -9
  198. package/public/docs/tuning-reference/bottleneck-gc.html +5 -9
  199. package/public/docs/tuning-reference/bottleneck-job-failure-rate.html +5 -9
  200. package/public/docs/tuning-reference/bottleneck-memory-utilization.html +5 -9
  201. package/public/docs/tuning-reference/bottleneck-retry-waste.html +5 -9
  202. package/public/docs/tuning-reference/bottleneck-shuffle.html +5 -9
  203. package/public/docs/tuning-reference/bottleneck-skew.html +5 -9
  204. package/public/docs/tuning-reference/bottleneck-slow-host.html +5 -9
  205. package/public/docs/tuning-reference/bottleneck-small-files.html +5 -9
  206. package/public/docs/tuning-reference/bottleneck-spill.html +5 -9
  207. package/public/docs/tuning-reference/bottleneck-straggler.html +5 -9
  208. package/public/docs/tuning-reference/bottleneck-tiny-tasks.html +5 -9
  209. package/public/docs/tuning-reference/bottleneck-utilization.html +5 -9
  210. package/public/docs/tuning-reference/caching.html +5 -9
  211. package/public/docs/tuning-reference/cluster-config.html +5 -9
  212. package/public/docs/tuning-reference/config.html +5 -9
  213. package/public/docs/tuning-reference/data-formats.html +5 -9
  214. package/public/docs/tuning-reference/index.html +5 -9
  215. package/public/docs/tuning-reference/intro.html +5 -9
  216. package/public/docs/tuning-reference/joins.html +5 -9
  217. package/public/docs/tuning-reference/memory-model.html +5 -9
  218. package/public/docs/tuning-reference/metrics.html +5 -9
  219. package/public/docs/tuning-reference/partitioning.html +5 -9
  220. package/public/docs/tuning-reference/pyspark.html +5 -9
  221. package/public/docs/tuning-reference/shuffle.html +5 -9
  222. package/public/docs/tuning-reference/spark-architecture.html +5 -9
  223. package/public/docs/tuning-reference/table-formats.html +5 -9
  224. package/public/docs/user-guide/alternative-log-retrieval/managed-platforms.html +5 -9
  225. package/public/docs/user-guide/alternative-log-retrieval/ssh-bastion.html +5 -9
  226. package/public/docs/user-guide/alternative-log-retrieval.html +5 -9
  227. package/public/docs/user-guide/getting-started/ci-and-automation.html +17 -15
  228. package/public/docs/user-guide/getting-started/reading-the-dashboard.html +6 -10
  229. package/public/docs/user-guide/getting-started/regression-metric-keys.html +5 -9
  230. package/public/docs/user-guide/getting-started/tuning-thresholds.html +7 -11
  231. package/public/docs/user-guide/getting-started.html +5 -9
  232. package/public/docs/user-guide/mcp-tools/compare-runs.html +16 -15
  233. package/public/docs/user-guide/mcp-tools/diagnose-run.html +28 -11
  234. package/public/docs/user-guide/mcp-tools/evaluate-budgets.html +6 -10
  235. package/public/docs/user-guide/mcp-tools/get-finding-documentation.html +5 -9
  236. package/public/docs/user-guide/mcp-tools/get-finding-evidence.html +7 -10
  237. package/public/docs/user-guide/mcp-tools/get-reference-doc.html +5 -9
  238. package/public/docs/user-guide/mcp-tools/get-run-summary.html +6 -10
  239. package/public/docs/user-guide/mcp-tools/list-runs.html +5 -9
  240. package/public/docs/user-guide/mcp-tools.html +6 -10
  241. package/public/docs/user-guide/run-comparison/how-stages-are-matched.html +5 -9
  242. package/public/docs/user-guide/run-comparison.html +6 -10
  243. package/public/docs/user-guide/understanding-findings.html +6 -10
  244. package/public/export-template.html +46 -46
  245. package/public/favicon.svg +2 -2
  246. package/public/index.html +8 -10
  247. package/public/sample-runs/sample-run.ndjson.gz +0 -0
  248. package/vendor-core/allocation.js +126 -39
  249. package/vendor-core/analyzer.js +85 -58
  250. package/vendor-core/api-merge-writes.js +3 -0
  251. package/vendor-core/aqe-skew.js +347 -0
  252. package/vendor-core/cli/budgets.js +6 -4
  253. package/vendor-core/cli/collect-run-pair.js +89 -0
  254. package/vendor-core/cli/collect-run-worker.js +16 -0
  255. package/vendor-core/cli/collect-run.js +1 -2
  256. package/vendor-core/cli/native-zstd.js +12 -16
  257. package/vendor-core/comparability.js +234 -0
  258. package/vendor-core/comparison-output.js +15 -7
  259. package/vendor-core/comparison-verdict.js +105 -25
  260. package/vendor-core/core-source-hash.txt +1 -1
  261. package/vendor-core/core-time-series.js +0 -1
  262. package/vendor-core/core-usage-locality.js +1 -1
  263. package/vendor-core/delta-log-paths.js +10 -1
  264. package/vendor-core/detectors.js +1078 -416
  265. package/vendor-core/docs-config.js +0 -4
  266. package/vendor-core/docs-content/detection/cache.md +2 -1
  267. package/vendor-core/docs-content/detection/cfg.md +19 -11
  268. package/vendor-core/docs-content/detection/cold.md +2 -1
  269. package/vendor-core/docs-content/detection/gc.md +7 -3
  270. package/vendor-core/docs-content/detection/host.md +11 -1
  271. package/vendor-core/docs-content/detection/mem.md +17 -6
  272. package/vendor-core/docs-content/detection/part.md +3 -1
  273. package/vendor-core/docs-content/detection/plan.md +55 -13
  274. package/vendor-core/docs-content/detection/shfl.md +11 -5
  275. package/vendor-core/docs-content/detection/skew.md +48 -1
  276. package/vendor-core/docs-content/detection/slow.md +4 -2
  277. package/vendor-core/docs-content/detection/spec.md +6 -1
  278. package/vendor-core/docs-content/detection/spill.md +6 -3
  279. package/vendor-core/docs-content/detection/strag.md +19 -6
  280. package/vendor-core/docs-content/detection/tiny.md +9 -3
  281. package/vendor-core/docs-content/detection/util.md +6 -2
  282. package/vendor-core/effective-conf.js +7 -10
  283. package/vendor-core/efficiency-model.js +24 -16
  284. package/vendor-core/etl-phases.js +1 -2
  285. package/vendor-core/event-handlers.js +175 -57
  286. package/vendor-core/event-schemas.js +21 -16
  287. package/vendor-core/evidence-availability.js +9 -2
  288. package/vendor-core/evidence-report.js +118 -35
  289. package/vendor-core/executor-peaks.js +41 -0
  290. package/vendor-core/export-data.js +5 -5
  291. package/vendor-core/finding-identity.js +78 -0
  292. package/vendor-core/finding-presentation.js +129 -39
  293. package/vendor-core/finding-types.js +108 -30
  294. package/vendor-core/format-utils.js +14 -15
  295. package/vendor-core/html-export.js +1 -1
  296. package/vendor-core/impact-band.js +2 -2
  297. package/vendor-core/impact-format.js +47 -19
  298. package/vendor-core/impact-model.js +28 -17
  299. package/vendor-core/job-groups.js +4 -4
  300. package/vendor-core/mcp-server-factory.js +48 -12
  301. package/vendor-core/mcp-tools.js +179 -24
  302. package/vendor-core/median.js +2 -2
  303. package/vendor-core/nested-loop-join.js +73 -0
  304. package/vendor-core/occupancy.js +1 -2
  305. package/vendor-core/parser-worker.js +1 -1
  306. package/vendor-core/plan-graph-model.js +14 -22
  307. package/vendor-core/plan-node-detail.js +2 -3
  308. package/vendor-core/plan-summary.js +24 -19
  309. package/vendor-core/plan-tree-walk.js +6 -8
  310. package/vendor-core/python-stage.js +12 -1
  311. package/vendor-core/recommendation-rollup.js +13 -12
  312. package/vendor-core/redact.js +52 -46
  313. package/vendor-core/remediation.js +10 -5
  314. package/vendor-core/run-aggregates.js +6 -8
  315. package/vendor-core/run-comparison.js +226 -66
  316. package/vendor-core/run-identity.js +12 -0
  317. package/vendor-core/run-interpretation.js +21 -0
  318. package/vendor-core/run-metrics.js +2 -2
  319. package/vendor-core/run-output.js +44 -11
  320. package/vendor-core/run-totals.js +12 -0
  321. package/vendor-core/run-verdict.js +76 -3
  322. package/vendor-core/scaling-sim.js +3 -4
  323. package/vendor-core/snappy-block.js +2 -2
  324. package/vendor-core/spark-conf.js +168 -0
  325. package/vendor-core/sql-stages.js +2 -2
  326. package/vendor-core/stage-alignment.js +2 -12
  327. package/vendor-core/stage-identity.js +58 -3
  328. package/vendor-core/stage-label.js +14 -0
  329. package/vendor-core/stage-plan-nodes.js +17 -3
  330. package/vendor-core/stage-quantiles.js +141 -9
  331. package/vendor-core/stage-structure.js +17 -3
  332. package/vendor-core/threshold-overrides.js +9 -3
  333. package/vendor-core/types.js +70 -23
  334. package/vendor-core/wall-clock.js +8 -0
  335. package/vendor-core/wasted-core-hours.js +12 -12
  336. package/vendor-core/write-targets.js +146 -14
  337. package/vendor-core/zstd-worker-client.js +1 -1
  338. package/vendor-core/zstd-worker.js +1 -1
  339. package/public/assets/AutoscalingChurn-8t3U3isq.js +0 -1
  340. package/public/assets/CacheUtilization-CJs9tTum.js +0 -1
  341. package/public/assets/CachingOpportunity-D1NcHs0V.js +0 -1
  342. package/public/assets/ColdStart-CdRJ9gD8.js +0 -1
  343. package/public/assets/ConfigAudit-B5Gbe5js.js +0 -1
  344. package/public/assets/CoreUsageArea-a--NTRGY.js +0 -1
  345. package/public/assets/Dashboard-C1hNeX49.js +0 -2
  346. package/public/assets/DisclosureContext-ZpE6CkHx.js +0 -1
  347. package/public/assets/DuplicatePlanSubtree-dobXdz7E.js +0 -1
  348. package/public/assets/ExecutorUtilization-D9grwi7t.js +0 -1
  349. package/public/assets/GcPressure-8-G2ri10.js +0 -1
  350. package/public/assets/IncompleteRun-B91lky7a.js +0 -1
  351. package/public/assets/JobFailures-BsWvBMyJ.js +0 -1
  352. package/public/assets/MemoryUtilization-BM5schZI.js +0 -1
  353. package/public/assets/OverBroadcast-DdN8JIJH.js +0 -1
  354. package/public/assets/PartitionHint-CUvbnnxy.js +0 -1
  355. package/public/assets/PartitionSizing-CY5HDYtp.js +0 -1
  356. package/public/assets/PlanExplorer-DgHCDBD_.js +0 -1
  357. package/public/assets/PlanGraphRoute-DH3KJb8j.js +0 -1
  358. package/public/assets/RetryWaste-DOnE9vGF.js +0 -1
  359. package/public/assets/ShuffleIO-MMnDESkF.js +0 -1
  360. package/public/assets/Skew-BmZZJ5ms.js +0 -1
  361. package/public/assets/SlowHost-CsJzfEqo.js +0 -1
  362. package/public/assets/SmallFiles-BoK3R1Ex.js +0 -1
  363. package/public/assets/SpeculationWaste-CnVxB5AJ.js +0 -1
  364. package/public/assets/Spill-BhNicyBy.js +0 -1
  365. package/public/assets/StageFailed-mAhHU4l_.js +0 -1
  366. package/public/assets/StageFindingGroup-BEtPWcT0.js +0 -1
  367. package/public/assets/StageHeader-DJkYkrsU.js +0 -1
  368. package/public/assets/StageShape-BDxRknLp.js +0 -1
  369. package/public/assets/StageSlowness-BPRw-otN.js +0 -1
  370. package/public/assets/Straggler-CZCbnOWP.js +0 -1
  371. package/public/assets/TaskFailures-C57YcLFQ.js +0 -1
  372. package/public/assets/TinyTask-Bi84qU7n.js +0 -1
  373. package/public/assets/UnderBroadcast-DdQV7htE.js +0 -1
  374. package/public/assets/charts-vendor-0VMaLVLN.js +0 -59
  375. package/public/assets/finding-anchor-Cl6_5hfV.js +0 -1
  376. package/public/assets/impact-sort-CssZ6MT2.js +0 -1
  377. package/public/assets/index-2h_TgUAb.css +0 -2
  378. package/public/assets/index-B-03mG7K.js +0 -14
  379. package/public/assets/plan-graph-vendor-BYl-AzN5.js +0 -7
  380. package/public/assets/react-vendor-DEdfrRTJ.js +0 -9
  381. package/public/assets/select-CQjNhprm.js +0 -1
  382. package/public/assets/usePagedRows-Dyl6T8Ok.js +0 -1
  383. package/public/assets/useRecentFiles-XZEnLlal.js +0 -11
  384. package/public/assets/useSortMode-Dhvit8dt.js +0 -1
  385. package/public/docs/assets/chunks/@localSearchIndexroot.u_e5bQz4.js +0 -1
  386. package/public/docs/assets/contributor-guide_architecture_board-widgets.md.YQcj5EI4.js +0 -1
  387. package/public/docs/assets/contributor-guide_architecture_board-widgets.md.YQcj5EI4.lean.js +0 -1
  388. package/public/docs/assets/contributor-guide_architecture_board-widgets_plan-advisor.md.RlzvUmNm.js +0 -1
  389. package/public/docs/assets/contributor-guide_architecture_board-widgets_visual-system-and-templating.md.DYgUpIJB.js +0 -1
  390. package/public/docs/assets/contributor-guide_architecture_board-widgets_visual-system-and-templating.md.DYgUpIJB.lean.js +0 -1
  391. package/public/docs/assets/contributor-guide_architecture_detector-contract.md.wj6Et6UD.js +0 -1
  392. package/public/docs/assets/contributor-guide_architecture_detector-contract_plan-attribution.md.yNex4bys.js +0 -1
  393. package/public/docs/assets/contributor-guide_architecture_detector-contract_plan-attribution.md.yNex4bys.lean.js +0 -1
  394. package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.CKjWG56Y.js +0 -1
  395. package/public/docs/assets/contributor-guide_architecture_drill-down_plan-graph.md.DwOozIEs.js +0 -1
  396. package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.CZv_ZGOp.js +0 -1
  397. package/public/docs/assets/contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.BUucRc0A.js +0 -1
  398. package/public/docs/assets/contributor-guide_architecture_impact-estimation_stage-union-rollup.md.DdCGtAGu.js +0 -1
  399. package/public/docs/assets/contributor-guide_architecture_impact-estimation_stage-union-rollup.md.DdCGtAGu.lean.js +0 -1
  400. package/public/docs/assets/contributor-guide_architecture_overview.md.C1sSvm9i.js +0 -1
  401. package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.CZA0JLtV.js +0 -1
  402. package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.BOZ1azXG.js +0 -1
  403. package/public/docs/assets/contributor-guide_architecture_widget-rendering_findings-tab.md.BQXycK6G.js +0 -1
  404. package/public/docs/assets/contributor-guide_architecture_widget-rendering_findings-tab.md.BQXycK6G.lean.js +0 -1
  405. package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.BAnhi2-d.lean.js +0 -1
  406. package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.DC1YqXBp.js +0 -1
  407. package/public/docs/assets/contributor-guide_architecture_worker-protocol_evidence-report.md.DVGZOWvW.js +0 -1
  408. package/public/docs/assets/contributor-guide_contributing.md.CQMvGwFt.js +0 -1
  409. package/public/docs/assets/contributor-guide_development-setup.md.D9bu4j_c.lean.js +0 -1
  410. package/public/docs/assets/contributor-guide_testing.md.By6XGR9a.js +0 -1
  411. package/public/docs/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
  412. package/public/docs/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
  413. package/public/docs/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
  414. package/public/docs/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
  415. package/public/docs/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
  416. package/public/docs/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
  417. package/public/docs/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
  418. package/public/docs/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
  419. package/public/docs/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
  420. package/public/docs/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
  421. package/public/docs/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
  422. package/public/docs/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
  423. package/public/docs/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
  424. package/public/docs/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
  425. package/public/docs/assets/style.tmDhD3c8.css +0 -1
  426. package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.OUQqw3mI.js +0 -23
  427. package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.OUQqw3mI.lean.js +0 -1
  428. package/public/docs/assets/user-guide_getting-started_reading-the-dashboard.md.Obf3W3__.js +0 -1
  429. package/public/docs/assets/user-guide_mcp-tools_compare-runs.md.Cc_gLoix.lean.js +0 -1
  430. package/public/docs/assets/user-guide_mcp-tools_diagnose-run.md.BP1vnHA5.js +0 -23
  431. package/public/docs/assets/user-guide_run-comparison.md.BUALkohi.js +0 -1
  432. package/public/docs/assets/user-guide_run-comparison.md.BUALkohi.lean.js +0 -1
  433. package/public/docs/assets/user-guide_understanding-findings.md.C9YRruMT.js +0 -1
  434. package/public/docs/assets/user-guide_understanding-findings.md.C9YRruMT.lean.js +0 -1
  435. /package/public/docs/assets/{contributor-guide_architecture_board-widgets_caching-and-memory.md.8bH1HnZp.lean.js → contributor-guide_architecture_board-widgets_caching-and-memory.md.BYDo3ClR.lean.js} +0 -0
  436. /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
  437. /package/public/docs/assets/{contributor-guide_architecture_overview.md.C1sSvm9i.lean.js → contributor-guide_architecture_overview.md.nxkpOhgf.lean.js} +0 -0
  438. /package/public/docs/assets/{contributor-guide_architecture_widget-rendering.md.BOZ1azXG.lean.js → contributor-guide_architecture_widget-rendering.md.BrSz8Ezh.lean.js} +0 -0
  439. /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
  440. /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
  441. /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
  442. /package/public/docs/assets/{contributor-guide_contributing.md.CQMvGwFt.lean.js → contributor-guide_contributing.md.DBW4drtZ.lean.js} +0 -0
  443. /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
  444. /package/public/docs/assets/{user-guide_mcp-tools.md.C65SjWUL.lean.js → user-guide_mcp-tools.md.49r3L2YE.lean.js} +0 -0
  445. /package/public/docs/assets/{user-guide_mcp-tools_get-run-summary.md.DpxZ4ZNp.lean.js → user-guide_mcp-tools_get-run-summary.md.EhaIsele.lean.js} +0 -0
@@ -2,10 +2,12 @@ import { z } from 'zod';
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
 
4
4
  import {
5
- resolveOrCreateRun, diagnoseRun, getRunSummary, compareRuns, getFindingEvidence, getFindingDocumentation, getReferenceDoc, evaluateBudgetsForRun,
5
+ resolveOrCreateRun, diagnoseRun, diagnoseHeadline, getRunSummary, compareRuns, getFindingEvidence, getFindingDocumentation, getReferenceDoc, evaluateBudgetsForRun,
6
+
6
7
  } from './mcp-tools.js';
7
8
  import { listRuns } from './list-runs.js';
8
9
 
10
+
9
11
  import { MAX_NORMALIZE_PATTERN_LENGTH, MAX_NORMALIZE_PATTERNS } from './stage-alignment.js';
10
12
 
11
13
  const sourceSchema = z.union([
@@ -42,7 +44,7 @@ function toolErrorResult(error )
42
44
  };
43
45
  }
44
46
 
45
- // diagnoseRun/compareRuns may carry a `markdown` field (only when the caller asked for
47
+ // compareRuns may carry a `markdown` field (only when the caller asked for
46
48
  // format: 'md'); it never belongs in structuredContent (whose shape must stay stable regardless of
47
49
  // format), so it's stripped here and, when present, becomes content[0].text instead of JSON.stringify.
48
50
  function toolResultWithMarkdown (promise ) {
@@ -55,16 +57,40 @@ function toolResultWithMarkdown (promise
55
57
  );
56
58
  }
57
59
 
60
+ // diagnose_run's own mapping: with `format: 'md'` the Markdown is the content and structuredContent
61
+ // shrinks to a few headline fields, so a client that stores it does not store the whole report.
62
+ function diagnoseToolResult(promise ) {
63
+ return promise.then(
64
+ (value) => {
65
+ const { markdown, ...structuredContent } = value;
66
+ return markdown !== undefined
67
+ ? toCallToolResult(markdown, diagnoseHeadline(value))
68
+ : toCallToolResult(JSON.stringify(structuredContent), structuredContent );
69
+ },
70
+ toolErrorResult,
71
+ );
72
+ }
73
+
58
74
  // Counterpart for the tools that never produce a markdown field: its own mapping, so there's no
59
75
  // need to cast T to pretend it might carry a `markdown` field it never does.
60
76
  function toolResult (promise ) {
61
77
  return promise.then((value) => toCallToolResult(JSON.stringify(value), value ), toolErrorResult);
62
78
  }
63
79
 
80
+ // The handshake version of a server built without a `generator`: no package to read a version from.
81
+ const UNKNOWN_VERSION = '0.0.0';
82
+
83
+ // Shared by every tool that returns a run handle.
84
+ const RUN_ID_NOTE = 'runId is a handle for the run this server process has cached. It differs per tool by design (compare_runs returns runIdA and runIdB, evaluate_budgets the runId of its first run) and per server process, so match runs by app.id and app.attemptId, not by runId.';
85
+
64
86
  /** `thresholds`: the bin's --thresholds overrides, applied to every analyzing tool for the life
65
- * of the server. Omitted, every tool runs the specification defaults. */
66
- export function createMcpServer({ thresholds } = {}) {
67
- const server = new McpServer({ name: 'sparkforensics', version: '1.0.0' });
87
+ * of the server. Omitted, every tool runs the specification defaults.
88
+ * `generator`: the host package's name, version and core build id. The server reports its version
89
+ * in the MCP handshake and every diagnose_run and compare_runs result carries the block. */
90
+ export function createMcpServer(
91
+ { thresholds, generator } = {},
92
+ ) {
93
+ const server = new McpServer({ name: 'sparkforensics', version: generator?.version ?? UNKNOWN_VERSION });
68
94
 
69
95
  server.registerTool('list_runs', {
70
96
  description: 'List candidate Spark event-log runs from a local directory or a Spark History Server, before diagnosing one with the other tools.',
@@ -72,16 +98,22 @@ export function createMcpServer({ thresholds }
72
98
  }, (params) => toolResult(listRuns(params)));
73
99
 
74
100
  server.registerTool('diagnose_run', {
75
- description: 'Diagnose a Spark run: the dashboard verdict (title, summary, and the top places to look, ranked by potential savings), thresholded findings with remediation text, an impact-ranked fix recommendation rollup, clean-check status, the checks the log lacked the data to run, and the writeTargets, metrics and effectiveConf blocks of the CLI report. When the server was started with --thresholds, findings and clean checks from a tuned detector carry tunedThresholds, and their impact estimates are uncalibrated.',
101
+ description: 'Diagnose a Spark run: the dashboard verdict (title, summary, and the top places to look, ranked by potential savings, each with its lead finding), findings, an impact-ranked fix recommendation rollup, and the writeTargets, metrics and effectiveConf blocks of the CLI report. view "compact" (the default) lists one short row per finding and leaves out the clean-check and not-checked lists; view "full" returns every finding with its remediation text and evidence, and those lists. Every finding row has a key that stays the same for the same finding in a rerun of the job (compare_runs rows list the same keys), and an id for get_finding_evidence. With format "md" the content is Markdown and structuredContent holds a few headline fields. When the server was started with --thresholds, findings and clean checks from a tuned detector carry tunedThresholds, and their impact estimates are uncalibrated. The result names the Spark application (app) and the build that wrote it (generator). ' + RUN_ID_NOTE,
76
102
  inputSchema: {
77
103
  ...runRefSchema, redact: z.boolean().optional(),
104
+ view: z.enum(['compact', 'full']).optional()
105
+ .describe('compact (default): one short row per finding. full: the CLI report\'s rows, clean checks and not-checked checks.'),
78
106
  include: z.array(z.enum(['summary', 'evidenceAvailability', 'detectors'])).optional(),
79
107
  impactBand: z.array(z.string()).optional(), type: z.array(z.string()).optional(), stageId: z.number().int().optional(),
108
+ minBand: z.enum(['critical', 'warning', 'info']).optional()
109
+ .describe('List only findings at this band or a more severe one.'),
110
+ maxFindings: z.number().int().nonnegative().optional()
111
+ .describe('List at most this many findings, most severe first. findingsOmitted counts the ones left out; each verdict step still carries its lead finding.'),
80
112
  ...formatSchema,
81
113
  },
82
- }, ({ source, runId, redact, include, format, impactBand, type, stageId }) => toolResultWithMarkdown(
114
+ }, ({ source, runId, redact, include, format, impactBand, type, stageId, view, minBand, maxFindings }) => diagnoseToolResult(
83
115
  resolveOrCreateRun({ source, runId }).then(({ runId: id }) =>
84
- diagnoseRun(id, { redact, include, markdown: format === 'md', impactBand, type, stageId, thresholds })),
116
+ diagnoseRun(id, { redact, include, markdown: format === 'md', impactBand, type, stageId, thresholds, generator, view: view ?? 'compact', minBand, maxFindings })),
85
117
  ));
86
118
 
87
119
  server.registerTool('get_run_summary', {
@@ -92,7 +124,7 @@ export function createMcpServer({ thresholds }
92
124
  ));
93
125
 
94
126
  server.registerTool('compare_runs', {
95
- description: 'Compare two runs: the verdict, metric deltas (metrics), categorized findings delta (findings) and the stage pairs (stagePairs) with the share of executor run time they cover (runtimeCoverage). confidence is ok, low or insufficient. metricDeltas and findingsDelta repeat metrics and findings under their earlier names and will be removed.',
127
+ description: 'Compare two runs: the verdict, metric deltas (metrics), categorized findings delta (findings), the stage pairs (stagePairs) with the share of executor run time they cover (runtimeCoverage), and comparability: the configuration differences, plan equality, data volume, executor capacity and the factors that bridge the wall-clock difference. confidence is ok, low or insufficient. metricDeltas and findingsDelta repeat metrics and findings under their earlier names and will be removed. The result names both Spark applications (baselineApp, candidateApp) and the build that wrote it (generator). Each findings row lists its stage names in stages and the same names without call-site directories in stageLabels. ' + RUN_ID_NOTE,
96
128
  inputSchema: {
97
129
  runIdA: z.string().optional(), sourceA: sourceSchema.optional(),
98
130
  ...secondRunRefSchema,
@@ -101,14 +133,18 @@ export function createMcpServer({ thresholds }
101
133
  .describe('Extra blocks to return. stagePairs: one row per paired stage with its deltas, which can be large.'),
102
134
  normalizePath: z.array(z.string().max(MAX_NORMALIZE_PATTERN_LENGTH)).max(MAX_NORMALIZE_PATTERNS).optional()
103
135
  .describe('Regular expressions whose every match in a plan node\'s text is replaced with a fixed token before stages are paired, so run-specific text (an output directory that differs per run) does not keep the same stage from pairing. A generic pattern, not only for paths. Affects stage pairing, runtimeCoverage and confidence, never findings. A pattern that backtracks catastrophically can stall the server: keep them simple and anchored.'),
136
+ ignoreConf: z.array(z.string().max(MAX_NORMALIZE_PATTERN_LENGTH)).max(MAX_NORMALIZE_PATTERNS).optional()
137
+ .describe('Regular expressions matched against Spark configuration keys. A key that matches is left out of comparability.conf, for keys that differ by design such as a per-run output path. Per-run identity keys (application id, start time, driver address) are always left out.'),
138
+ noiseBand: z.number().min(0).max(1).optional()
139
+ .describe('Share of the baseline value under which a metric row is marked withinNoise (default 0.02). Never changes a row\'s direction.'),
104
140
  ...formatSchema,
105
141
  },
106
- }, ({ runIdA, sourceA, runIdB, sourceB, redact, include, normalizePath, format }) => toolResultWithMarkdown(
107
- compareRuns({ runId: runIdA, source: sourceA }, { runId: runIdB, source: sourceB }, { redact, markdown: format === 'md', thresholds, normalizePath, include }),
142
+ }, ({ runIdA, sourceA, runIdB, sourceB, redact, include, normalizePath, ignoreConf, noiseBand, format }) => toolResultWithMarkdown(
143
+ compareRuns({ runId: runIdA, source: sourceA }, { runId: runIdB, source: sourceB }, { redact, markdown: format === 'md', thresholds, normalizePath, ignoreConf, noiseBand, include, generator }),
108
144
  ));
109
145
 
110
146
  server.registerTool('evaluate_budgets', {
111
- description: 'Evaluate a run against pass/fail thresholds, optionally against a baseline run for regression budgets. With two runs, absolute budgets apply to the candidate (sourceB/runIdB), matching the CLI. A run with no ApplicationEnd always adds an inconclusive run-complete result.',
147
+ description: 'Evaluate a run against pass/fail thresholds, optionally against a baseline run for regression budgets. With two runs, absolute budgets apply to the candidate (sourceB/runIdB), matching the CLI. A run with no ApplicationEnd always adds an inconclusive run-complete result. ' + RUN_ID_NOTE,
112
148
  inputSchema: {
113
149
  source: sourceSchema.optional().describe('The run to evaluate. When sourceB/runIdB is given, this is the regression baseline instead, and the absolute budgets apply to sourceB/runIdB.'),
114
150
  runId: runRefSchema.runId.describe('Same as `source`, referencing an already-resolved run by id.'),
@@ -1,17 +1,21 @@
1
1
  import { resolve as resolvePath, join, dirname } from 'node:path';
2
- import { existsSync, statSync, readFileSync } from 'node:fs';
2
+ import { existsSync, statSync, readFileSync, readdirSync, } from 'node:fs';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { collectRun } from './cli/collect-run.js';
6
6
  import { deriveEvidenceAvailability } from './evidence-availability.js';
7
7
  import { resolveFromShs, DEFAULT_MAX_ARCHIVE_BYTES, DEFAULT_IDLE_TIMEOUT_MS } from './shs-load.js';
8
8
  import { mcpError } from './mcp-error.js';
9
- import { buildEvidenceReport, runFindings, toFindingsFilter, } from './evidence-report.js';
9
+ import {
10
+ buildEvidenceReport, runFindings, toFindingsFilter,
11
+
12
+ } from './evidence-report.js';
10
13
  import { computeWallClock } from './wall-clock.js';
11
14
  import { renderComparisonMarkdown, } from './run-comparison.js';
12
15
  import { buildComparisonOutput, } from './comparison-output.js';
13
16
  import { compileNormalizePatterns } from './stage-alignment.js';
14
- import { runOutputBlocks } from './run-output.js';
17
+ import { compileConfIgnorePatterns } from './comparability.js';
18
+ import { runOutputBlocks, } from './run-output.js';
15
19
  import { evaluateBudgets, } from './cli/budgets.js';
16
20
  import { assertStageBudgetMetric, combineRegressionBudgets } from './cli/regression-budgets.js';
17
21
  import { FINDING_NAMES, titleCase } from './finding-names.js';
@@ -20,7 +24,7 @@ import { DETECTORS, } from './detectors.js';
20
24
  import { tunedDetectors } from './threshold-overrides.js';
21
25
  import { tuningDocSlugForAnchor, pageForAnchor } from './docs-config.js';
22
26
  import { typeTag } from './format-utils.js';
23
-
27
+
24
28
 
25
29
 
26
30
 
@@ -53,7 +57,7 @@ import { typeTag } from './format-utils.js';
53
57
 
54
58
 
55
59
 
56
-
60
+
57
61
 
58
62
 
59
63
 
@@ -72,18 +76,70 @@ const CACHE_TTL_MS = envInt('SPARKFORENSICS_MCP_CACHE_TTL_MS', 15 * 60 * 1000);
72
76
 
73
77
  const byRunId = new Map (); // runId -> { appModel, cacheKey, lastAccess }
74
78
  const byCacheKey = new Map (); // cacheKey -> runId
79
+ // Built comparisons, keyed by both runs and every option that changes the output. Each entry follows
80
+ // its runs' TTL and eviction (deleteRunEntry), and the map is capped on its own: a comparison holds
81
+ // a row per paired stage, and 8 runs allow 56 ordered pairs.
82
+ const COMPARISON_CACHE_CAP = 16;
83
+
84
+ const comparisonCache = new Map ();
85
+ // Threshold overrides are one fixed object per server process (or DEFAULT for none): identity keys them.
86
+ const thresholdsIds = new WeakMap ();
87
+ let nextThresholdsId = 1;
88
+ function thresholdsKey(thresholds ) {
89
+ if (!thresholds) return 0;
90
+ let id = thresholdsIds.get(thresholds);
91
+ if (id === undefined) {
92
+ id = nextThresholdsId++;
93
+ thresholdsIds.set(thresholds, id);
94
+ }
95
+ return id;
96
+ }
97
+
75
98
  const pendingByCacheKey = new Map (); // cacheKey -> in-flight Promise<{runId, appModel}>
76
99
 
77
100
  export function pathCacheKey(path ) {
78
101
  const resolved = resolvePath(path);
79
102
  if (!existsSync(resolved)) throw mcpError('invalid-event-log', `No such file: ${resolved}`);
103
+ const stat = statSync(resolved);
104
+ // A rolling event-log directory grows by appending to its live events_N file, which leaves the
105
+ // directory's own mtime and size alone: the key covers the files inside it.
106
+ if (stat.isDirectory()) return `path:${resolved}:${directoryStatKey(resolved)}`;
80
107
  // Size narrows a same-millisecond mtime collision; ctime narrows the case where a copy tool
81
108
  // (rsync --preserve-times, tar) restores an identical mtime+size for different content: ctime
82
109
  // can't be set by the copying tool, so it still reflects when the file landed on disk.
83
- const { mtimeMs, ctimeMs, size } = statSync(resolved);
84
- return `path:${resolved}:${mtimeMs}:${ctimeMs}:${size}`;
110
+ return `path:${resolved}:${fileStatKey(stat)}`;
111
+ }
112
+
113
+ function fileStatKey({ mtimeMs, ctimeMs, size } ) {
114
+ return `${mtimeMs}:${ctimeMs}:${size}`;
85
115
  }
86
116
 
117
+ // The file count, newest mtime and ctime, and total size of the files under a directory (the
118
+ // parts of a rolling log, which Spark appends to in place): any part growing, arriving or going
119
+ // changes it.
120
+ function directoryStatKey(dir ) {
121
+ let files = 0, mtimeMs = 0, ctimeMs = 0, size = 0;
122
+ const walk = (path , depth ) => {
123
+ for (const entry of readdirSync(path, { withFileTypes: true })) {
124
+ const child = join(path, entry.name);
125
+ if (entry.isDirectory()) {
126
+ if (depth < ROLLING_LOG_DEPTH) walk(child, depth + 1);
127
+ continue;
128
+ }
129
+ const stat = statSync(child);
130
+ files++;
131
+ mtimeMs = Math.max(mtimeMs, stat.mtimeMs);
132
+ ctimeMs = Math.max(ctimeMs, stat.ctimeMs);
133
+ size += stat.size;
134
+ }
135
+ };
136
+ walk(dir, 0);
137
+ return `dir:${files}:${mtimeMs}:${ctimeMs}:${size}`;
138
+ }
139
+ // A rolling log's parts sit in the directory the path names, or one level down in its
140
+ // eventlog_v2_* subdirectory.
141
+ const ROLLING_LOG_DEPTH = 1;
142
+
87
143
  function shsCacheKey({ shsBaseUrl, appId, attemptId } ) {
88
144
  return `shs:${shsBaseUrl}:${appId}:${attemptId ?? ''}`;
89
145
  }
@@ -99,6 +155,10 @@ function touch(runId ) {
99
155
 
100
156
  function deleteRunEntry(id , entry ) {
101
157
  byRunId.delete(id);
158
+ // A comparison outlives neither of its runs: its entry goes with the first one evicted.
159
+ for (const [key, cached] of comparisonCache) {
160
+ if (cached.runIdA === id || cached.runIdB === id) comparisonCache.delete(key);
161
+ }
102
162
  if (byCacheKey.get(entry.cacheKey) === id) byCacheKey.delete(entry.cacheKey);
103
163
  }
104
164
 
@@ -121,6 +181,11 @@ function evictOverflow() {
121
181
  }
122
182
  }
123
183
 
184
+ /** How many built comparisons the cache holds: the occupancy the cap and eviction rules bound. */
185
+ export function comparisonCacheSize() {
186
+ return comparisonCache.size;
187
+ }
188
+
124
189
  export function getCachedAppModel(runId ) {
125
190
  evictStale();
126
191
  const entry = byRunId.get(runId);
@@ -166,6 +231,8 @@ export async function resolveOrCreateRun(
166
231
  const appModel = 'path' in source
167
232
  ? await resolveFromPath(source.path)
168
233
  : (await resolveFromShs(source.shsBaseUrl, source.appId, source.attemptId, { fetchImpl, maxArchiveBytes, idleTimeoutMs })).appModel;
234
+ // The attempt the caller asked the History Server for, when the log itself does not name one.
235
+ if (!('path' in source) && source.attemptId && appModel.app && !appModel.app.attemptId) appModel.app.attemptId = source.attemptId;
169
236
 
170
237
  const newRunId = randomUUID();
171
238
  byRunId.set(newRunId, { appModel, cacheKey, lastAccess: Date.now() });
@@ -180,30 +247,83 @@ export async function resolveOrCreateRun(
180
247
  return resolution;
181
248
  }
182
249
 
250
+ /** `compact`: an index, one short row per finding and no clean or not-run check lists. `full`: the
251
+ * report as the CLI writes it. */
252
+
253
+
254
+ /** A finding as `view: 'compact'` lists it: enough to rank it, name it and pair it across reruns.
255
+ * The rest of the row (recommendation, evidence, remediation, estimate) comes from
256
+ * `get_finding_evidence` by `id`. */
257
+
258
+
259
+
260
+
261
+
262
+
263
+
264
+
265
+ export function compactFindingRow(row ) {
266
+ return {
267
+ id: row.id, key: row.key, type: row.type, tag: row.tag, name: row.name,
268
+ impactBand: row.impactBand, stageId: row.stageId, actionLabel: row.actionLabel,
269
+ metric: row.metric ?? null, value: row.value ?? null,
270
+ ...(row.valueText !== undefined ? { valueText: row.valueText } : {}),
271
+ ...(row.impact !== undefined ? { impact: row.impact, impactMeaning: row.impactMeaning ?? null } : {}),
272
+ // Kept: a tuned finding's impact figure is uncalibrated, and a reader must see that on its row.
273
+ ...(row.tunedThresholds !== undefined ? { tunedThresholds: row.tunedThresholds } : {}),
274
+ };
275
+ }
276
+
277
+ /** A verdict step with its lead finding's row, which `findings` may not list under `maxFindings` or
278
+ * `minBand`. Null for a step whose lead has no id. */
279
+
280
+
281
+
282
+
283
+
284
+
285
+
286
+
287
+
288
+
289
+
290
+
291
+
292
+
293
+
294
+
183
295
  // `thresholds` on every analyzing tool below: the server's --thresholds overrides, fixed for the
184
296
  // process by createMcpServer(). A client can't set them per call; the dashboard never has them.
297
+ // `view` defaults to `full` here; the MCP tool defaults it to `compact`.
185
298
  export function diagnoseRun(runId , opts
186
299
 
187
-
188
- )
189
-
190
-
191
-
192
-
193
- {
300
+
301
+
302
+ ) {
194
303
  const appModel = getCachedAppModel(runId);
195
- const findingsFilter = toFindingsFilter(opts?.impactBand, opts?.type, opts?.stageId);
196
- const { json, markdown } = buildEvidenceReport(appModel, {
304
+ const view = opts?.view ?? 'full';
305
+ const findingsFilter = toFindingsFilter(opts?.impactBand, opts?.type, opts?.stageId, opts?.minBand, opts?.maxFindings);
306
+ const { json, markdown, findingsMatched } = buildEvidenceReport(appModel, {
197
307
  redact: opts?.redact, markdown: opts?.markdown, findingsFilter, thresholds: opts?.thresholds,
198
308
  });
309
+ // The lead rows come from the unfiltered report (cached, so this costs no second analysis).
310
+ const allRows = findingsFilter
311
+ ? buildEvidenceReport(appModel, { redact: opts?.redact, markdown: false, thresholds: opts?.thresholds }).json.findings
312
+ : json.findings;
313
+ const shape = (row ) => (view === 'compact' ? compactFindingRow(row) : row);
314
+ const steps = json.verdict.steps.map((step) => {
315
+ const lead = step.leadFindingId == null ? undefined : allRows.find((row) => row.id === step.leadFindingId && row.type === step.type);
316
+ return { ...step, leadFinding: lead ? shape(lead) : null };
317
+ });
199
318
  const include = opts?.include ?? [];
200
319
  const tuned = json.summary.tunedThresholds;
201
320
  return {
202
- runId, verdict: json.verdict, findings: json.findings, recommendations: json.recommendations, cleanChecks: json.cleanChecks,
203
- notRunChecks: json.notRunChecks,
321
+ runId, view, verdict: { ...json.verdict, steps }, findings: json.findings.map(shape), findingsOmitted: findingsMatched - json.findings.length,
322
+ recommendations: json.recommendations,
323
+ ...(view === 'full' ? { cleanChecks: json.cleanChecks, notRunChecks: json.notRunChecks } : {}),
204
324
  // The CLI report's own blocks, so a run reads the same through either surface.
205
325
  writeTargets: json.writeTargets,
206
- ...runOutputBlocks(appModel, { redact: opts?.redact, thresholds: opts?.thresholds }),
326
+ ...runOutputBlocks(appModel, { redact: opts?.redact, thresholds: opts?.thresholds, generator: opts?.generator }),
207
327
  runComplete: appModel.app?.endTime != null,
208
328
  // Top level too, so a client that never asks for `summary` still sees the run was tuned.
209
329
  ...(tuned ? { tunedThresholds: tuned } : {}),
@@ -214,6 +334,16 @@ export function diagnoseRun(runId , opts
214
334
  };
215
335
  }
216
336
 
337
+ /** The few fields `format: 'md'` returns as `structuredContent`: the Markdown carries the rest. */
338
+ export function diagnoseHeadline(result )
339
+
340
+ {
341
+ return {
342
+ runId: result.runId, runComplete: result.runComplete, title: result.verdict.title,
343
+ findingCount: result.findings.length, findingsOmitted: result.findingsOmitted, recommendationCount: result.recommendations.length,
344
+ };
345
+ }
346
+
217
347
  const DOCS_CONTENT_DIR = join(dirname(fileURLToPath(import.meta.url)), 'docs-content');
218
348
 
219
349
  // Both vendored corpora lead with their own heading line; strips the leading '#'/'###' markers, an
@@ -351,6 +481,7 @@ async function resolveAndAnalyze(
351
481
 
352
482
  export async function compareRuns(
353
483
  a , b , opts
484
+
354
485
  ,
355
486
  ) {
356
487
  // Before any log is read: a bad pattern refuses the call, as the CLI's usage error does.
@@ -359,6 +490,11 @@ export async function compareRuns(
359
490
  } catch (e) {
360
491
  throw mcpError('access-or-upstream-failure', `normalizePath: ${(e ).message}`);
361
492
  }
493
+ try {
494
+ compileConfIgnorePatterns(opts?.ignoreConf);
495
+ } catch (e) {
496
+ throw mcpError('access-or-upstream-failure', `ignoreConf: ${(e ).message}`);
497
+ }
362
498
  const [
363
499
  { runId: runIdA, appModel: appModelA, catalog: catalogA },
364
500
  { runId: runIdB, appModel: appModelB, catalog: catalogB },
@@ -368,11 +504,30 @@ export async function compareRuns(
368
504
  // interactive stage-detail drill-down, which none of compare/matchStages/metricDeltas/findingsDelta
369
505
  // read. Findings/metrics come from `catalog` and appModel.stages/sql, both fully populated. Produces
370
506
  // identical output to the dashboard; the MCP tool just never exposes per-task drill-down.
371
- const { comparison, output } = buildComparisonOutput(
372
- { label: runIdA, appModel: appModelA, catalog: catalogA },
373
- { label: runIdB, appModel: appModelB, catalog: catalogB },
374
- { redact: opts?.redact, normalizePath: opts?.normalizePath, view: opts?.include?.includes('stagePairs') ? 'full' : 'summary' },
375
- );
507
+ const view = opts?.include?.includes('stagePairs') ? 'full' : 'summary';
508
+ const cacheKey = JSON.stringify([runIdA, runIdB, thresholdsKey(opts?.thresholds), !!opts?.redact, view, opts?.normalizePath ?? [], opts?.ignoreConf ?? [], opts?.noiseBand ?? null, opts?.generator ?? null]);
509
+ let built = comparisonCache.get(cacheKey)?.built;
510
+ if (built) {
511
+ // Re-insert so the oldest entry is the least recently used.
512
+ comparisonCache.delete(cacheKey);
513
+ comparisonCache.set(cacheKey, { runIdA, runIdB, built });
514
+ } else {
515
+ built = buildComparisonOutput(
516
+ { label: runIdA, appModel: appModelA, catalog: catalogA },
517
+ { label: runIdB, appModel: appModelB, catalog: catalogB },
518
+ { redact: opts?.redact, normalizePath: opts?.normalizePath, ignoreConf: opts?.ignoreConf, noiseBand: opts?.noiseBand, view, generator: opts?.generator },
519
+ );
520
+ // Either run can be evicted while the other one is still being read or parsed (the awaits
521
+ // above): an entry for a run that is gone would outlive its eviction, so it is only kept
522
+ // while both are cached.
523
+ if (byRunId.has(runIdA) && byRunId.has(runIdB)) {
524
+ comparisonCache.set(cacheKey, { runIdA, runIdB, built });
525
+ while (comparisonCache.size > COMPARISON_CACHE_CAP) comparisonCache.delete(comparisonCache.keys().next().value );
526
+ }
527
+ }
528
+ const { comparison } = built;
529
+ // A copy, so a caller that changes the result cannot change what the next caller is served.
530
+ const output = structuredClone(built.output);
376
531
 
377
532
  return {
378
533
  runIdA,
@@ -1,5 +1,5 @@
1
- // The textbook median of values already sorted ascending: the middle value for an odd count, the
2
- // mean of the two middle values for an even one. 0 for an empty list.
1
+ // Median of values already sorted ascending: the mean of the two middle values on an even count.
2
+ // 0 for an empty list.
3
3
  export function medianOfSorted(sorted ) {
4
4
  const n = sorted.length;
5
5
  if (n === 0) return 0;
@@ -0,0 +1,73 @@
1
+ // Reads a nested-loop join's operator, join type and condition from its plan line, and the row
2
+ // counts around it from the executors' "number of output rows" metric.
3
+ //
4
+ // Spark plans a join with no equi-join keys as a BroadcastNestedLoopJoin when one side can be
5
+ // broadcast, else a CartesianProduct for an inner-like join (JoinSelection in
6
+ // sql/core/src/main/scala/org/apache/spark/sql/execution/SparkStrategies.scala, v3.5.0). Both
7
+ // compare every left row with every right row.
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+ const OUTPUT_ROWS = 'number of output rows';
21
+
22
+ // BroadcastNestedLoopJoin <BuildLeft|BuildRight>, <JoinType>[, <condition>]
23
+ const BNLJ_LINE = /^BroadcastNestedLoopJoin\s+Build(?:Left|Right),\s*(\w+)(?:,\s*([\s\S]+))?$/;
24
+ // CartesianProduct[ <condition>]: the operator only plans inner-like joins and prints no type.
25
+ const CARTESIAN_LINE = /^CartesianProduct(?:\s+([\s\S]+))?$/;
26
+
27
+ function cleanCondition(raw ) {
28
+ const text = (raw ?? '').replace(/#\d+L?/g, '').trim();
29
+ return text.length > 0 ? text : null;
30
+ }
31
+
32
+ /** The nested-loop operator, join type and condition behind a plan node, or null for any other node. */
33
+ export function parseNestedLoopJoin(node ) {
34
+ const detail = (node.detail ?? '').trim();
35
+ if (node.name === 'BroadcastNestedLoopJoin') {
36
+ const m = detail.match(BNLJ_LINE);
37
+ return { operator: 'BroadcastNestedLoopJoin', joinType: m?.[1] ?? null, condition: cleanCondition(m?.[2]) };
38
+ }
39
+ if (node.name === 'CartesianProduct') {
40
+ const m = detail.match(CARTESIAN_LINE);
41
+ return { operator: 'CartesianProduct', joinType: 'Inner', condition: cleanCondition(m?.[1]) };
42
+ }
43
+ return null;
44
+ }
45
+
46
+ // Operators that pass every row of their one child through, so the child's row count is theirs.
47
+ // A node outside this list that reports no row metric (Expand, Union, Limit...) can change the
48
+ // count, so the walk stops there rather than guess.
49
+ const ROW_PRESERVING = new Set([
50
+ 'WholeStageCodegen', 'InputAdapter', 'Project', 'Sort', 'Exchange', 'BroadcastExchange',
51
+ 'ShuffleQueryStage', 'BroadcastQueryStage', 'AQEShuffleRead', 'CustomShuffleReader',
52
+ 'ColumnarToRow', 'AdaptiveSparkPlan',
53
+ ]);
54
+
55
+ /** The rows a subtree produced: the nearest "number of output rows" at or below `node`, walking
56
+ * through row-preserving single-child operators. Null when the log reports none for it. */
57
+ export function outputRowsOf(node ) {
58
+ let current = node;
59
+ while (current) {
60
+ const metric = current.metrics?.find((m) => m.name === OUTPUT_ROWS);
61
+ if (metric) return metric.value;
62
+ const preserving = ROW_PRESERVING.has(current.name) || /^WholeStageCodegen/.test(current.name);
63
+ if (!preserving || current.children.length !== 1) return null;
64
+ current = current.children[0];
65
+ }
66
+ return null;
67
+ }
68
+
69
+ /** The node's own "number of output rows" value, null when absent. */
70
+ export function ownOutputRows(node ) {
71
+ const metric = node.metrics?.find((m) => m.name === OUTPUT_ROWS);
72
+ return metric ? metric.value : null;
73
+ }
@@ -193,8 +193,7 @@ export function stragglerFixLongestTaskMs(stage ) {
193
193
 
194
194
 
195
195
 
196
-
197
-
196
+
198
197
 
199
198
 
200
199
 
@@ -251,7 +251,7 @@ export async function runParseFiles(
251
251
  emitParseCompletion(state, emit, linesProcessed);
252
252
  }
253
253
 
254
- // ─── Worker message bus (only active when running as a Web Worker) ──────────────
254
+ // Message bus, active only when running as a Web Worker.
255
255
 
256
256
  const isWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
257
257
 
@@ -1,11 +1,11 @@
1
1
  // Flattens a resolved planTree into a { nodes, edges } graph shape for the
2
2
  // React Flow + Dagre plan graph view ("Plan graph view" in
3
3
  // docs-site/contributor-guide/architecture/drill-down/plan-graph.md). The Exchange
4
- // write/read split is done upstream now, in resolvePlanTree
5
- // (event-handlers.ts): this file just walks the already-split tree and maps
6
- // each real PlanNode onto a PlanGraphNodeData. sourceNodeId still lets a
7
- // write half point back to its read half's id, so aggregate logic over
8
- // nodes can group a pair without special-casing exchangeRole everywhere.
4
+ // write/read split happens upstream in resolvePlanTree (event-handlers.ts):
5
+ // this file walks the already-split tree and maps each PlanNode onto a
6
+ // PlanGraphNodeData. sourceNodeId lets a write half point back to its read
7
+ // half's id, so aggregate logic can group a pair without special-casing
8
+ // exchangeRole.
9
9
  import { walkPlanTree } from './plan-tree-walk.js';
10
10
  import {
11
11
  classifyNode,
@@ -82,15 +82,9 @@ export function buildPlanGraphModel(
82
82
 
83
83
  const durationMap =
84
84
  sqlExec && sqlExecutionId != null ? buildDurationMap(planTree, appModel, sqlExec, sqlExecutionId, durationMode) : null;
85
- // A second, always-exclusive map, independent of the requested `durationMode`.
86
- // Exclusive shares are a true, non-overlapping partition of each paired
87
- // stage's wall time (computeExclusiveSharesForPairs), so their sum is a
88
- // stable normalization total. PlanGraphCanvas needs this alongside
89
- // `durationMap` (the mode-selected values actually shown) because summing
90
- // `durationMap` itself would double/triple-count under 'inclusive' mode,
91
- // where an ancestor's share already folds in every descendant's share.
92
- // durationMode === 'exclusive' already computed the same map above; only
93
- // redo the work when the requested mode actually differs.
85
+ // Always-exclusive map, independent of `durationMode`: the stable total a
86
+ // percentage denominator sums (see PlanGraphNodeData.exclusiveDurationShare).
87
+ // Reuses `durationMap` when the requested mode is already exclusive.
94
88
  const exclusiveDurationMap =
95
89
  durationMode === 'exclusive'
96
90
  ? durationMap
@@ -110,14 +104,12 @@ export function buildPlanGraphModel(
110
104
  }
111
105
  }
112
106
 
113
- // resolvePlanTree (event-handlers.ts) now prefixes every id with its owning
114
- // execution, but this filter stays as defense in depth: a finding is scoped
115
- // to the execution whose graph is being built before it's indexed by node
116
- // id, so a stale/synthetic finding whose planNodeIds happen to collide with
117
- // this tree's ids (e.g. hand-built test fixtures, or data from before the
118
- // prefix existed) still can't badge onto the wrong tree. No linked SQL
119
- // execution means no findings attach, matching the sqlExec-falsy behavior
120
- // used elsewhere in this function (durationMap, linkedStageIds).
107
+ // resolvePlanTree (event-handlers.ts) prefixes every id with its owning
108
+ // execution, but a finding is also scoped to the execution whose graph is
109
+ // being built, as defense in depth: a synthetic finding whose planNodeIds
110
+ // collide with this tree's ids (e.g. hand-built test fixtures) cannot badge
111
+ // onto the wrong tree. No linked SQL execution means no findings attach,
112
+ // like durationMap and linkedStageIds above.
121
113
  const findingsByNodeId = new Map ();
122
114
  if (sqlExecutionId != null) {
123
115
  for (const finding of findings) {
@@ -164,9 +164,8 @@ export function buildDurationMap(
164
164
  const s = appModel.stages.get(id);
165
165
  if (s) stagesById.set(id, { submittedAt: s.submittedAt, completedAt: s.completedAt });
166
166
  }
167
- // attributeStageDurationToPlan reads its 3rd arg's own `.stageIds` field;
168
- // override it with the derived array rather than the (always-empty on real
169
- // data) one already on `sqlExec`.
167
+ // attributeStageDurationToPlan reads `.stageIds`; use the derived array, not sqlExec's own
168
+ // (see contributor-guide/architecture/detector-contract/plan-attribution.md).
170
169
  return durationMode === 'inclusive'
171
170
  ? attributeStageDurationToPlanInclusive(planTree, stagesById, { ...sqlExec, stageIds })
172
171
  : attributeStageDurationToPlan(planTree, stagesById, { ...sqlExec, stageIds });