sparkforensics-server 0.2.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (399) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -1
  3. package/index.js +5 -4
  4. package/package.json +1 -1
  5. package/public/assets/AutoscalingChurn-CUSIvvab.js +1 -0
  6. package/public/assets/CacheUtilization-Ce0K6_X2.js +1 -0
  7. package/public/assets/CachingOpportunity-Cn-LmRA4.js +1 -0
  8. package/public/assets/ColdStart-BoiYzEyV.js +1 -0
  9. package/public/assets/ConfigAudit-BoJc6PAw.js +1 -0
  10. package/public/assets/CoreUsageArea-BFfXKu7K.js +1 -0
  11. package/public/assets/Dashboard-Bo_1jGqH.js +2 -0
  12. package/public/assets/DisclosureContext-LYzHroHO.js +1 -0
  13. package/public/assets/DuplicatePlanSubtree-BaAuGAoo.js +1 -0
  14. package/public/assets/ExecutorUtilization-OE6VMFkG.js +1 -0
  15. package/public/assets/GcPressure-AoL_ZNrV.js +1 -0
  16. package/public/assets/IncompleteRun-Bv6IzgsQ.js +1 -0
  17. package/public/assets/JobFailures-DF3U2z1f.js +1 -0
  18. package/public/assets/MemoryUtilization-B65OVZlq.js +1 -0
  19. package/public/assets/OverBroadcast-DXbJREDl.js +1 -0
  20. package/public/assets/PartitionSizing-DZLnjy0d.js +1 -0
  21. package/public/assets/PlanExplorer-DdmPkKO1.js +1 -0
  22. package/public/assets/PlanGraphRoute-C-BGmC6j.js +1 -0
  23. package/public/assets/RetryWaste-BbEPsdMw.js +1 -0
  24. package/public/assets/ShuffleIO-wZB94LS8.js +1 -0
  25. package/public/assets/Skew-SpQE7zeJ.js +1 -0
  26. package/public/assets/SlowHost-DwDBCTvr.js +1 -0
  27. package/public/assets/SmallFiles-yjY_6sT1.js +1 -0
  28. package/public/assets/SpeculationWaste-73PDmj0c.js +1 -0
  29. package/public/assets/Spill-DEw0Kd1i.js +1 -0
  30. package/public/assets/StageFailed-BEfXdEJc.js +1 -0
  31. package/public/assets/StageFindingGroup-Bzm2TY03.js +1 -0
  32. package/public/assets/StageHeader-BSV8ALhT.js +1 -0
  33. package/public/assets/StageShape-CzR4jHOt.js +1 -0
  34. package/public/assets/StageSlowness-vMPsnAaa.js +1 -0
  35. package/public/assets/Straggler-Cf__OGl5.js +1 -0
  36. package/public/assets/TaskFailures-BZ8kLZiO.js +1 -0
  37. package/public/assets/{TinyTask-DYXks5sT.js → TinyTask-CIPRxWfT.js} +1 -1
  38. package/public/assets/UnderBroadcast-CBm8ZPoS.js +1 -0
  39. package/public/assets/{finding-anchor-CDbHtqKT.js → finding-anchor-DF1cSHO1.js} +1 -1
  40. package/public/assets/impact-sort-7z0ZcBZf.js +1 -0
  41. package/public/assets/index-BcqTLKFp.css +2 -0
  42. package/public/assets/index-CEM9bz_x.js +13 -0
  43. package/public/assets/parser-worker-Q5KCPdKT.js +122 -0
  44. package/public/assets/react-vendor-BAKt3vny.js +9 -0
  45. package/public/assets/select-Cy5pmbrh.js +1 -0
  46. package/public/assets/usePagedRows-BjPOpUU8.js +1 -0
  47. package/public/assets/useRecentFiles-BrBl-AP1.js +11 -0
  48. package/public/assets/useSortMode-CU0vzThu.js +1 -0
  49. package/public/docs/404.html +2 -2
  50. package/public/docs/assets/{app.DsFQcynM.js → app.C6w47a9W.js} +1 -1
  51. package/public/docs/assets/chunks/@localSearchIndexroot.DbZy5EtM.js +1 -0
  52. package/public/docs/assets/chunks/{VPLocalSearchBox.oa8NbaiV.js → VPLocalSearchBox.BcTNvT55.js} +1 -1
  53. package/public/docs/assets/chunks/{theme.Ca4dR9dL.js → theme.D-mE6j02.js} +2 -2
  54. package/public/docs/assets/{contributor-guide_architecture_board-widgets.md.9ToZXcTk.js → contributor-guide_architecture_board-widgets.md.VTcYTH4d.js} +1 -1
  55. package/public/docs/assets/contributor-guide_architecture_detector-contract.md.CsqZHz_T.js +1 -0
  56. package/public/docs/assets/{contributor-guide_architecture_detector-contract.md.aQQfP7pj.lean.js → contributor-guide_architecture_detector-contract.md.CsqZHz_T.lean.js} +1 -1
  57. package/public/docs/assets/contributor-guide_architecture_drill-down.md.CFNCE8JB.js +1 -0
  58. package/public/docs/assets/{contributor-guide_architecture_drill-down.md.Bawx6NgP.lean.js → contributor-guide_architecture_drill-down.md.CFNCE8JB.lean.js} +1 -1
  59. package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.HT9IJGIl.js +1 -0
  60. package/public/docs/assets/{contributor-guide_architecture_impact-estimation.md.sf9DU2bA.lean.js → contributor-guide_architecture_impact-estimation.md.HT9IJGIl.lean.js} +1 -1
  61. package/public/docs/assets/contributor-guide_architecture_overview.md.CTEqVeV4.js +1 -0
  62. package/public/docs/assets/contributor-guide_architecture_overview.md.CTEqVeV4.lean.js +1 -0
  63. package/public/docs/assets/contributor-guide_architecture_state-and-history.md.DOTVHjTV.js +1 -0
  64. package/public/docs/assets/{contributor-guide_architecture_state-and-history.md.C-xxn0q7.lean.js → contributor-guide_architecture_state-and-history.md.DOTVHjTV.lean.js} +1 -1
  65. package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.BmpARZve.js +1 -0
  66. package/public/docs/assets/{contributor-guide_architecture_widget-rendering.md.DYwFeyjV.lean.js → contributor-guide_architecture_widget-rendering.md.BmpARZve.lean.js} +1 -1
  67. package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.21pTYhFF.js +6 -0
  68. package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.21pTYhFF.lean.js +1 -0
  69. package/public/docs/assets/contributor-guide_contributing.md.B2E9ni79.js +1 -0
  70. package/public/docs/assets/contributor-guide_contributing.md.B2E9ni79.lean.js +1 -0
  71. package/public/docs/assets/contributor-guide_development-setup.md.CjgZJwjK.js +15 -0
  72. package/public/docs/assets/{contributor-guide_development-setup.md.DvMV1xhQ.lean.js → contributor-guide_development-setup.md.CjgZJwjK.lean.js} +1 -1
  73. package/public/docs/assets/contributor-guide_testing.md.DTwhpoN2.js +1 -0
  74. package/public/docs/assets/contributor-guide_testing.md.DTwhpoN2.lean.js +1 -0
  75. package/public/docs/assets/tuning-reference_anti-patterns.md.D08Uaod7.js +1 -0
  76. package/public/docs/assets/tuning-reference_anti-patterns.md.D08Uaod7.lean.js +1 -0
  77. package/public/docs/assets/tuning-reference_aqe.md.C1Ae6A8u.js +1 -0
  78. package/public/docs/assets/tuning-reference_aqe.md.C1Ae6A8u.lean.js +1 -0
  79. package/public/docs/assets/tuning-reference_bottleneck-broadcast-sizing.md.6wjKuJNE.js +1 -0
  80. package/public/docs/assets/tuning-reference_bottleneck-broadcast-sizing.md.6wjKuJNE.lean.js +1 -0
  81. package/public/docs/assets/tuning-reference_bottleneck-cold-start.md.BVQbsuHP.js +7 -0
  82. package/public/docs/assets/tuning-reference_bottleneck-cold-start.md.BVQbsuHP.lean.js +1 -0
  83. package/public/docs/assets/tuning-reference_bottleneck-duplicate-plan-subtree.md.DtaYnaiF.js +1 -0
  84. package/public/docs/assets/tuning-reference_bottleneck-duplicate-plan-subtree.md.DtaYnaiF.lean.js +1 -0
  85. package/public/docs/assets/{tuning-reference_bottleneck-failures.md.CAsFf3RR.js → tuning-reference_bottleneck-failures.md.Z1Tdmdd3.js} +2 -2
  86. package/public/docs/assets/tuning-reference_bottleneck-failures.md.Z1Tdmdd3.lean.js +1 -0
  87. package/public/docs/assets/tuning-reference_bottleneck-gc.md.BCnXLf1n.js +6 -0
  88. package/public/docs/assets/tuning-reference_bottleneck-gc.md.BCnXLf1n.lean.js +1 -0
  89. package/public/docs/assets/tuning-reference_bottleneck-job-failure-rate.md.CcvQj2g9.js +8 -0
  90. package/public/docs/assets/tuning-reference_bottleneck-job-failure-rate.md.CcvQj2g9.lean.js +1 -0
  91. package/public/docs/assets/tuning-reference_bottleneck-memory-utilization.md.B-C0-8h-.js +7 -0
  92. package/public/docs/assets/tuning-reference_bottleneck-memory-utilization.md.B-C0-8h-.lean.js +1 -0
  93. package/public/docs/assets/tuning-reference_bottleneck-retry-waste.md.DCnhWiqj.js +1 -0
  94. package/public/docs/assets/tuning-reference_bottleneck-retry-waste.md.DCnhWiqj.lean.js +1 -0
  95. package/public/docs/assets/{tuning-reference_bottleneck-shuffle.md.DDMQVOW6.js → tuning-reference_bottleneck-shuffle.md.B5OMJCd3.js} +2 -2
  96. package/public/docs/assets/tuning-reference_bottleneck-shuffle.md.B5OMJCd3.lean.js +1 -0
  97. package/public/docs/assets/{tuning-reference_bottleneck-skew.md.B0MzhWcX.js → tuning-reference_bottleneck-skew.md.Cvo6NQm_.js} +1 -1
  98. package/public/docs/assets/tuning-reference_bottleneck-skew.md.Cvo6NQm_.lean.js +1 -0
  99. package/public/docs/assets/{tuning-reference_bottleneck-slow-host.md.BlIo6UDW.js → tuning-reference_bottleneck-slow-host.md.ja5qr3wq.js} +1 -1
  100. package/public/docs/assets/tuning-reference_bottleneck-slow-host.md.ja5qr3wq.lean.js +1 -0
  101. package/public/docs/assets/tuning-reference_bottleneck-small-files.md.Crjtb0i2.js +5 -0
  102. package/public/docs/assets/tuning-reference_bottleneck-small-files.md.Crjtb0i2.lean.js +1 -0
  103. package/public/docs/assets/tuning-reference_bottleneck-spill.md.Bl8KtSRg.js +6 -0
  104. package/public/docs/assets/tuning-reference_bottleneck-spill.md.Bl8KtSRg.lean.js +1 -0
  105. package/public/docs/assets/tuning-reference_bottleneck-straggler.md.B0VN8reT.js +7 -0
  106. package/public/docs/assets/tuning-reference_bottleneck-straggler.md.B0VN8reT.lean.js +1 -0
  107. package/public/docs/assets/tuning-reference_bottleneck-tiny-tasks.md.DQCftGhu.js +8 -0
  108. package/public/docs/assets/tuning-reference_bottleneck-tiny-tasks.md.DQCftGhu.lean.js +1 -0
  109. package/public/docs/assets/{tuning-reference_bottleneck-utilization.md.DqC_iGkw.js → tuning-reference_bottleneck-utilization.md.BXDl30f9.js} +1 -1
  110. package/public/docs/assets/tuning-reference_bottleneck-utilization.md.BXDl30f9.lean.js +1 -0
  111. package/public/docs/assets/tuning-reference_caching.md.CcaiMn_7.js +1 -0
  112. package/public/docs/assets/tuning-reference_caching.md.CcaiMn_7.lean.js +1 -0
  113. package/public/docs/assets/tuning-reference_cluster-config.md.DbWG1yZq.js +1 -0
  114. package/public/docs/assets/tuning-reference_cluster-config.md.DbWG1yZq.lean.js +1 -0
  115. package/public/docs/assets/tuning-reference_config.md.YUqSpwFB.js +1 -0
  116. package/public/docs/assets/tuning-reference_config.md.YUqSpwFB.lean.js +1 -0
  117. package/public/docs/assets/tuning-reference_data-formats.md.C5DX5xL4.js +1 -0
  118. package/public/docs/assets/tuning-reference_data-formats.md.C5DX5xL4.lean.js +1 -0
  119. package/public/docs/assets/tuning-reference_index.md.ZVIpQ6Dk.js +1 -0
  120. package/public/docs/assets/tuning-reference_index.md.ZVIpQ6Dk.lean.js +1 -0
  121. package/public/docs/assets/tuning-reference_intro.md.BSSaDlqx.js +1 -0
  122. package/public/docs/assets/tuning-reference_intro.md.BSSaDlqx.lean.js +1 -0
  123. package/public/docs/assets/tuning-reference_joins.md.BVhNCDnE.js +1 -0
  124. package/public/docs/assets/tuning-reference_joins.md.BVhNCDnE.lean.js +1 -0
  125. package/public/docs/assets/tuning-reference_memory-model.md.CfjFdgbl.js +1 -0
  126. package/public/docs/assets/tuning-reference_memory-model.md.CfjFdgbl.lean.js +1 -0
  127. package/public/docs/assets/tuning-reference_metrics.md.C64VNEL4.js +1 -0
  128. package/public/docs/assets/tuning-reference_metrics.md.C64VNEL4.lean.js +1 -0
  129. package/public/docs/assets/tuning-reference_partitioning.md.CtLK0xsS.js +1 -0
  130. package/public/docs/assets/tuning-reference_partitioning.md.CtLK0xsS.lean.js +1 -0
  131. package/public/docs/assets/tuning-reference_pyspark.md.D-92XgF8.js +6 -0
  132. package/public/docs/assets/tuning-reference_pyspark.md.D-92XgF8.lean.js +1 -0
  133. package/public/docs/assets/tuning-reference_shuffle.md.Iq2v1oHJ.js +1 -0
  134. package/public/docs/assets/tuning-reference_shuffle.md.Iq2v1oHJ.lean.js +1 -0
  135. package/public/docs/assets/tuning-reference_spark-architecture.md._qwnE_Sc.js +1 -0
  136. package/public/docs/assets/tuning-reference_spark-architecture.md._qwnE_Sc.lean.js +1 -0
  137. package/public/docs/assets/tuning-reference_table-formats.md.BSYEK-eN.js +1 -0
  138. package/public/docs/assets/tuning-reference_table-formats.md.BSYEK-eN.lean.js +1 -0
  139. package/public/docs/assets/{user-guide_alternative-log-retrieval.md.B4tPGIal.js → user-guide_alternative-log-retrieval.md.ZHPSbBkF.js} +1 -1
  140. package/public/docs/assets/{user-guide_alternative-log-retrieval.md.B4tPGIal.lean.js → user-guide_alternative-log-retrieval.md.ZHPSbBkF.lean.js} +1 -1
  141. package/public/docs/assets/user-guide_getting-started.md.CEa2ZX-l.js +6 -0
  142. package/public/docs/assets/user-guide_getting-started.md.CEa2ZX-l.lean.js +1 -0
  143. package/public/docs/assets/{user-guide_mcp-tools.md.DGvolGzt.js → user-guide_mcp-tools.md.dQwIvdTF.js} +13 -7
  144. package/public/docs/assets/user-guide_mcp-tools.md.dQwIvdTF.lean.js +1 -0
  145. package/public/docs/assets/user-guide_run-comparison.md.ChLI3MK0.js +1 -0
  146. package/public/docs/assets/user-guide_run-comparison.md.ChLI3MK0.lean.js +1 -0
  147. package/public/docs/assets/user-guide_understanding-findings.md.D1ccs60C.js +1 -0
  148. package/public/docs/assets/{user-guide_understanding-findings.md.B29f0ucS.lean.js → user-guide_understanding-findings.md.D1ccs60C.lean.js} +1 -1
  149. package/public/docs/contributor-guide/architecture/board-widgets.html +5 -5
  150. package/public/docs/contributor-guide/architecture/detector-contract.html +5 -5
  151. package/public/docs/contributor-guide/architecture/drill-down.html +5 -5
  152. package/public/docs/contributor-guide/architecture/impact-estimation.html +5 -5
  153. package/public/docs/contributor-guide/architecture/index.html +3 -3
  154. package/public/docs/contributor-guide/architecture/overview.html +5 -5
  155. package/public/docs/contributor-guide/architecture/state-and-history.html +5 -5
  156. package/public/docs/contributor-guide/architecture/widget-rendering.html +5 -5
  157. package/public/docs/contributor-guide/architecture/worker-protocol.html +6 -6
  158. package/public/docs/contributor-guide/contributing.html +5 -5
  159. package/public/docs/contributor-guide/development-setup.html +13 -10
  160. package/public/docs/contributor-guide/testing.html +5 -5
  161. package/public/docs/hashmap.json +1 -1
  162. package/public/docs/index.html +3 -3
  163. package/public/docs/tuning-reference/anti-patterns.html +6 -6
  164. package/public/docs/tuning-reference/aqe.html +6 -6
  165. package/public/docs/tuning-reference/bottleneck-broadcast-sizing.html +6 -6
  166. package/public/docs/tuning-reference/bottleneck-cold-start.html +6 -6
  167. package/public/docs/tuning-reference/bottleneck-duplicate-plan-subtree.html +6 -6
  168. package/public/docs/tuning-reference/bottleneck-failures.html +6 -6
  169. package/public/docs/tuning-reference/bottleneck-gc.html +6 -6
  170. package/public/docs/tuning-reference/bottleneck-job-failure-rate.html +6 -6
  171. package/public/docs/tuning-reference/bottleneck-memory-utilization.html +6 -6
  172. package/public/docs/tuning-reference/bottleneck-retry-waste.html +6 -6
  173. package/public/docs/tuning-reference/bottleneck-shuffle.html +6 -6
  174. package/public/docs/tuning-reference/bottleneck-skew.html +6 -6
  175. package/public/docs/tuning-reference/bottleneck-slow-host.html +6 -6
  176. package/public/docs/tuning-reference/bottleneck-small-files.html +6 -6
  177. package/public/docs/tuning-reference/bottleneck-spill.html +6 -6
  178. package/public/docs/tuning-reference/bottleneck-straggler.html +6 -6
  179. package/public/docs/tuning-reference/bottleneck-tiny-tasks.html +6 -6
  180. package/public/docs/tuning-reference/bottleneck-utilization.html +6 -6
  181. package/public/docs/tuning-reference/caching.html +6 -6
  182. package/public/docs/tuning-reference/cluster-config.html +6 -6
  183. package/public/docs/tuning-reference/config.html +6 -6
  184. package/public/docs/tuning-reference/data-formats.html +6 -6
  185. package/public/docs/tuning-reference/index.html +6 -6
  186. package/public/docs/tuning-reference/intro.html +6 -6
  187. package/public/docs/tuning-reference/joins.html +6 -6
  188. package/public/docs/tuning-reference/memory-model.html +6 -6
  189. package/public/docs/tuning-reference/metrics.html +6 -6
  190. package/public/docs/tuning-reference/partitioning.html +6 -6
  191. package/public/docs/tuning-reference/pyspark.html +6 -6
  192. package/public/docs/tuning-reference/shuffle.html +6 -6
  193. package/public/docs/tuning-reference/spark-architecture.html +6 -6
  194. package/public/docs/tuning-reference/table-formats.html +6 -6
  195. package/public/docs/user-guide/alternative-log-retrieval.html +5 -5
  196. package/public/docs/user-guide/getting-started.html +9 -6
  197. package/public/docs/user-guide/mcp-tools.html +15 -9
  198. package/public/docs/user-guide/run-comparison.html +5 -5
  199. package/public/docs/user-guide/understanding-findings.html +5 -5
  200. package/public/export-template.html +111 -0
  201. package/public/index.html +4 -3
  202. package/vendor-core/analyzer.js +156 -48
  203. package/vendor-core/check-coverage.js +88 -0
  204. package/vendor-core/cli/budgets.js +31 -18
  205. package/vendor-core/cli/collect-run.js +76 -31
  206. package/vendor-core/cli/native-zstd.js +2 -2
  207. package/vendor-core/cli/threshold-config.js +28 -0
  208. package/vendor-core/comparison-verdict.js +177 -0
  209. package/vendor-core/core-source-hash.txt +1 -0
  210. package/vendor-core/core-usage-locality.js +56 -2
  211. package/vendor-core/detector-docs.js +58 -0
  212. package/vendor-core/detectors.js +933 -459
  213. package/vendor-core/docs-config.js +0 -36
  214. package/vendor-core/docs-content/chapters/nav-index.json +31 -0
  215. package/vendor-core/docs-content/detection/cstor.md +9 -0
  216. package/vendor-core/docs-content/detection/fail.md +6 -2
  217. package/vendor-core/docs-site-config.js +3 -0
  218. package/vendor-core/event-handlers.js +191 -6
  219. package/vendor-core/event-schemas.js +29 -0
  220. package/vendor-core/evidence-report.js +440 -112
  221. package/vendor-core/export-data.js +79 -6
  222. package/vendor-core/finding-action-label.js +9 -88
  223. package/vendor-core/finding-filter-predicate.js +9 -0
  224. package/vendor-core/finding-generic-recommendation.js +6 -104
  225. package/vendor-core/finding-names.js +21 -45
  226. package/vendor-core/finding-presentation.js +333 -0
  227. package/vendor-core/finding-tag-help.js +110 -0
  228. package/vendor-core/finding-types.js +361 -0
  229. package/vendor-core/findings-of-type.js +11 -0
  230. package/vendor-core/format-utils.js +92 -27
  231. package/vendor-core/html-export.js +51 -0
  232. package/vendor-core/impact-band.js +21 -8
  233. package/vendor-core/impact-estimator.js +8 -521
  234. package/vendor-core/impact-format.js +114 -0
  235. package/vendor-core/impact-model.js +175 -0
  236. package/vendor-core/ingest.js +2 -0
  237. package/vendor-core/intervals.js +13 -0
  238. package/vendor-core/list-runs.js +2 -3
  239. package/vendor-core/load-vendored.js +70 -5
  240. package/vendor-core/mcp-server-factory.js +14 -10
  241. package/vendor-core/mcp-tools.js +105 -45
  242. package/vendor-core/model-assembler.js +12 -0
  243. package/vendor-core/occupancy.js +1 -1
  244. package/vendor-core/parser-worker.js +22 -5
  245. package/vendor-core/plan-graph-model.js +3 -2
  246. package/vendor-core/plan-node-detail.js +1 -1
  247. package/vendor-core/recommendation-rollup.js +63 -3
  248. package/vendor-core/redact.js +68 -28
  249. package/vendor-core/run-comparison.js +40 -7
  250. package/vendor-core/run-interpretation.js +290 -0
  251. package/vendor-core/run-outcome.js +74 -0
  252. package/vendor-core/run-payload.js +17 -0
  253. package/vendor-core/run-shape.js +40 -0
  254. package/vendor-core/run-verdict.js +353 -0
  255. package/vendor-core/scaling-sim.js +4 -5
  256. package/vendor-core/scorecard-estimates.js +62 -0
  257. package/vendor-core/shs-fetch.js +175 -65
  258. package/vendor-core/shs-load.js +1 -1
  259. package/vendor-core/sql-stages.js +11 -0
  260. package/vendor-core/stage-quantiles.js +14 -0
  261. package/vendor-core/task-failure.js +151 -0
  262. package/vendor-core/threshold-overrides.js +160 -0
  263. package/vendor-core/threshold-summary.js +11 -33
  264. package/vendor-core/types.js +6 -42
  265. package/vendor-core/vendor/fflate.js +1 -1
  266. package/vendor-core/wall-clock.js +1 -12
  267. package/vendor-core/wasted-core-hours.js +2 -2
  268. package/vendor-core/zip-archive.js +167 -0
  269. package/public/assets/AutoscalingChurn-BOiUUfuY.js +0 -1
  270. package/public/assets/CacheUtilization-DwX7lisd.js +0 -1
  271. package/public/assets/CachingOpportunity-DUE3GKpx.js +0 -1
  272. package/public/assets/ColdStart-ZHxPrJmI.js +0 -1
  273. package/public/assets/ConfigAudit-BkVnq2XE.js +0 -1
  274. package/public/assets/CoreUsageArea-DobrSXk2.js +0 -1
  275. package/public/assets/Dashboard-y3_9iQJo.js +0 -1
  276. package/public/assets/DuplicatePlanSubtree-BSQZFeNq.js +0 -1
  277. package/public/assets/DurationHistogram-DlQv4ZCB.js +0 -1
  278. package/public/assets/EmptyState-DqnSxTjJ.js +0 -1
  279. package/public/assets/ExecutorUtilization-CuQUxaCd.js +0 -1
  280. package/public/assets/GcPressure-CLT5hkMJ.js +0 -1
  281. package/public/assets/ImpactEstimate-Df_laG-n.js +0 -1
  282. package/public/assets/IncompleteRun-CpIkS4XZ.js +0 -1
  283. package/public/assets/JobFailures-DKbvYc4J.js +0 -1
  284. package/public/assets/MemoryUtilization-C5kHmXIb.js +0 -1
  285. package/public/assets/OverBroadcast-C5pF66aq.js +0 -1
  286. package/public/assets/PartitionSizing-BvEJQ5M1.js +0 -1
  287. package/public/assets/PlanExplorer-DjGTZifP.js +0 -1
  288. package/public/assets/PlanGraphRoute-DoIGA7_H.js +0 -1
  289. package/public/assets/RetryWaste-C0znHd_w.js +0 -1
  290. package/public/assets/RowPagination-2icPidMP.js +0 -1
  291. package/public/assets/ShuffleIO-L_XSnJ_V.js +0 -1
  292. package/public/assets/Skew-w4uV5H_g.js +0 -1
  293. package/public/assets/SlowHost-D5tWJ1UR.js +0 -1
  294. package/public/assets/SmallFiles-CTtNiSOh.js +0 -1
  295. package/public/assets/SpeculationWaste-xCKwjz2K.js +0 -1
  296. package/public/assets/Spill-BT8my24K.js +0 -1
  297. package/public/assets/StageFailed-De3vVDbc.js +0 -1
  298. package/public/assets/StageFindingGroup-R4OW-g4U.js +0 -1
  299. package/public/assets/StageHeader-CGeWqC-0.js +0 -1
  300. package/public/assets/StagePill-BwwKLcFl.js +0 -1
  301. package/public/assets/StageShape-D4LmPVyV.js +0 -1
  302. package/public/assets/StageSlowness-8-z6qn4Q.js +0 -1
  303. package/public/assets/Straggler-BIgxsowI.js +0 -1
  304. package/public/assets/TaskFailures-B9lLt6Rw.js +0 -1
  305. package/public/assets/UnderBroadcast-BWqWMpeG.js +0 -1
  306. package/public/assets/WidgetLeadSummary-BdUUi7L5.js +0 -1
  307. package/public/assets/downsample-WmNM1qc9.js +0 -1
  308. package/public/assets/impact-sort-DxaUheWV.js +0 -1
  309. package/public/assets/inbox-M_EuWNiK.js +0 -1
  310. package/public/assets/index-CjVklAdW.css +0 -2
  311. package/public/assets/index-pnImGbgQ.js +0 -9
  312. package/public/assets/parser-worker-C3BAO6z9.js +0 -117
  313. package/public/assets/react-vendor-D2TLlSzI.js +0 -9
  314. package/public/assets/select-C-MZGNpb.js +0 -1
  315. package/public/assets/toggle-group-C3nz7UQ_.js +0 -1
  316. package/public/assets/usePagedRows-jNUWgVTD.js +0 -1
  317. package/public/assets/useRecentFiles-0XuhS91q.js +0 -11
  318. package/public/assets/useSortMode-DHlKZeWv.js +0 -1
  319. package/public/docs/assets/chunks/@localSearchIndexroot.BEfYag5Q.js +0 -1
  320. package/public/docs/assets/contributor-guide_architecture_detector-contract.md.aQQfP7pj.js +0 -1
  321. package/public/docs/assets/contributor-guide_architecture_drill-down.md.Bawx6NgP.js +0 -1
  322. package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.sf9DU2bA.js +0 -1
  323. package/public/docs/assets/contributor-guide_architecture_overview.md.BXQdgG5u.js +0 -1
  324. package/public/docs/assets/contributor-guide_architecture_overview.md.BXQdgG5u.lean.js +0 -1
  325. package/public/docs/assets/contributor-guide_architecture_state-and-history.md.C-xxn0q7.js +0 -1
  326. package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.DYwFeyjV.js +0 -1
  327. package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.P7d1vtEJ.js +0 -6
  328. package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.P7d1vtEJ.lean.js +0 -1
  329. package/public/docs/assets/contributor-guide_contributing.md.CvRsdr6J.js +0 -1
  330. package/public/docs/assets/contributor-guide_contributing.md.CvRsdr6J.lean.js +0 -1
  331. package/public/docs/assets/contributor-guide_development-setup.md.DvMV1xhQ.js +0 -12
  332. package/public/docs/assets/contributor-guide_testing.md.BU6vyA-9.js +0 -1
  333. package/public/docs/assets/contributor-guide_testing.md.BU6vyA-9.lean.js +0 -1
  334. package/public/docs/assets/tuning-reference_anti-patterns.md.Df1YMIHu.js +0 -1
  335. package/public/docs/assets/tuning-reference_anti-patterns.md.Df1YMIHu.lean.js +0 -1
  336. package/public/docs/assets/tuning-reference_aqe.md.BIsCtLzm.js +0 -1
  337. package/public/docs/assets/tuning-reference_aqe.md.BIsCtLzm.lean.js +0 -1
  338. package/public/docs/assets/tuning-reference_bottleneck-broadcast-sizing.md.CEstB3Ia.js +0 -1
  339. package/public/docs/assets/tuning-reference_bottleneck-broadcast-sizing.md.CEstB3Ia.lean.js +0 -1
  340. package/public/docs/assets/tuning-reference_bottleneck-cold-start.md.CEuy-72y.js +0 -7
  341. package/public/docs/assets/tuning-reference_bottleneck-cold-start.md.CEuy-72y.lean.js +0 -1
  342. package/public/docs/assets/tuning-reference_bottleneck-duplicate-plan-subtree.md.CIohQDfn.js +0 -1
  343. package/public/docs/assets/tuning-reference_bottleneck-duplicate-plan-subtree.md.CIohQDfn.lean.js +0 -1
  344. package/public/docs/assets/tuning-reference_bottleneck-failures.md.CAsFf3RR.lean.js +0 -1
  345. package/public/docs/assets/tuning-reference_bottleneck-gc.md.B4T3kJYV.js +0 -6
  346. package/public/docs/assets/tuning-reference_bottleneck-gc.md.B4T3kJYV.lean.js +0 -1
  347. package/public/docs/assets/tuning-reference_bottleneck-job-failure-rate.md.BaJl__1W.js +0 -8
  348. package/public/docs/assets/tuning-reference_bottleneck-job-failure-rate.md.BaJl__1W.lean.js +0 -1
  349. package/public/docs/assets/tuning-reference_bottleneck-memory-utilization.md.DbP-SJZc.js +0 -7
  350. package/public/docs/assets/tuning-reference_bottleneck-memory-utilization.md.DbP-SJZc.lean.js +0 -1
  351. package/public/docs/assets/tuning-reference_bottleneck-retry-waste.md.D5JMjVOt.js +0 -1
  352. package/public/docs/assets/tuning-reference_bottleneck-retry-waste.md.D5JMjVOt.lean.js +0 -1
  353. package/public/docs/assets/tuning-reference_bottleneck-shuffle.md.DDMQVOW6.lean.js +0 -1
  354. package/public/docs/assets/tuning-reference_bottleneck-skew.md.B0MzhWcX.lean.js +0 -1
  355. package/public/docs/assets/tuning-reference_bottleneck-slow-host.md.BlIo6UDW.lean.js +0 -1
  356. package/public/docs/assets/tuning-reference_bottleneck-small-files.md.DHGMTmPs.js +0 -5
  357. package/public/docs/assets/tuning-reference_bottleneck-small-files.md.DHGMTmPs.lean.js +0 -1
  358. package/public/docs/assets/tuning-reference_bottleneck-spill.md.PNH7mITt.js +0 -6
  359. package/public/docs/assets/tuning-reference_bottleneck-spill.md.PNH7mITt.lean.js +0 -1
  360. package/public/docs/assets/tuning-reference_bottleneck-straggler.md.DIbXdAcQ.js +0 -7
  361. package/public/docs/assets/tuning-reference_bottleneck-straggler.md.DIbXdAcQ.lean.js +0 -1
  362. package/public/docs/assets/tuning-reference_bottleneck-tiny-tasks.md.CB05v3zh.js +0 -8
  363. package/public/docs/assets/tuning-reference_bottleneck-tiny-tasks.md.CB05v3zh.lean.js +0 -1
  364. package/public/docs/assets/tuning-reference_bottleneck-utilization.md.DqC_iGkw.lean.js +0 -1
  365. package/public/docs/assets/tuning-reference_caching.md.B7aQ8asB.js +0 -1
  366. package/public/docs/assets/tuning-reference_caching.md.B7aQ8asB.lean.js +0 -1
  367. package/public/docs/assets/tuning-reference_cluster-config.md.CD6vzYtD.js +0 -1
  368. package/public/docs/assets/tuning-reference_cluster-config.md.CD6vzYtD.lean.js +0 -1
  369. package/public/docs/assets/tuning-reference_config.md.UvveiWG3.js +0 -1
  370. package/public/docs/assets/tuning-reference_config.md.UvveiWG3.lean.js +0 -1
  371. package/public/docs/assets/tuning-reference_data-formats.md.bjCAWH3N.js +0 -1
  372. package/public/docs/assets/tuning-reference_data-formats.md.bjCAWH3N.lean.js +0 -1
  373. package/public/docs/assets/tuning-reference_index.md.BQ_NooMV.js +0 -1
  374. package/public/docs/assets/tuning-reference_index.md.BQ_NooMV.lean.js +0 -1
  375. package/public/docs/assets/tuning-reference_intro.md.CobD-lGB.js +0 -1
  376. package/public/docs/assets/tuning-reference_intro.md.CobD-lGB.lean.js +0 -1
  377. package/public/docs/assets/tuning-reference_joins.md.BtKs_CuW.js +0 -1
  378. package/public/docs/assets/tuning-reference_joins.md.BtKs_CuW.lean.js +0 -1
  379. package/public/docs/assets/tuning-reference_memory-model.md.CWhvgevP.js +0 -1
  380. package/public/docs/assets/tuning-reference_memory-model.md.CWhvgevP.lean.js +0 -1
  381. package/public/docs/assets/tuning-reference_metrics.md.mLOh7Apj.js +0 -1
  382. package/public/docs/assets/tuning-reference_metrics.md.mLOh7Apj.lean.js +0 -1
  383. package/public/docs/assets/tuning-reference_partitioning.md.q0zKF_8X.js +0 -1
  384. package/public/docs/assets/tuning-reference_partitioning.md.q0zKF_8X.lean.js +0 -1
  385. package/public/docs/assets/tuning-reference_pyspark.md.DDCfvN9t.js +0 -6
  386. package/public/docs/assets/tuning-reference_pyspark.md.DDCfvN9t.lean.js +0 -1
  387. package/public/docs/assets/tuning-reference_shuffle.md.BZZ7R4Ix.js +0 -1
  388. package/public/docs/assets/tuning-reference_shuffle.md.BZZ7R4Ix.lean.js +0 -1
  389. package/public/docs/assets/tuning-reference_spark-architecture.md.Dwzm5avO.js +0 -1
  390. package/public/docs/assets/tuning-reference_spark-architecture.md.Dwzm5avO.lean.js +0 -1
  391. package/public/docs/assets/tuning-reference_table-formats.md.D6wj-2dX.js +0 -1
  392. package/public/docs/assets/tuning-reference_table-formats.md.D6wj-2dX.lean.js +0 -1
  393. package/public/docs/assets/user-guide_getting-started.md.BJvwLEIM.js +0 -3
  394. package/public/docs/assets/user-guide_getting-started.md.BJvwLEIM.lean.js +0 -1
  395. package/public/docs/assets/user-guide_mcp-tools.md.DGvolGzt.lean.js +0 -1
  396. package/public/docs/assets/user-guide_run-comparison.md.CQc1aoU8.js +0 -1
  397. package/public/docs/assets/user-guide_run-comparison.md.CQc1aoU8.lean.js +0 -1
  398. package/public/docs/assets/user-guide_understanding-findings.md.B29f0ucS.js +0 -1
  399. /package/public/docs/assets/{contributor-guide_architecture_board-widgets.md.9ToZXcTk.lean.js → contributor-guide_architecture_board-widgets.md.VTcYTH4d.lean.js} +0 -0
package/public/index.html CHANGED
@@ -23,13 +23,14 @@
23
23
  document.documentElement.setAttribute('data-theme', 'light');
24
24
  } catch (_) {}
25
25
  </script>
26
- <script type="module" crossorigin src="./assets/index-pnImGbgQ.js"></script>
26
+ <script type="module" crossorigin src="./assets/index-CEM9bz_x.js"></script>
27
27
  <link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-hePW80VL.js">
28
28
  <link rel="modulepreload" crossorigin href="./assets/charts-vendor-0VMaLVLN.js">
29
29
  <link rel="modulepreload" crossorigin href="./assets/plan-graph-vendor-B0sEms9Z.js">
30
- <link rel="modulepreload" crossorigin href="./assets/react-vendor-D2TLlSzI.js">
30
+ <link rel="modulepreload" crossorigin href="./assets/react-vendor-BAKt3vny.js">
31
+ <link rel="modulepreload" crossorigin href="./assets/DisclosureContext-LYzHroHO.js">
31
32
  <link rel="stylesheet" crossorigin href="./assets/plan-graph-vendor-DLioOiRN.css">
32
- <link rel="stylesheet" crossorigin href="./assets/index-CjVklAdW.css">
33
+ <link rel="stylesheet" crossorigin href="./assets/index-BcqTLKFp.css">
33
34
  </head>
34
35
  <body>
35
36
  <div id="root"></div>
@@ -1,14 +1,28 @@
1
- import { DETECTORS, } from './detectors.js';
1
+ import { DETECTORS, ENTRY_BY_TYPE, } from './detectors.js';
2
+ import { effectiveThresholds, findingTunedThresholds, overridesFor, tunedThresholdsNote } from './threshold-overrides.js';
2
3
  import { computePeakConcurrentCores } from './core-count.js';
3
4
  import { assertNever } from './assert-never.js';
4
- import { estimateImpact } from './impact-estimator.js';
5
+ import { estimateImpact, } from './impact-estimator.js';
5
6
  import { computeOccupancy, } from './occupancy.js';
6
- import { deriveImpactBand } from './impact-band.js';
7
+ import { deriveImpactBand, IMPACT_FLOOR_PCT_CRIT, IMPACT_FLOOR_PCT_WARN, } from './impact-band.js';
7
8
  import { IMPACT_BAND_ORDER } from './format-utils.js';
8
9
 
9
-
10
+
10
11
 
11
12
 
13
+ // The runner reads each entry through the Detector contract, not its own precise `as const` shape:
14
+ // the scope switch below calls each entry's bound detect with that scope's target.
15
+ const detectors = DETECTORS;
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
12
26
  // FNV-1a 32-bit stable string hash: deterministic finding id across runs, no timestamps/randomness.
13
27
  function fnv1a(str ) {
14
28
  let h = 0x811c9dc5;
@@ -19,28 +33,59 @@ function fnv1a(str ) {
19
33
  return (h >>> 0).toString(16).padStart(8, '0');
20
34
  }
21
35
 
36
+ // The id hash's discriminator slots, in the order it has always joined them: reordering or renaming
37
+ // one changes every finding id, which needs an EVIDENCE_SCHEMA_VERSION bump.
38
+ const DISCRIMINATOR_SLOTS = [
39
+ 'host', 'executorId', 'rule', 'variant', 'dimension',
40
+ 'direction', 'nodeName', 'rootName', 'subtreeSize', 'groupIndex', 'largerSideBytes',
41
+ 'rddId', 'relation', 'format', 'operator', 'executionIds',
42
+ ] ;
43
+
44
+
45
+ // Per type, the fields that tell apart sibling findings sharing one location and metric; without
46
+ // them those siblings hash to one id:
47
+ // slowHost (host), memoryUtilization (executorId), partitionSizing (rule);
48
+ // cacheUtilization (rddId+variant), cachingOpportunity (relation/format for leaf,
49
+ // operator+relation for composite, executionIds as last resort);
50
+ // smallFiles (direction/nodeName), duplicatePlanSubtree (groupIndex is the real
51
+ // uniqueness guarantee: rootName+subtreeSize can collide across groups),
52
+ // underBroadcast (value+largerSideBytes per node/side).
53
+ // memoryUtilization leaves out `rule`: one heap band per executor, so executorId is already
54
+ // unique and folding rule in risks id churn if band logic changes. partitionSizing keeps
55
+ // `rule`: a stage can emit several rules at once sharing stageId+metric.
56
+ const ID_DISCRIMINATORS = {
57
+ skew: [], stageShape: ['rule'], shuffle: [], partitionSizing: ['rule'], spill: [], gc: ['direction'],
58
+ slowHost: ['host', 'executorId', 'variant', 'dimension'], stageSlowness: [], stageFailed: ['variant'],
59
+ failures: [], straggler: [], speculationWaste: [], retryWaste: [], tinyTask: [],
60
+ incompleteRun: [], coldStart: [], utilization: [], memoryUtilization: ['executorId', 'variant'],
61
+ cacheUtilization: ['variant', 'rddId'], coreLocality: [], autoscalingChurn: [],
62
+ cachingOpportunity: ['variant', 'relation', 'format', 'operator', 'executionIds'],
63
+ jobFailureRate: [], configAudit: [],
64
+ duplicatePlanSubtree: ['rootName', 'subtreeSize', 'groupIndex'], smallFiles: ['direction', 'nodeName'],
65
+ underBroadcast: ['largerSideBytes'], overBroadcast: [],
66
+ };
67
+
68
+ // ID_DISCRIMINATORS as Sets, built once: findingId runs once per finding.
69
+ const ID_DISCRIMINATOR_SETS = new Map (
70
+ Object.entries(ID_DISCRIMINATORS).map(([type, slots]) => [type, new Set (slots)]),
71
+ );
72
+
73
+ // Location key: stage, else SQL execution, else audited config property.
74
+ function locationKey(f ) {
75
+ if (f.stageId != null) return f.stageId;
76
+ if ('executionId' in f) return f.executionId;
77
+ if (f.type === 'configAudit') return f.property;
78
+ return '';
79
+ }
80
+
22
81
  export function findingId(f ) {
23
- // Location key: stage, else SQL execution, else audited config property.
24
- const locKey = f.stageId ?? f.executionId ?? f.property ?? '';
25
- // Discriminators for detectors that emit multiple findings on the same
26
- // location+metric; without them these siblings hash to one id:
27
- // slowHost (host), memoryUtilization (executorId+dimension), partitionSizing (rule);
28
- // cacheUtilization (rddId+variant), cachingOpportunity (relation/format for leaf,
29
- // operator+relation for composite, executionIds as last resort);
30
- // smallFiles (direction/nodeName), duplicatePlanSubtree (groupIndex is the real
31
- // uniqueness guarantee: rootName+subtreeSize can collide across groups),
32
- // broadcastSizing (value+largerSideBytes per node/side).
33
- // memoryUtilization excludes `rule`: one heap band per executor, so executorId is already
34
- // unique and folding rule in risks id churn if band logic changes. partitionSizing keeps
35
- // `rule`: a stage can emit several rules at once sharing stageId+metric.
36
- const rule = f.type === 'memoryUtilization' ? undefined : f.rule;
37
- const disc = [
38
- f.host, f.executorId, rule, f.variant, f.dimension,
39
- f.direction, f.nodeName, f.rootName, f.subtreeSize, f.groupIndex, f.largerSideBytes,
40
- f.rddId, f.relation, f.format, f.operator,
41
- f.executionIds ? f.executionIds.join(',') : '',
42
- ].map((v) => v ?? '').join('|');
43
- return fnv1a(`${f.type}|${locKey}|${f.metric ?? ''}|${f.value ?? ''}|${disc}`);
82
+ const fields = f ;
83
+ const listed = ID_DISCRIMINATOR_SETS.get(f.type);
84
+ const disc = DISCRIMINATOR_SLOTS.map((slot) => {
85
+ const v = listed?.has(slot) ? fields[slot] : undefined;
86
+ return Array.isArray(v) ? v.join(',') : v ?? '';
87
+ }).join('|');
88
+ return fnv1a(`${f.type}|${locationKey(f)}|${f.metric ?? ''}|${f.value ?? f.valueText ?? ''}|${disc}`);
44
89
  }
45
90
 
46
91
  // skew's max/median branch (stage.taskCount below minTasksForP95) and straggler are both driven
@@ -72,13 +117,13 @@ function flagSkewStragglerOverlap(findings ) {
72
117
  }
73
118
  }
74
119
 
75
- function push(out , entry , result ) {
120
+ function push(out , entry , result , tuned = null) {
76
121
  if (!result) return;
77
122
  const detectorVersion = entry.version ?? 1;
78
123
  for (const f of (Array.isArray(result) ? result : [result])) {
79
124
  if (!f) continue;
80
- if (entry.suppressWhen && entry.suppressWhen(f, out)) continue;
81
125
  const stamped = { ...f, docAnchor: f.docAnchor ?? entry.docAnchor, detectorVersion };
126
+ if (tuned) stamped.tunedThresholds = tuned;
82
127
  const id = findingId(stamped);
83
128
  // Dedup guard: same id => same finding, keep first. Correctness depends on
84
129
  // findingId's discriminators being unique per distinct finding, not on this line.
@@ -87,6 +132,50 @@ function push(out , entry , result
87
132
  }
88
133
  }
89
134
 
135
+ // A tuned finding's caveat, once its estimate is known: only a finding with an estimate figure
136
+ // (wall-clock or raw waste) says that figure is unvalidated.
137
+ function noteTunedThresholds(findings ) {
138
+ for (const f of findings) {
139
+ if (!f.tunedThresholds) continue;
140
+ const hasFigure = f.impactEstimate != null && f.impactEstimate.basis !== 'informational';
141
+ const note = tunedThresholdsNote(f.tunedThresholds, hasFigure);
142
+ f.validationRequired = f.validationRequired ? `${f.validationRequired} ${note}` : note;
143
+ }
144
+ }
145
+
146
+ // skew and straggler gate on floorPctWarn/floorPctCrit thresholds that default to the band's own
147
+ // floors, so a finding they admit at their warn floor grades at least warning. A tuned floor grades
148
+ // that entry's findings too; a type whose entry has no such threshold keeps the defaults.
149
+ function bandFloors(type , overrides ) {
150
+ const entry = ENTRY_BY_TYPE.get(type);
151
+ if (!entry) return null;
152
+ const { floorPctWarn, floorPctCrit } = effectiveThresholds(entry, overrides);
153
+ return {
154
+ warnPct: typeof floorPctWarn === 'number' ? floorPctWarn : IMPACT_FLOOR_PCT_WARN,
155
+ critPct: typeof floorPctCrit === 'number' ? floorPctCrit : IMPACT_FLOOR_PCT_CRIT,
156
+ };
157
+ }
158
+
159
+ // An entry's `suppressedBy` names another entry: drop its findings on every stage that entry
160
+ // flagged. Runs once every detector has, so neither declaration order matters, and reads the
161
+ // unsuppressed findings, so the result doesn't depend on which suppression is applied first.
162
+ function applySuppression(out ) {
163
+ const dropped = new Map ();
164
+ for (const entry of detectors) {
165
+ if (!entry.suppressedBy) continue;
166
+ const suppressorTypes = new Set (
167
+ detectors.filter((d) => d.type === entry.suppressedBy).flatMap((d) => d.emits),
168
+ );
169
+ for (const type of entry.emits) {
170
+ const stagesToDrop = dropped.get(type) ?? new Set ();
171
+ for (const f of out) if (suppressorTypes.has(f.type) && f.stageId != null) stagesToDrop.add(f.stageId);
172
+ dropped.set(type, stagesToDrop);
173
+ }
174
+ }
175
+ if (dropped.size === 0) return out;
176
+ return out.filter((f) => f.stageId == null || !dropped.get(f.type)?.has(f.stageId));
177
+ }
178
+
90
179
  // `app` widened to `SparkAppInfo | null` to match real callers (AppModel.app is
91
180
  // nullable at the type level); every detector below tolerates a null app.
92
181
  export function analyze(
@@ -97,6 +186,7 @@ export function analyze(
97
186
  jobs ,
98
187
  sql = new Map(),
99
188
  runAggregates = null,
189
+ { thresholds } = {},
100
190
  ) {
101
191
  // `app ?? {}`: detectors tolerate a null app (malformed logs), so this must too.
102
192
  // computePeakConcurrentCores (not computeTotalCores): the occupancy ceiling needs a
@@ -108,37 +198,55 @@ export function analyze(
108
198
  executorsAdded ,
109
199
  executorsRemoved ,
110
200
  );
111
- // Computed once so detectors gate impact band on the same occupancy-clipped waste
112
- // estimateImpact displays as savings, not a raw pre-clip delta the two passes would disagree on.
113
- const occupancy = computeOccupancy(stages , totalCores);
114
- const ctx = {
115
- app, stages, executorsAdded, executorsRemoved, jobs, sql, runAggregates, occupancy,
201
+ // One occupancy sweep per analysis, shared by the detectors' runtime floors and every entry's
202
+ // estimate(), so a floor gates on the same occupancy-clipped figure displayed as savings.
203
+ const impact = {
204
+ stages, totalCores,
205
+ occupancy: computeOccupancy(stages , totalCores),
206
+ };
207
+ // The one cast from the posted-model types to the detector-side shapes: types.ts's Stage and
208
+ // SqlExecution carry a catch-all index signature, while every field DetectorStage/DetectorSqlExec
209
+ // declare is one finalizeStage and event-handlers.ts always set (see detectors.ts's header).
210
+ const ctx = {
211
+ app, jobs, executorsAdded, executorsRemoved, runAggregates, impact,
212
+ stages: stages ,
213
+ sql: sql ,
116
214
  };
117
215
  const out = [];
118
- for (const d of DETECTORS) {
216
+ for (const d of detectors) {
119
217
  if (d.inScorecard === false) continue;
218
+ const overrides = overridesFor(d, thresholds);
219
+ const tuned = findingTunedThresholds(d, thresholds);
120
220
  switch (d.scope) {
121
- case 'stage':
122
- for (const s of stages.values()) push(out, d, d.detect(s, ctx));
221
+ case 'stage': {
222
+ const detect = d.withThresholds(overrides);
223
+ for (const s of ctx.stages.values()) push(out, d, detect(s, ctx), tuned);
123
224
  break;
124
- case 'sql':
125
- for (const e of sql.values()) push(out, d, d.detect(e, ctx));
225
+ }
226
+ case 'sql': {
227
+ const detect = d.withThresholds(overrides);
228
+ for (const e of ctx.sql.values()) push(out, d, detect(e, ctx), tuned);
126
229
  break;
230
+ }
127
231
  case 'app':
232
+ push(out, d, d.withThresholds(overrides)(ctx), tuned);
233
+ break;
128
234
  case 'config':
129
- push(out, d, d.detect(ctx));
235
+ push(out, d, d.withThresholds(overrides)(ctx ), tuned);
130
236
  break;
131
237
  default:
132
- assertNever(d.scope);
238
+ assertNever(d);
133
239
  }
134
240
  }
135
- estimateImpact(out, stages, totalCores);
136
- deriveImpactBand(out, app);
137
- flagSkewStragglerOverlap(out);
241
+ const findings = applySuppression(out);
242
+ estimateImpact(findings, impact);
243
+ noteTunedThresholds(findings);
244
+ deriveImpactBand(findings, app, thresholds ? (type) => bandFloors(type, thresholds) : undefined);
245
+ flagSkewStragglerOverlap(findings);
138
246
  // Ascending IMPACT_BAND_ORDER (critical 0 -> info 2) puts the worst band first;
139
247
  // stable sort keeps DETECTORS declaration order within a band.
140
- out.sort((a, b) => IMPACT_BAND_ORDER[a.impactBand] - IMPACT_BAND_ORDER[b.impactBand]);
141
- return out;
248
+ findings.sort((a, b) => IMPACT_BAND_ORDER[a.impactBand] - IMPACT_BAND_ORDER[b.impactBand]);
249
+ return findings;
142
250
  }
143
251
 
144
252
  // Memoizes auditConfig by `app` identity (like evidence-report.ts's jsonCache) so config detectors
@@ -149,10 +257,10 @@ const auditConfigCache = new WeakMap ();
149
257
 
150
258
  function computeAuditConfig(app ) {
151
259
  const out = [];
152
- for (const d of DETECTORS) if (d.scope === 'config') push(out, d, d.detect({ app }));
153
- // configAudit's impact case is unconditionally costOnly('none'): needs no stages/totalCores,
154
- // an empty stages map gives parity with analyze().
155
- estimateImpact(out, new Map());
260
+ for (const d of detectors) if (d.scope === 'config') push(out, d, d.withThresholds()({ app }));
261
+ // configAudit's estimate is unconditionally costOnly('none'): needs no stages/totalCores, so an
262
+ // empty context gives parity with analyze().
263
+ estimateImpact(out, { stages: new Map(), occupancy: new Map(), totalCores: 0 });
156
264
  deriveImpactBand(out, app);
157
265
  return out.map((f) => ({ ...f, stageId: f.stageId ?? null }));
158
266
  }
@@ -0,0 +1,88 @@
1
+ // Which checks a log could actually run. Shared by the dashboard (verdict, top bar, Clean checks)
2
+ // and the CLI/MCP evidence report, so a check the log lacked the data for never reads as passed on
3
+ // one path and "not checked" on another.
4
+ import { detectorCatalog } from './detectors.js';
5
+ import { isRealFinding } from './recommendation-rollup.js';
6
+ import { summarizeRunOutcome } from './run-outcome.js';
7
+
8
+
9
+ export const NO_FINISHED_STAGE_GAP = 'No stage in this log recorded an end, so the stage checks had nothing to measure.';
10
+ export const INCOMPLETE_RUN_GAP =
11
+ 'The log has no end-of-run record, so the core usage, memory and executor churn checks had no run length to measure.';
12
+
13
+ /** True when at least one stage recorded both a start and an end, so the
14
+ * stage checks had something to measure. */
15
+ export function hasFinishedStage(stages ) {
16
+ return [...stages.values()].some((stage) => stage.submittedAt != null && stage.completedAt != null);
17
+ }
18
+
19
+ /** A finding that reports a check could not run for lack of evidence (cache
20
+ * storage without block updates, memory without executor metrics), rather
21
+ * than a problem found. Its recommendation names the setting to turn on. */
22
+ export function isEvidenceCaveat(finding ) {
23
+ return ('dataUnavailable' in finding && finding.dataUnavailable === true) || !isRealFinding(finding);
24
+ }
25
+
26
+ /** App-level checks measured over the run's full span, which a log with no
27
+ * ApplicationEnd (an `incompleteRun` finding) cannot give them. */
28
+ export const RUN_SPAN_CHECK_TYPES = new Set(['utilization', 'memoryUtilization', 'autoscalingChurn']);
29
+
30
+ /** Detector types that measure each stage, read from the detector catalog. */
31
+ export const PER_STAGE_CHECK_TYPES = new Set(
32
+ detectorCatalog().filter((entry) => entry.scope === 'stage').map((entry) => entry.type),
33
+ );
34
+
35
+ export function isIncompleteRun(allFindings ) {
36
+ return allFindings.some((finding) => finding.type === 'incompleteRun');
37
+ }
38
+
39
+ /** What this log could not check, in plain sentences, each saying what to
40
+ * turn on for the next run where the detector names it. */
41
+ export function verdictGaps(allFindings , noFinishedStages ) {
42
+ const gaps = new Set ();
43
+ if (noFinishedStages) gaps.add(NO_FINISHED_STAGE_GAP);
44
+ if (isIncompleteRun(allFindings)) gaps.add(INCOMPLETE_RUN_GAP);
45
+ for (const finding of allFindings) {
46
+ if (isEvidenceCaveat(finding) && finding.recommendation) gaps.add(finding.recommendation);
47
+ }
48
+ return [...gaps];
49
+ }
50
+
51
+ /** The one rule for calling a run clean, shared by the verdict, the top bar
52
+ * and the evidence report: no finding at all, no failed job, and nothing the
53
+ * log lacked to run a check. */
54
+ export function isCleanRun(appModel , allFindings ) {
55
+ if (summarizeRunOutcome(appModel.jobs, allFindings).failedJobs > 0) return false;
56
+ if (allFindings.some(isRealFinding)) return false;
57
+ return verdictGaps(allFindings, !hasFinishedStage(appModel.stages)).length === 0;
58
+ }
59
+
60
+
61
+
62
+
63
+
64
+
65
+
66
+
67
+
68
+ /** The not-run rule for one run: an evidence caveat of that type, a per-stage
69
+ * check on a log where no stage finished, or a run-span check on a log with
70
+ * no ApplicationEnd. */
71
+ export function checkCoverage(stages , allFindings ) {
72
+ const noFinishedStages = !hasFinishedStage(stages);
73
+ const incomplete = isIncompleteRun(allFindings);
74
+ const caveatReasons = new Map ();
75
+ for (const finding of allFindings) {
76
+ if (!isEvidenceCaveat(finding)) continue;
77
+ if (!caveatReasons.get(finding.type)) caveatReasons.set(finding.type, finding.recommendation ?? null);
78
+ }
79
+ const notRunReason = (type ) => {
80
+ const caveatReason = caveatReasons.get(type);
81
+ if (caveatReason) return caveatReason;
82
+ if (noFinishedStages && PER_STAGE_CHECK_TYPES.has(type)) return NO_FINISHED_STAGE_GAP;
83
+ if (incomplete && RUN_SPAN_CHECK_TYPES.has(type)) return INCOMPLETE_RUN_GAP;
84
+ // A caveat with no recommendation still means the check did not run.
85
+ return caveatReasons.has(type) ? 'The log lacked the data this check needs.' : null;
86
+ };
87
+ return { isNotRun: (type) => notRunReason(type) != null, notRunReason };
88
+ }
@@ -1,5 +1,6 @@
1
1
  import { computeEfficiencyModel } from '../efficiency-model.js';
2
- import { computeSkewRatio, DETECTORS } from '../detectors.js';
2
+ import { computeSkewRatio, ENTRY_BY_TYPE, } from '../detectors.js';
3
+ import { effectiveThresholds } from '../threshold-overrides.js';
3
4
  import { IMPACT_BAND_ORDER } from '../format-utils.js';
4
5
 
5
6
 
@@ -13,26 +14,26 @@ const IMPACT_BANDS = Object.keys(IMPACT_BAND_ORDER) ;
13
14
 
14
15
 
15
16
 
16
-
17
+
17
18
 
18
19
 
19
20
 
20
21
 
21
- const skewDetector = DETECTORS.find((d) => d.type === 'skew');
22
- const SKEW_MIN_TASKS_FOR_P95 = skewDetector .thresholds .minTasksForP95 ;
22
+ // The skew entry's minTasksForP95 under the run's overrides, so the budget measures the same
23
+ // ratio (P95/median or max/median) the skew finding reports.
24
+ function skewMinTasksForP95(thresholds ) {
25
+ return effectiveThresholds(ENTRY_BY_TYPE.get('skew') , thresholds).minTasksForP95 ;
26
+ }
23
27
 
24
28
  function taskDataTrusted(appModel ) {
25
29
  const entry = appModel.evidenceAvailability?.entries?.find((e) => e.key === 'taskCoreTime');
26
30
  return entry?.state === 'present';
27
31
  }
28
32
 
29
- // Finding.value is number|string (some detectors put text there); spill findings are
30
- // always numeric, so the typeof guard narrows without changing behavior for real input.
31
33
  function maxFindingValue(catalog , type ) {
32
34
  const values = catalog
33
35
  .filter((f) => f.type === type)
34
- .map((f) => f.value ?? 0)
35
- .filter((v) => typeof v === 'number');
36
+ .map((f) => f.value ?? 0);
36
37
  return values.length > 0 ? Math.max(...values) : null;
37
38
  }
38
39
 
@@ -58,7 +59,7 @@ function checkSpill(appModel , catalog , maxSpillGb )
58
59
  : { name: 'max-spill', status: 'pass', detail: `Peak stage spill ${maxBytes} bytes within budget ${budgetBytes} bytes.` };
59
60
  }
60
61
 
61
- function checkSkew(appModel , maxSkewRatio ) {
62
+ function checkSkew(appModel , maxSkewRatio , minTasksForP95 ) {
62
63
  if (!taskDataTrusted(appModel)) {
63
64
  return { name: 'max-skew', status: 'inconclusive', detail: 'No trustworthy task-level evidence to measure skew.' };
64
65
  }
@@ -69,7 +70,7 @@ function checkSkew(appModel , maxSkewRatio ) {
69
70
  // Recompute the true ratio per stage: the skew detector floors findings at
70
71
  // thresholds.ratioWarn (3), so a stricter budget can't be enforced from catalog alone.
71
72
  const ratios = stages
72
- .map((stage) => computeSkewRatio(stage, SKEW_MIN_TASKS_FOR_P95))
73
+ .map((stage) => computeSkewRatio(stage, minTasksForP95))
73
74
  .filter((r) => r !== null)
74
75
  .map((r) => r.ratio);
75
76
  if (ratios.length === 0) {
@@ -97,21 +98,24 @@ function checkFailedTaskRate(appModel , catalog , maxPct
97
98
  return { name: 'max-failed-task-rate', status: 'pass', detail: `No job-failure-rate finding: task failure rate is below the detector's reporting floor.` };
98
99
  }
99
100
 
101
+ // Busy core time: the share of available executor core time that ran tasks, 100 minus the
102
+ // dashboard's "Unused core time". Not the dashboard's Efficiency tile (the share of wall-clock with
103
+ // a stage running), so the detail never calls it "Efficiency".
100
104
  function checkEfficiency(appModel , minPct ) {
101
105
  if (!taskDataTrusted(appModel)) {
102
- return { name: 'min-efficiency', status: 'inconclusive', detail: 'No trustworthy task-level evidence to measure efficiency.' };
106
+ return { name: 'min-efficiency', status: 'inconclusive', detail: 'No trustworthy task-level evidence to measure busy core time.' };
103
107
  }
104
108
  const model = computeEfficiencyModel({
105
109
  app: appModel.app, stages: appModel.stages,
106
110
  executorsAdded: appModel.executors.added, runAggregates: appModel.runAggregates,
107
111
  });
108
112
  if (model.wastagePct == null) {
109
- return { name: 'min-efficiency', status: 'inconclusive', detail: 'Efficiency could not be computed (no available compute hours).' };
113
+ return { name: 'min-efficiency', status: 'inconclusive', detail: 'Busy core time could not be computed (no available compute hours).' };
110
114
  }
111
- const efficiencyPct = 100 - model.wastagePct;
112
- return efficiencyPct < minPct
113
- ? { name: 'min-efficiency', status: 'violation', detail: `Efficiency ${efficiencyPct}% below budget ${minPct}%.` }
114
- : { name: 'min-efficiency', status: 'pass', detail: `Efficiency ${efficiencyPct}% meets budget ${minPct}%.` };
115
+ const busyCorePct = 100 - model.wastagePct;
116
+ return busyCorePct < minPct
117
+ ? { name: 'min-efficiency', status: 'violation', detail: `Busy core time ${busyCorePct}% below budget ${minPct}%.` }
118
+ : { name: 'min-efficiency', status: 'pass', detail: `Busy core time ${busyCorePct}% meets budget ${minPct}%.` };
115
119
  }
116
120
 
117
121
  function checkRegression(comparison , maxRegressionPct , regressionMetric ) {
@@ -158,13 +162,16 @@ function pushComparisonBudget(
158
162
  results.push(comparison ? check(comparison) : { name, status: 'inconclusive', detail: 'No baseline comparison available to evaluate this budget.' });
159
163
  }
160
164
 
161
- export function evaluateBudgets({ appModel, catalog, budgets, comparison }
165
+ /** `thresholds`: the overrides the catalog was analyzed with, so a budget that recomputes a
166
+ * detector's figure (--max-skew) uses the same thresholds. */
167
+ export function evaluateBudgets({ appModel, catalog, budgets, comparison, thresholds }
162
168
 
169
+
163
170
  ) {
164
171
  const results = [];
165
172
  if (Number.isFinite(budgets.maxRuntimeMs)) results.push(checkRuntime(appModel, budgets.maxRuntimeMs ));
166
173
  if (Number.isFinite(budgets.maxSpillGb)) results.push(checkSpill(appModel, catalog, budgets.maxSpillGb ));
167
- if (Number.isFinite(budgets.maxSkewRatio)) results.push(checkSkew(appModel, budgets.maxSkewRatio ));
174
+ if (Number.isFinite(budgets.maxSkewRatio)) results.push(checkSkew(appModel, budgets.maxSkewRatio , skewMinTasksForP95(thresholds)));
168
175
  if (Number.isFinite(budgets.maxFailedTaskRatePct)) results.push(checkFailedTaskRate(appModel, catalog, budgets.maxFailedTaskRatePct ));
169
176
  if (Number.isFinite(budgets.minEfficiencyPct)) results.push(checkEfficiency(appModel, budgets.minEfficiencyPct ));
170
177
  // Guarded here so any evaluateBudgets caller benefits: regressionMetric without
@@ -181,6 +188,12 @@ export function evaluateBudgets({ appModel, catalog, budgets, comparison }
181
188
  pushComparisonBudget(results, comparison, 'fail-on-introduced',
182
189
  (c) => checkFailOnIntroduced(c, budgets.failOnIntroduced ));
183
190
  }
191
+ // Always checked, unlike the opt-in budgets above: a run with no ApplicationEnd is
192
+ // inconclusive by default, so a passing budget can't hide a truncated log.
193
+ const incompleteRunFinding = catalog.find((f) => f.type === 'incompleteRun');
194
+ if (incompleteRunFinding) {
195
+ results.push({ name: 'run-complete', status: 'inconclusive', detail: incompleteRunFinding.recommendation ?? 'Event log has no ApplicationEnd event.' });
196
+ }
184
197
  return {
185
198
  results,
186
199
  violated: results.some((r) => r.status === 'violation'),
@@ -1,4 +1,4 @@
1
- import { readFileSync, readdirSync, statSync, openSync, readSync, closeSync } from 'node:fs';
1
+ import { readdirSync, statSync, openSync, readSync, closeSync } from 'node:fs';
2
2
  import { join, basename } from 'node:path';
3
3
  import { createState, runParse, runParseFiles, reassembleRollingEntries } from '../parser-worker.js';
4
4
  import { nodeParseCodecs } from './native-zstd.js';
@@ -6,10 +6,13 @@ import { createModelCallbacks } from '../model-assembler.js';
6
6
  import { routeMessage, } from '../ingest.js';
7
7
 
8
8
 
9
+ // No whole-file arrayBuffer(): the parser only ever reads bounded slices, and a
10
+ // whole-file read is what capped local logs at 2 GiB.
9
11
 
10
12
 
11
13
 
12
-
14
+
15
+
13
16
 
14
17
 
15
18
  export function emptyAppModel() {
@@ -24,18 +27,48 @@ export function emptyAppModel() {
24
27
  };
25
28
  }
26
29
 
27
- // File-like shape runParse/runParseFiles need: name, size, slice().arrayBuffer(), arrayBuffer().
30
+ // File-like shape runParse/runParseFiles need (name, size, slice().arrayBuffer()), read with
31
+ // positioned readSync so only the requested slice is ever in memory, whatever the file size.
32
+ // The descriptor opens on the first read and closes once a read reaches the end of the file,
33
+ // so a rolling directory's parts hold at most one open descriptor at a time while streaming.
34
+ // A later read (a zip archive reads its tail first) reopens it; callers must still call
35
+ // close() once parsing settles, to cover reads that stopped early on an error.
28
36
  export function nodeFileFromPath(path ) {
29
- const bytes = readFileSync(path);
30
- const u8 = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
37
+ const name = basename(path);
38
+ const size = statSync(path).size;
39
+ let fd = null;
40
+ const close = () => {
41
+ if (fd === null) return;
42
+ const open = fd;
43
+ fd = null;
44
+ closeSync(open);
45
+ };
31
46
  return {
32
- name: basename(path),
33
- size: u8.length,
47
+ name,
48
+ size,
34
49
  slice(start , end ) {
35
- const view = u8.subarray(start, end);
36
- return { async arrayBuffer() { return view.slice().buffer; } };
50
+ return {
51
+ async arrayBuffer() {
52
+ const from = Math.max(0, start);
53
+ const to = Math.min(end, size);
54
+ if (to <= from) return new ArrayBuffer(0);
55
+ const buf = new Uint8Array(to - from);
56
+ fd ??= openSync(path, 'r');
57
+ // readSync may return fewer bytes than asked; loop until the slice is full.
58
+ for (let filled = 0; filled < buf.length;) {
59
+ const n = readSync(fd, buf, filled, buf.length - filled, from + filled);
60
+ if (n === 0) {
61
+ close();
62
+ throw new Error(`"${name}" shrank while it was being read`);
63
+ }
64
+ filled += n;
65
+ }
66
+ if (to === size) close();
67
+ return buf.buffer;
68
+ },
69
+ };
37
70
  },
38
- async arrayBuffer() { return u8.slice().buffer; },
71
+ close,
39
72
  };
40
73
  }
41
74
 
@@ -107,26 +140,38 @@ export function collectViaDispatch(
107
140
 
108
141
  export async function collectRun(inputPath ) {
109
142
  const stat = statSync(inputPath);
110
- return collectViaDispatch((state, emit, reject) => {
111
- if (stat.isDirectory()) {
112
- if (!isRollingLogDirectory(inputPath)) {
113
- reject(new Error("This isn't a Spark rolling event-log directory. Pass a single event-log file instead."));
114
- return;
115
- }
116
- const names = readdirSync(inputPath);
117
- let ordered;
118
- try {
119
- ordered = reassembleRollingEntries(names);
120
- } catch (e) {
121
- reject(e);
122
- return;
143
+ // Every file opened for this run, closed once parsing settles on any path (done, parse
144
+ // error, decode error, or a throw past the parser's guards).
145
+ const opened = [];
146
+ const open = (path ) => {
147
+ const file = nodeFileFromPath(path);
148
+ opened.push(file);
149
+ return file;
150
+ };
151
+ try {
152
+ return await collectViaDispatch((state, emit, reject) => {
153
+ if (stat.isDirectory()) {
154
+ if (!isRollingLogDirectory(inputPath)) {
155
+ reject(new Error("This isn't a Spark rolling event-log directory. Pass a single event-log file instead."));
156
+ return;
157
+ }
158
+ const names = readdirSync(inputPath);
159
+ let ordered;
160
+ try {
161
+ ordered = reassembleRollingEntries(names);
162
+ } catch (e) {
163
+ reject(e);
164
+ return;
165
+ }
166
+ const files = ordered.map((name) => open(join(inputPath, name)));
167
+ // .catch(reject), not void: a throw past the parser's guards would otherwise leave
168
+ // this Promise pending forever, surfacing only as an unhandled rejection.
169
+ runParseFiles(files, state, { emit, ...nodeParseCodecs }).catch(reject);
170
+ } else {
171
+ runParse(open(inputPath), state, { emit, ...nodeParseCodecs }).catch(reject);
123
172
  }
124
- const files = ordered.map((name) => nodeFileFromPath(join(inputPath, name)));
125
- // .catch(reject), not void: a throw past the parser's guards would otherwise leave
126
- // this Promise pending forever, surfacing only as an unhandled rejection.
127
- runParseFiles(files, state, { emit, ...nodeParseCodecs }).catch(reject);
128
- } else {
129
- runParse(nodeFileFromPath(inputPath), state, { emit, ...nodeParseCodecs }).catch(reject);
130
- }
131
- }, (msg) => new Error((msg ).message));
173
+ }, (msg) => new Error((msg ).message));
174
+ } finally {
175
+ for (const file of opened) file.close();
176
+ }
132
177
  }