sparkforensics-cli 0.1.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. package/README.md +6 -0
  2. package/bin/sparkforensics-analyze.mjs +113 -48
  3. package/export-template/docs/404.html +25 -0
  4. package/export-template/docs/assets/app.DQTZyGL1.js +1 -0
  5. package/export-template/docs/assets/aqe-loop.IwQSATHw.svg +1 -0
  6. package/export-template/docs/assets/aqe-loop.dark.DGbaxqJE.svg +1 -0
  7. package/export-template/docs/assets/broadcast-vs-shuffle.Db4WY1XK.svg +1 -0
  8. package/export-template/docs/assets/broadcast-vs-shuffle.dark.C7Bxs0mG.svg +1 -0
  9. package/export-template/docs/assets/cache-lifecycle.dark.B-hS7AgU.svg +1 -0
  10. package/export-template/docs/assets/cache-lifecycle.rEOVYQNU.svg +1 -0
  11. package/export-template/docs/assets/chunks/@localSearchIndexroot.DppnXnDE.js +1 -0
  12. package/export-template/docs/assets/chunks/VPLocalSearchBox.BkBIPFs6.js +9 -0
  13. package/export-template/docs/assets/chunks/duplicate-plan-subtree.dark.Cdp70QhV.js +1 -0
  14. package/export-template/docs/assets/chunks/framework.DSg0KOwT.js +20 -0
  15. package/export-template/docs/assets/chunks/retry-escalation-ladder.dark.DHipdJgZ.js +1 -0
  16. package/export-template/docs/assets/chunks/theme.DP0u1AUq.js +2 -0
  17. package/export-template/docs/assets/cold-start-timeline.DxC_Sc7w.svg +1 -0
  18. package/export-template/docs/assets/cold-start-timeline.dark.CZ17YcAG.svg +1 -0
  19. package/export-template/docs/assets/columnar-layout.PghGeOEA.svg +1 -0
  20. package/export-template/docs/assets/columnar-layout.dark.BVNlz0ff.svg +1 -0
  21. package/export-template/docs/assets/container-memory.DIO0AnIm.svg +1 -0
  22. package/export-template/docs/assets/container-memory.dark.CP-5zuCl.svg +1 -0
  23. package/export-template/docs/assets/contributor-guide_architecture_board-widgets.md.CWpj01WU.js +1 -0
  24. package/export-template/docs/assets/contributor-guide_architecture_board-widgets.md.CWpj01WU.lean.js +1 -0
  25. package/export-template/docs/assets/contributor-guide_architecture_detector-contract.md.CgzUsQ6W.js +1 -0
  26. package/export-template/docs/assets/contributor-guide_architecture_detector-contract.md.CgzUsQ6W.lean.js +1 -0
  27. package/export-template/docs/assets/contributor-guide_architecture_drill-down.md.BtPdlM7r.js +1 -0
  28. package/export-template/docs/assets/contributor-guide_architecture_drill-down.md.BtPdlM7r.lean.js +1 -0
  29. package/export-template/docs/assets/contributor-guide_architecture_impact-estimation.md.CooslVJt.js +1 -0
  30. package/export-template/docs/assets/contributor-guide_architecture_impact-estimation.md.CooslVJt.lean.js +1 -0
  31. package/export-template/docs/assets/contributor-guide_architecture_index.md.3TO9ic6w.js +1 -0
  32. package/export-template/docs/assets/contributor-guide_architecture_index.md.3TO9ic6w.lean.js +1 -0
  33. package/export-template/docs/assets/contributor-guide_architecture_overview.md.CehiRmGn.js +1 -0
  34. package/export-template/docs/assets/contributor-guide_architecture_overview.md.CehiRmGn.lean.js +1 -0
  35. package/export-template/docs/assets/contributor-guide_architecture_state-and-history.md.C-xxn0q7.js +1 -0
  36. package/export-template/docs/assets/contributor-guide_architecture_state-and-history.md.C-xxn0q7.lean.js +1 -0
  37. package/export-template/docs/assets/contributor-guide_architecture_widget-rendering.md.R27gQrgY.js +1 -0
  38. package/export-template/docs/assets/contributor-guide_architecture_widget-rendering.md.R27gQrgY.lean.js +1 -0
  39. package/export-template/docs/assets/contributor-guide_architecture_worker-protocol.md.IbnfNrV3.js +6 -0
  40. package/export-template/docs/assets/contributor-guide_architecture_worker-protocol.md.IbnfNrV3.lean.js +1 -0
  41. package/export-template/docs/assets/contributor-guide_contributing.md.CvRsdr6J.js +1 -0
  42. package/export-template/docs/assets/contributor-guide_contributing.md.CvRsdr6J.lean.js +1 -0
  43. package/export-template/docs/assets/contributor-guide_development-setup.md.DvAN_9mK.js +12 -0
  44. package/export-template/docs/assets/contributor-guide_development-setup.md.DvAN_9mK.lean.js +1 -0
  45. package/export-template/docs/assets/contributor-guide_testing.md.6rIKqSyY.js +1 -0
  46. package/export-template/docs/assets/contributor-guide_testing.md.6rIKqSyY.lean.js +1 -0
  47. package/export-template/docs/assets/dag-stages.DSz_S937.svg +1 -0
  48. package/export-template/docs/assets/dag-stages.dark.F72UzxH4.svg +1 -0
  49. package/export-template/docs/assets/driver-executor.D5pQ7YN1.svg +1 -0
  50. package/export-template/docs/assets/driver-executor.dark.BmX9cPvh.svg +1 -0
  51. package/export-template/docs/assets/duplicate-plan-subtree.B4cvN6fj.svg +1 -0
  52. package/export-template/docs/assets/duplicate-plan-subtree.dark.Dw8wS0Ag.svg +1 -0
  53. package/export-template/docs/assets/index.md.CHJVslga.js +1 -0
  54. package/export-template/docs/assets/index.md.CHJVslga.lean.js +1 -0
  55. package/export-template/docs/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
  56. package/export-template/docs/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
  57. package/export-template/docs/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
  58. package/export-template/docs/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
  59. package/export-template/docs/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
  60. package/export-template/docs/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
  61. package/export-template/docs/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
  62. package/export-template/docs/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
  63. package/export-template/docs/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
  64. package/export-template/docs/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
  65. package/export-template/docs/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
  66. package/export-template/docs/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
  67. package/export-template/docs/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
  68. package/export-template/docs/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
  69. package/export-template/docs/assets/join-strategy.C_FvrCEo.svg +1 -0
  70. package/export-template/docs/assets/join-strategy.dark.ChMLnNII.svg +1 -0
  71. package/export-template/docs/assets/memory-borrowing.BqQRJg0u.svg +1 -0
  72. package/export-template/docs/assets/memory-borrowing.dark.Yhh20O9C.svg +1 -0
  73. package/export-template/docs/assets/memory-regions.XHvO7jHG.svg +1 -0
  74. package/export-template/docs/assets/memory-regions.dark.D4TP9_08.svg +1 -0
  75. package/export-template/docs/assets/repartition-vs-coalesce.BovLRrpj.svg +1 -0
  76. package/export-template/docs/assets/repartition-vs-coalesce.dark.BhAczKZQ.svg +1 -0
  77. package/export-template/docs/assets/retry-escalation-ladder.DyTKJJmZ.svg +1 -0
  78. package/export-template/docs/assets/retry-escalation-ladder.dark.BdsabtU3.svg +1 -0
  79. package/export-template/docs/assets/shuffle-map-reduce.KuOEZVmg.svg +1 -0
  80. package/export-template/docs/assets/shuffle-map-reduce.dark.BgQZnFSb.svg +1 -0
  81. package/export-template/docs/assets/spill-classification.BU2euYDO.svg +1 -0
  82. package/export-template/docs/assets/spill-classification.dark.D7i1M40d.svg +1 -0
  83. package/export-template/docs/assets/style.DSixAiZE.css +1 -0
  84. package/export-template/docs/assets/tuning-reference_anti-patterns.md.Df1YMIHu.js +1 -0
  85. package/export-template/docs/assets/tuning-reference_anti-patterns.md.Df1YMIHu.lean.js +1 -0
  86. package/export-template/docs/assets/tuning-reference_aqe.md.BIsCtLzm.js +1 -0
  87. package/export-template/docs/assets/tuning-reference_aqe.md.BIsCtLzm.lean.js +1 -0
  88. package/export-template/docs/assets/tuning-reference_bottleneck-broadcast-sizing.md.CEstB3Ia.js +1 -0
  89. package/export-template/docs/assets/tuning-reference_bottleneck-broadcast-sizing.md.CEstB3Ia.lean.js +1 -0
  90. package/export-template/docs/assets/tuning-reference_bottleneck-cold-start.md.CEuy-72y.js +7 -0
  91. package/export-template/docs/assets/tuning-reference_bottleneck-cold-start.md.CEuy-72y.lean.js +1 -0
  92. package/export-template/docs/assets/tuning-reference_bottleneck-duplicate-plan-subtree.md.CIohQDfn.js +1 -0
  93. package/export-template/docs/assets/tuning-reference_bottleneck-duplicate-plan-subtree.md.CIohQDfn.lean.js +1 -0
  94. package/export-template/docs/assets/tuning-reference_bottleneck-failures.md.4z5BXGJ2.js +6 -0
  95. package/export-template/docs/assets/tuning-reference_bottleneck-failures.md.4z5BXGJ2.lean.js +1 -0
  96. package/export-template/docs/assets/tuning-reference_bottleneck-gc.md.DSxzZRK7.js +6 -0
  97. package/export-template/docs/assets/tuning-reference_bottleneck-gc.md.DSxzZRK7.lean.js +1 -0
  98. package/export-template/docs/assets/tuning-reference_bottleneck-job-failure-rate.md.BaJl__1W.js +8 -0
  99. package/export-template/docs/assets/tuning-reference_bottleneck-job-failure-rate.md.BaJl__1W.lean.js +1 -0
  100. package/export-template/docs/assets/tuning-reference_bottleneck-memory-utilization.md.DbP-SJZc.js +7 -0
  101. package/export-template/docs/assets/tuning-reference_bottleneck-memory-utilization.md.DbP-SJZc.lean.js +1 -0
  102. package/export-template/docs/assets/tuning-reference_bottleneck-retry-waste.md.D5JMjVOt.js +1 -0
  103. package/export-template/docs/assets/tuning-reference_bottleneck-retry-waste.md.D5JMjVOt.lean.js +1 -0
  104. package/export-template/docs/assets/tuning-reference_bottleneck-shuffle.md.CM-nTmIH.js +12 -0
  105. package/export-template/docs/assets/tuning-reference_bottleneck-shuffle.md.CM-nTmIH.lean.js +1 -0
  106. package/export-template/docs/assets/tuning-reference_bottleneck-skew.md.BdUwiDhn.js +14 -0
  107. package/export-template/docs/assets/tuning-reference_bottleneck-skew.md.BdUwiDhn.lean.js +1 -0
  108. package/export-template/docs/assets/tuning-reference_bottleneck-slow-host.md.BlIo6UDW.js +7 -0
  109. package/export-template/docs/assets/tuning-reference_bottleneck-slow-host.md.BlIo6UDW.lean.js +1 -0
  110. package/export-template/docs/assets/tuning-reference_bottleneck-small-files.md.B8kloyx8.js +5 -0
  111. package/export-template/docs/assets/tuning-reference_bottleneck-small-files.md.B8kloyx8.lean.js +1 -0
  112. package/export-template/docs/assets/tuning-reference_bottleneck-spill.md.PNH7mITt.js +6 -0
  113. package/export-template/docs/assets/tuning-reference_bottleneck-spill.md.PNH7mITt.lean.js +1 -0
  114. package/export-template/docs/assets/tuning-reference_bottleneck-straggler.md.DY36fHN5.js +7 -0
  115. package/export-template/docs/assets/tuning-reference_bottleneck-straggler.md.DY36fHN5.lean.js +1 -0
  116. package/export-template/docs/assets/tuning-reference_bottleneck-tiny-tasks.md.QTV7O8kU.js +8 -0
  117. package/export-template/docs/assets/tuning-reference_bottleneck-tiny-tasks.md.QTV7O8kU.lean.js +1 -0
  118. package/export-template/docs/assets/tuning-reference_bottleneck-utilization.md.DTiueZC3.js +5 -0
  119. package/export-template/docs/assets/tuning-reference_bottleneck-utilization.md.DTiueZC3.lean.js +1 -0
  120. package/export-template/docs/assets/tuning-reference_caching.md.B7aQ8asB.js +1 -0
  121. package/export-template/docs/assets/tuning-reference_caching.md.B7aQ8asB.lean.js +1 -0
  122. package/export-template/docs/assets/tuning-reference_cluster-config.md.ZVmDGsQ3.js +1 -0
  123. package/export-template/docs/assets/tuning-reference_cluster-config.md.ZVmDGsQ3.lean.js +1 -0
  124. package/export-template/docs/assets/tuning-reference_config.md.UvveiWG3.js +1 -0
  125. package/export-template/docs/assets/tuning-reference_config.md.UvveiWG3.lean.js +1 -0
  126. package/export-template/docs/assets/tuning-reference_data-formats.md.bjCAWH3N.js +1 -0
  127. package/export-template/docs/assets/tuning-reference_data-formats.md.bjCAWH3N.lean.js +1 -0
  128. package/export-template/docs/assets/tuning-reference_index.md.BQ_NooMV.js +1 -0
  129. package/export-template/docs/assets/tuning-reference_index.md.BQ_NooMV.lean.js +1 -0
  130. package/export-template/docs/assets/tuning-reference_intro.md.CobD-lGB.js +1 -0
  131. package/export-template/docs/assets/tuning-reference_intro.md.CobD-lGB.lean.js +1 -0
  132. package/export-template/docs/assets/tuning-reference_joins.md.BtKs_CuW.js +1 -0
  133. package/export-template/docs/assets/tuning-reference_joins.md.BtKs_CuW.lean.js +1 -0
  134. package/export-template/docs/assets/tuning-reference_memory-model.md.DhT-n4y3.js +1 -0
  135. package/export-template/docs/assets/tuning-reference_memory-model.md.DhT-n4y3.lean.js +1 -0
  136. package/export-template/docs/assets/tuning-reference_metrics.md.mLOh7Apj.js +1 -0
  137. package/export-template/docs/assets/tuning-reference_metrics.md.mLOh7Apj.lean.js +1 -0
  138. package/export-template/docs/assets/tuning-reference_partitioning.md.q0zKF_8X.js +1 -0
  139. package/export-template/docs/assets/tuning-reference_partitioning.md.q0zKF_8X.lean.js +1 -0
  140. package/export-template/docs/assets/tuning-reference_pyspark.md.DDCfvN9t.js +6 -0
  141. package/export-template/docs/assets/tuning-reference_pyspark.md.DDCfvN9t.lean.js +1 -0
  142. package/export-template/docs/assets/tuning-reference_shuffle.md.BZZ7R4Ix.js +1 -0
  143. package/export-template/docs/assets/tuning-reference_shuffle.md.BZZ7R4Ix.lean.js +1 -0
  144. package/export-template/docs/assets/tuning-reference_spark-architecture.md.Dwzm5avO.js +1 -0
  145. package/export-template/docs/assets/tuning-reference_spark-architecture.md.Dwzm5avO.lean.js +1 -0
  146. package/export-template/docs/assets/tuning-reference_table-formats.md.D6wj-2dX.js +1 -0
  147. package/export-template/docs/assets/tuning-reference_table-formats.md.D6wj-2dX.lean.js +1 -0
  148. package/export-template/docs/assets/udf-execution-models.BUFDICuG.svg +1 -0
  149. package/export-template/docs/assets/udf-execution-models.dark.YTNS6GDq.svg +1 -0
  150. package/export-template/docs/assets/user-guide_alternative-log-retrieval.md.B4tPGIal.js +1 -0
  151. package/export-template/docs/assets/user-guide_alternative-log-retrieval.md.B4tPGIal.lean.js +1 -0
  152. package/export-template/docs/assets/user-guide_getting-started.md.BJvwLEIM.js +3 -0
  153. package/export-template/docs/assets/user-guide_getting-started.md.BJvwLEIM.lean.js +1 -0
  154. package/export-template/docs/assets/user-guide_mcp-tools.md.Vi3RoflJ.js +125 -0
  155. package/export-template/docs/assets/user-guide_mcp-tools.md.Vi3RoflJ.lean.js +1 -0
  156. package/export-template/docs/assets/user-guide_run-comparison.md.CQc1aoU8.js +1 -0
  157. package/export-template/docs/assets/user-guide_run-comparison.md.CQc1aoU8.lean.js +1 -0
  158. package/export-template/docs/assets/user-guide_understanding-findings.md.DL1UDhvR.js +1 -0
  159. package/export-template/docs/assets/user-guide_understanding-findings.md.DL1UDhvR.lean.js +1 -0
  160. package/export-template/docs/contributor-guide/architecture/board-widgets.html +25 -0
  161. package/export-template/docs/contributor-guide/architecture/detector-contract.html +25 -0
  162. package/export-template/docs/contributor-guide/architecture/drill-down.html +25 -0
  163. package/export-template/docs/contributor-guide/architecture/impact-estimation.html +25 -0
  164. package/export-template/docs/contributor-guide/architecture/index.html +25 -0
  165. package/export-template/docs/contributor-guide/architecture/overview.html +25 -0
  166. package/export-template/docs/contributor-guide/architecture/state-and-history.html +25 -0
  167. package/export-template/docs/contributor-guide/architecture/widget-rendering.html +25 -0
  168. package/export-template/docs/contributor-guide/architecture/worker-protocol.html +30 -0
  169. package/export-template/docs/contributor-guide/contributing.html +25 -0
  170. package/export-template/docs/contributor-guide/development-setup.html +36 -0
  171. package/export-template/docs/contributor-guide/testing.html +25 -0
  172. package/export-template/docs/favicon.svg +4 -0
  173. package/export-template/docs/hashmap.json +1 -0
  174. package/export-template/docs/index.html +25 -0
  175. package/export-template/docs/package.json +1 -0
  176. package/export-template/docs/tuning-reference/anti-patterns.html +25 -0
  177. package/export-template/docs/tuning-reference/aqe.html +25 -0
  178. package/export-template/docs/tuning-reference/bottleneck-broadcast-sizing.html +25 -0
  179. package/export-template/docs/tuning-reference/bottleneck-cold-start.html +31 -0
  180. package/export-template/docs/tuning-reference/bottleneck-duplicate-plan-subtree.html +25 -0
  181. package/export-template/docs/tuning-reference/bottleneck-failures.html +30 -0
  182. package/export-template/docs/tuning-reference/bottleneck-gc.html +30 -0
  183. package/export-template/docs/tuning-reference/bottleneck-job-failure-rate.html +32 -0
  184. package/export-template/docs/tuning-reference/bottleneck-memory-utilization.html +31 -0
  185. package/export-template/docs/tuning-reference/bottleneck-retry-waste.html +25 -0
  186. package/export-template/docs/tuning-reference/bottleneck-shuffle.html +36 -0
  187. package/export-template/docs/tuning-reference/bottleneck-skew.html +38 -0
  188. package/export-template/docs/tuning-reference/bottleneck-slow-host.html +31 -0
  189. package/export-template/docs/tuning-reference/bottleneck-small-files.html +29 -0
  190. package/export-template/docs/tuning-reference/bottleneck-spill.html +30 -0
  191. package/export-template/docs/tuning-reference/bottleneck-straggler.html +31 -0
  192. package/export-template/docs/tuning-reference/bottleneck-tiny-tasks.html +32 -0
  193. package/export-template/docs/tuning-reference/bottleneck-utilization.html +29 -0
  194. package/export-template/docs/tuning-reference/caching.html +25 -0
  195. package/export-template/docs/tuning-reference/cluster-config.html +25 -0
  196. package/export-template/docs/tuning-reference/config.html +25 -0
  197. package/export-template/docs/tuning-reference/data-formats.html +25 -0
  198. package/export-template/docs/tuning-reference/index.html +25 -0
  199. package/export-template/docs/tuning-reference/intro.html +25 -0
  200. package/export-template/docs/tuning-reference/joins.html +25 -0
  201. package/export-template/docs/tuning-reference/memory-model.html +25 -0
  202. package/export-template/docs/tuning-reference/metrics.html +25 -0
  203. package/export-template/docs/tuning-reference/partitioning.html +25 -0
  204. package/export-template/docs/tuning-reference/pyspark.html +30 -0
  205. package/export-template/docs/tuning-reference/shuffle.html +25 -0
  206. package/export-template/docs/tuning-reference/spark-architecture.html +25 -0
  207. package/export-template/docs/tuning-reference/table-formats.html +25 -0
  208. package/export-template/docs/user-guide/alternative-log-retrieval.html +25 -0
  209. package/export-template/docs/user-guide/getting-started.html +27 -0
  210. package/export-template/docs/user-guide/mcp-tools.html +149 -0
  211. package/export-template/docs/user-guide/run-comparison.html +25 -0
  212. package/export-template/docs/user-guide/understanding-findings.html +25 -0
  213. package/export-template/docs/vp-icons.css +0 -0
  214. package/export-template/favicon.svg +4 -0
  215. package/export-template/index.html +111 -0
  216. package/export-template/parser-worker-DyjiQvfP.js +112 -0
  217. package/export-template/sample-runs/sample-run.ndjson.gz +0 -0
  218. package/package.json +20 -6
  219. package/vendor-core/analyzer.js +74 -74
  220. package/vendor-core/cli/budgets.js +13 -27
  221. package/vendor-core/cli/collect-run.js +43 -19
  222. package/vendor-core/core-count.js +25 -27
  223. package/vendor-core/core-locality-ratio.js +4 -11
  224. package/vendor-core/core-time-series.js +6 -12
  225. package/vendor-core/core-usage-locality.js +3 -4
  226. package/vendor-core/detectors.js +395 -389
  227. package/vendor-core/docs-config.js +69 -21
  228. package/vendor-core/docs-content/chapters/01-intro.md +32 -0
  229. package/vendor-core/docs-content/chapters/02-spark-architecture.md +76 -0
  230. package/vendor-core/docs-content/chapters/03-memory-model.md +73 -0
  231. package/vendor-core/docs-content/chapters/04-partitioning.md +65 -0
  232. package/vendor-core/docs-content/chapters/05-joins.md +62 -0
  233. package/vendor-core/docs-content/chapters/06-shuffle.md +59 -0
  234. package/vendor-core/docs-content/chapters/07-data-formats.md +81 -0
  235. package/vendor-core/docs-content/chapters/07b-table-formats.md +56 -0
  236. package/vendor-core/docs-content/chapters/08-caching.md +58 -0
  237. package/vendor-core/docs-content/chapters/09-pyspark.md +78 -0
  238. package/vendor-core/docs-content/chapters/10-aqe.md +167 -0
  239. package/vendor-core/docs-content/chapters/11-cluster-config.md +170 -0
  240. package/vendor-core/docs-content/chapters/12-anti-patterns.md +171 -0
  241. package/vendor-core/docs-content/chapters/14-metrics.md +87 -0
  242. package/vendor-core/docs-content/chapters/15-config.md +93 -0
  243. package/vendor-core/docs-content/chapters/nav-index.json +370 -0
  244. package/vendor-core/docs-content/detection/cache.md +7 -0
  245. package/vendor-core/docs-content/detection/cfg.md +15 -0
  246. package/vendor-core/docs-content/detection/chrn.md +9 -0
  247. package/vendor-core/docs-content/detection/cold.md +4 -0
  248. package/vendor-core/docs-content/detection/cstor.md +4 -0
  249. package/vendor-core/docs-content/detection/fail.md +5 -0
  250. package/vendor-core/docs-content/detection/gc.md +4 -0
  251. package/vendor-core/docs-content/detection/host.md +5 -0
  252. package/vendor-core/docs-content/detection/incmp.md +6 -0
  253. package/vendor-core/docs-content/detection/jobs.md +4 -0
  254. package/vendor-core/docs-content/detection/local.md +6 -0
  255. package/vendor-core/docs-content/detection/mem.md +10 -0
  256. package/vendor-core/docs-content/detection/part.md +5 -0
  257. package/vendor-core/docs-content/detection/plan.md +14 -0
  258. package/vendor-core/docs-content/detection/retry.md +4 -0
  259. package/vendor-core/docs-content/detection/sfail.md +5 -0
  260. package/vendor-core/docs-content/detection/shape.md +5 -0
  261. package/vendor-core/docs-content/detection/shfl.md +4 -0
  262. package/vendor-core/docs-content/detection/skew.md +6 -0
  263. package/vendor-core/docs-content/detection/slow.md +6 -0
  264. package/vendor-core/docs-content/detection/spec.md +8 -0
  265. package/vendor-core/docs-content/detection/spill.md +7 -0
  266. package/vendor-core/docs-content/detection/strag.md +5 -0
  267. package/vendor-core/docs-content/detection/tiny.md +4 -0
  268. package/vendor-core/docs-content/detection/util.md +4 -0
  269. package/vendor-core/docs-content/diagrams/aqe-loop.dark.svg +1 -0
  270. package/vendor-core/docs-content/diagrams/aqe-loop.svg +1 -0
  271. package/vendor-core/docs-content/diagrams/broadcast-vs-shuffle.dark.svg +1 -0
  272. package/vendor-core/docs-content/diagrams/broadcast-vs-shuffle.svg +1 -0
  273. package/vendor-core/docs-content/diagrams/cache-lifecycle.dark.svg +1 -0
  274. package/vendor-core/docs-content/diagrams/cache-lifecycle.svg +1 -0
  275. package/vendor-core/docs-content/diagrams/cold-start-timeline.dark.svg +1 -0
  276. package/vendor-core/docs-content/diagrams/cold-start-timeline.svg +1 -0
  277. package/vendor-core/docs-content/diagrams/columnar-layout.dark.svg +1 -0
  278. package/vendor-core/docs-content/diagrams/columnar-layout.svg +1 -0
  279. package/vendor-core/docs-content/diagrams/container-memory.dark.svg +1 -0
  280. package/vendor-core/docs-content/diagrams/container-memory.svg +1 -0
  281. package/vendor-core/docs-content/diagrams/dag-stages.dark.svg +1 -0
  282. package/vendor-core/docs-content/diagrams/dag-stages.svg +1 -0
  283. package/vendor-core/docs-content/diagrams/driver-executor.dark.svg +1 -0
  284. package/vendor-core/docs-content/diagrams/driver-executor.svg +1 -0
  285. package/vendor-core/docs-content/diagrams/duplicate-plan-subtree.dark.svg +1 -0
  286. package/vendor-core/docs-content/diagrams/duplicate-plan-subtree.svg +1 -0
  287. package/vendor-core/docs-content/diagrams/join-strategy.dark.svg +1 -0
  288. package/vendor-core/docs-content/diagrams/join-strategy.svg +1 -0
  289. package/vendor-core/docs-content/diagrams/memory-borrowing.dark.svg +1 -0
  290. package/vendor-core/docs-content/diagrams/memory-borrowing.svg +1 -0
  291. package/vendor-core/docs-content/diagrams/memory-regions.dark.svg +1 -0
  292. package/vendor-core/docs-content/diagrams/memory-regions.svg +1 -0
  293. package/vendor-core/docs-content/diagrams/repartition-vs-coalesce.dark.svg +1 -0
  294. package/vendor-core/docs-content/diagrams/repartition-vs-coalesce.svg +1 -0
  295. package/vendor-core/docs-content/diagrams/retry-escalation-ladder.dark.svg +1 -0
  296. package/vendor-core/docs-content/diagrams/retry-escalation-ladder.svg +1 -0
  297. package/vendor-core/docs-content/diagrams/shuffle-map-reduce.dark.svg +1 -0
  298. package/vendor-core/docs-content/diagrams/shuffle-map-reduce.svg +1 -0
  299. package/vendor-core/docs-content/diagrams/spill-classification.dark.svg +1 -0
  300. package/vendor-core/docs-content/diagrams/spill-classification.svg +1 -0
  301. package/vendor-core/docs-content/diagrams/udf-execution-models.dark.svg +1 -0
  302. package/vendor-core/docs-content/diagrams/udf-execution-models.svg +1 -0
  303. package/vendor-core/docs-content/tuning/broadcast-sizing.md +78 -0
  304. package/vendor-core/docs-content/tuning/cold-start.md +81 -0
  305. package/vendor-core/docs-content/tuning/duplicate-plan-subtree.md +45 -0
  306. package/vendor-core/docs-content/tuning/failures.md +124 -0
  307. package/vendor-core/docs-content/tuning/gc.md +110 -0
  308. package/vendor-core/docs-content/tuning/job-failure-rate.md +101 -0
  309. package/vendor-core/docs-content/tuning/memory-utilization.md +58 -0
  310. package/vendor-core/docs-content/tuning/retry-waste.md +90 -0
  311. package/vendor-core/docs-content/tuning/shuffle.md +154 -0
  312. package/vendor-core/docs-content/tuning/skew.md +123 -0
  313. package/vendor-core/docs-content/tuning/slow-host.md +117 -0
  314. package/vendor-core/docs-content/tuning/small-files.md +99 -0
  315. package/vendor-core/docs-content/tuning/spill.md +114 -0
  316. package/vendor-core/docs-content/tuning/straggler.md +103 -0
  317. package/vendor-core/docs-content/tuning/tiny-tasks.md +94 -0
  318. package/vendor-core/docs-content/tuning/utilization.md +90 -0
  319. package/vendor-core/docs-site-config.js +10 -17
  320. package/vendor-core/efficiency-model.js +7 -13
  321. package/vendor-core/etl-phases.js +3 -5
  322. package/vendor-core/event-handlers.js +232 -134
  323. package/vendor-core/event-schemas.js +48 -114
  324. package/vendor-core/evidence-availability.js +5 -10
  325. package/vendor-core/evidence-report.js +73 -123
  326. package/vendor-core/export-data.js +48 -0
  327. package/vendor-core/finding-action-label.js +4 -10
  328. package/vendor-core/finding-filter-predicate.js +3 -7
  329. package/vendor-core/finding-generic-recommendation.js +112 -0
  330. package/vendor-core/finding-names.js +51 -0
  331. package/vendor-core/format-utils.js +112 -38
  332. package/vendor-core/impact-band.js +18 -24
  333. package/vendor-core/impact-estimator.js +38 -74
  334. package/vendor-core/ingest.js +7 -13
  335. package/vendor-core/job-groups.js +3 -6
  336. package/vendor-core/list-runs.js +278 -0
  337. package/vendor-core/load-vendored.js +6 -12
  338. package/vendor-core/log-header-peek.js +81 -0
  339. package/vendor-core/lz4-block.js +4 -6
  340. package/vendor-core/mcp-server-factory.js +38 -8
  341. package/vendor-core/mcp-tools.js +105 -76
  342. package/vendor-core/model-assembler.js +8 -16
  343. package/vendor-core/occupancy.js +5 -9
  344. package/vendor-core/parser-worker.js +20 -29
  345. package/vendor-core/plan-dot.js +2 -5
  346. package/vendor-core/plan-duration-attribution.js +78 -29
  347. package/vendor-core/plan-graph-model.js +126 -69
  348. package/vendor-core/plan-node-detail.js +31 -17
  349. package/vendor-core/plan-summary.js +19 -8
  350. package/vendor-core/recommendation-rollup.js +35 -39
  351. package/vendor-core/redact.js +72 -16
  352. package/vendor-core/rolling-log-reassembly.js +4 -6
  353. package/vendor-core/run-comparison.js +86 -72
  354. package/vendor-core/scaling-sim.js +5 -7
  355. package/vendor-core/session-snapshot.js +1 -1
  356. package/vendor-core/shs-fetch.js +4 -6
  357. package/vendor-core/shs-load.js +9 -13
  358. package/vendor-core/shs-request.js +1 -1
  359. package/vendor-core/stage-quantiles.js +14 -0
  360. package/vendor-core/types.js +78 -18
  361. package/vendor-core/wasted-core-hours.js +7 -12
@@ -7,6 +7,8 @@
7
7
  // Deterministic (sorted assignment), idempotent (pseudonyms map to themselves),
8
8
  // and non-mutating (returns a fresh, deep-copied tree).
9
9
 
10
+
11
+
10
12
  // Host / IP identifier patterns. Used to enumerate host names that surface only
11
13
  // inside free text: recommendation strings, `stageFailed`'s failure-reason
12
14
  // value, SQL relation/node names, never as a structured `host` field, so
@@ -23,7 +25,7 @@ const HOST_PATTERNS = [
23
25
  // Spark application id (e.g. application_1690000000000_0001). Besides
24
26
  // redactReport's structured summary.app.id field, an app id can also surface
25
27
  // as a residual free-text token (e.g. embedded in a finding's evidence or
26
- // recommendation string) same as a host/IP token embedded in a stage name
28
+ // recommendation string), same as a host/IP token embedded in a stage name
27
29
  // or plan detail. redactComparison has no structured app-id field at all
28
30
  // (baselineLabel/candidateLabel are caller-supplied labels, not Spark app
29
31
  // ids), so free text is its *only* source of app ids.
@@ -57,19 +59,58 @@ function scanHostTokens(node , hosts ) {
57
59
  scanTokens(node, [{ patterns: HOST_PATTERNS, out: hosts }]);
58
60
  }
59
61
 
62
+ // Recursively collects every string value found under a key literally named
63
+ // `host`, anywhere in the tree. Host names surface at several depths, a
64
+ // finding's own `host`, `evidence.host`, and now
65
+ // `evidence.failedTaskDetails[].host` / `evidence.retriedTaskDetails[].host`,
66
+ // and only some of them are host-*pattern*-shaped (a plain FQDN like
67
+ // `worker-3.internal` matches neither HOST_PATTERN). Walking by key name rather
68
+ // than enumerating known paths keeps any future nested `host` field covered.
69
+ function collectHostFields(node , hosts ) {
70
+ if (Array.isArray(node)) {
71
+ for (const n of node) collectHostFields(n, hosts);
72
+ return;
73
+ }
74
+ if (node && typeof node === 'object') {
75
+ for (const [k, v] of Object.entries(node)) {
76
+ if (k === 'host' && typeof v === 'string' && v.length > 0) hosts.add(v);
77
+ else collectHostFields(v, hosts);
78
+ }
79
+ }
80
+ }
81
+
82
+ // Spark config keys ending in `host`/`hostname` (e.g. spark.driver.host,
83
+ // spark.yarn.am.hostname) carry plain FQDN host names that neither
84
+ // HOST_PATTERNS matches (no IP/EC2 shape) nor collectHostFields's by-key-name
85
+ // walk catches (the literal key is the dotted Spark property name, never
86
+ // `host` itself). app.config is a flat Record<string, string> unique to
87
+ // redactExportData: no other redact* export ships a raw Spark config dict.
88
+ // Known gap: a hostname value under a differently-named key isn't caught by
89
+ // this suffix check. Confirmed against a real cluster config: spark.master,
90
+ // spark.yarn.historyServer.address, and the plural YARN proxy/HA keys
91
+ // (...AmIpFilter.param.PROXY_HOSTS, ...RM_HA_URLS) all carry real hostnames
92
+ // through un-redacted today.
93
+ function collectConfigHostValues(config , hosts ) {
94
+ if (!config) return;
95
+ for (const [key, value] of Object.entries(config)) {
96
+ const lastSegment = key.slice(key.lastIndexOf('.') + 1).toLowerCase();
97
+ if ((lastSegment === 'host' || lastSegment === 'hostname') && value) hosts.add(value);
98
+ }
99
+ }
100
+
60
101
  // Known locations of the identifiers, so we don't have to guess which strings
61
- // are sensitive: the app id lives at summary.app.id; host names live on each
62
- // finding's `host` field. The real report nests that host under `evidence.host`
63
- // (host is not a first-class report column), so read both shapes, plus a
64
- // free-text scan for hosts/IPs and app ids that appear only inside string
65
- // values (e.g. a finding's evidence/recommendation text).
102
+ // are sensitive: the app id lives at summary.app.id; host names live under any
103
+ // `host` key in the findings tree (see collectHostFields), plus a free-text
104
+ // scan for hosts/IPs and app ids that appear only inside string values (e.g. a
105
+ // finding's evidence/recommendation text).
66
106
 
67
-
68
-
69
-
70
-
107
+
108
+
71
109
 
72
-
110
+
111
+
112
+
113
+
73
114
 
74
115
 
75
116
  function collectIds(report ) {
@@ -77,11 +118,7 @@ function collectIds(report )
77
118
  const hosts = new Set ();
78
119
  const appId = report?.summary?.app?.id;
79
120
  if (typeof appId === 'string' && appId.length > 0) appIds.add(appId);
80
- for (const f of report?.findings ?? []) {
81
- for (const h of [f?.host, f?.evidence?.host]) {
82
- if (typeof h === 'string' && h.length > 0) hosts.add(h);
83
- }
84
- }
121
+ collectHostFields(report?.findings, hosts);
85
122
  // Free-text scan for both host/IP tokens and app-id tokens: an app id can
86
123
  // surface in a finding's evidence/recommendation text (e.g. "retry app
87
124
  // application_1690000000000_0001 failed") same as redactComparison's scan.
@@ -173,3 +210,22 @@ export function redactComparison (comparison ) {
173
210
  scanTokens(comparison, [{ patterns: HOST_PATTERNS, out: hosts }, { patterns: APP_ID_PATTERNS, out: appIds }]);
174
211
  return applyReplacements(comparison, { appIds, hosts });
175
212
  }
213
+
214
+ // HTML-export counterpart to redactReport/redactComparison. Unlike
215
+ // redactReport (scoped to EvidenceReportJson's summary.app.id + findings),
216
+ // this also walks executors.added/removed for their literal `host` field
217
+ // (ExecutorAddedEvent.host), since raw executor records: not just findings
218
+ //: reach data.js.
219
+ export function redactExportData(data ) {
220
+ const appIds = new Set ();
221
+ const hosts = new Set ();
222
+ const appId = data.app?.id;
223
+ if (typeof appId === 'string' && appId.length > 0) appIds.add(appId);
224
+ collectHostFields(data.executors.added, hosts);
225
+ collectHostFields(data.executors.removed, hosts);
226
+ collectHostFields(data.catalog, hosts);
227
+ collectHostFields(data.configFindings, hosts);
228
+ collectConfigHostValues(data.app?.config, hosts);
229
+ scanTokens(data, [{ patterns: HOST_PATTERNS, out: hosts }, { patterns: APP_ID_PATTERNS, out: appIds }]);
230
+ return applyReplacements(data, { appIds, hosts });
231
+ }
@@ -1,9 +1,7 @@
1
- // Rolling event-log directory/zip member-name reassembly. Pulled out of
2
- // shs-fetch.ts (Task: bundle-size fix) so DropZone.tsx's drag-drop path can
3
- // import just this reassembly logic without pulling in the Zod schemas and
4
- // vendored zstd/gzip decompressors that the rest of shs-fetch.ts (and the
5
- // wider parser-worker.ts module graph) depend on. Keep this file free of any
6
- // import beyond plain JS/TS built-ins.
1
+ // Rolling event-log directory/zip member-name reassembly. Import-free (plain
2
+ // JS/TS built-ins only) so DropZone.tsx's drag-drop path can use it without
3
+ // pulling in the Zod schemas and vendored zstd/gzip decompressors that the rest
4
+ // of shs-fetch.ts depends on. Keep it that way.
7
5
 
8
6
  export function naturalCompare(a , b ) {
9
7
  const tokenize = (s ) => s.match(/(\d+)|(\D+)/g) ?? [s];
@@ -1,4 +1,3 @@
1
- // src/run-comparison.ts
2
1
  import { computeWallClock } from './wall-clock.js';
3
2
  import { normalizeDetail } from './detectors.js';
4
3
  import { captureSnapshot } from './session-snapshot.js';
@@ -20,7 +19,7 @@ import { captureSnapshot } from './session-snapshot.js';
20
19
 
21
20
 
22
21
 
23
-
22
+
24
23
 
25
24
 
26
25
 
@@ -36,16 +35,9 @@ export function normalizeStageName(name ) {
36
35
  .trim();
37
36
  }
38
37
 
39
- // cyrb53: fast, non-cryptographic 53-bit string hash, deterministic, portable
40
- // (runs in-browser worker and Node CLI, no crypto module dependency), fixed-
41
- // length hex output regardless of input size. src/analyzer.js already has a
42
- // small stable-hash helper (`fnv1a`, 32-bit) for deterministic finding ids,
43
- // but that runs once per finding; planTreeIdentity's `visit` below runs once
44
- // per PLAN NODE, for every stage, in both runs of a comparison: orders of
45
- // magnitude more hash calls, so the smaller 32-bit space's birthday-collision
46
- // risk is a real concern here in a way it isn't for finding ids. 53 bits keeps
47
- // collision probability negligible at that scale; adequate for a same-run
48
- // identity key, not a security boundary.
38
+ // cyrb53: fast, non-cryptographic, deterministic 53-bit string hash. Not a
39
+ // security boundary; 53 bits keeps collision risk negligible at the per-node,
40
+ // per-stage call volume planTreeIdentity/sqlNodeIdentity put through it.
49
41
  function cyrb53(str , seed = 0) {
50
42
  let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
51
43
  for (let i = 0; i < str.length; i++) {
@@ -59,38 +51,16 @@ function cyrb53(str , seed = 0) {
59
51
  }
60
52
 
61
53
  // Bottom-up, order-independent structural identity of a resolved plan tree:
62
- // each node folds its own normalized name and normalized `detail` with its
63
- // already-computed children's DIGESTS (children sorted, so sibling
64
- // reordering across runs (e.g. AQE picking a different broadcast side)
65
- // still matches), so two plans collide only when their whole shape AND every
66
- // node's normalized detail agree. `normalizeDetail` (src/detectors.js, built
67
- // for cachingOpportunity's composite join/union detection) strips expr ids,
68
- // `plan_id=`, codegen-stage numbers, and AQE's BuildLeft/BuildRight choice,
69
- // and canonicalizes commutative equality operands: exactly the run-to-run
70
- // noise a cross-run identity needs to ignore. Replaces the previous
71
- // flatten-and-sort-names identity, which ignored `detail` entirely (so a
72
- // join on one column and a join on another, with identical operator names,
73
- // collided) and ignored nesting (so two differently-shaped trees sharing a
74
- // node-name multiset also collided).
75
- //
76
- // Each node is encoded via JSON.stringify of its [name, detail, childDigests]
77
- // triple rather than an ad hoc delimiter-joined string: real Spark `detail`
78
- // text routinely contains its own `<`/`>`/`{`/`}`/`,` characters (e.g.
79
- // `ReadSchema: struct<id:bigint>`), so a hand-rolled delimiter scheme could
80
- // let two structurally different plans serialize to the same string;
81
- // JSON.stringify escapes/quotes each component so the encoding is
82
- // unambiguous. Critically, that JSON payload is then folded down to a fixed-
83
- // length `cyrb53` digest before being returned: the payload only ever
84
- // contains a node's own (short) name/detail plus an array of already-hashed,
85
- // fixed-length child digests, never a raw nested JSON blob. Embedding full
86
- // child identity STRINGS instead (this function's first version) made each
87
- // level re-stringify-and-escape the level below, so identity length grew
88
- // ~2^depth; real Spark plans routinely nest 20-60+ operators deep
89
- // (Scan→Filter→Project→HashAggregate→Exchange→Sort→Join, repeated per
90
- // join/aggregate), which blew up to megabytes-per-stage or a
91
- // `RangeError: Invalid string length` well within that range. Digest-folding
92
- // keeps per-node output size bounded by name+detail length plus a small
93
- // constant per child, independent of subtree depth.
54
+ // each node folds its normalized name/detail with its children's digests
55
+ // (children sorted, so AQE picking a different broadcast side still matches),
56
+ // so two plans collide only when their whole shape and every node's detail
57
+ // agree. `normalizeDetail` strips the run-to-run noise (expr ids, `plan_id=`,
58
+ // codegen numbers, AQE build-side choice, commutative-operand order). Each
59
+ // node folds to a fixed-length cyrb53 digest (JSON.stringify-encoded, so
60
+ // detail text containing `<`/`>`/`{`/`,` can't collide two different plans)
61
+ // instead of embedding full child identity strings, which would re-escape
62
+ // every level below and blow identity size up to ~2^depth on the 20-60+
63
+ // operator-deep plans real Spark produces.
94
64
  function planTreeIdentity(root ) {
95
65
  if (!root) return null;
96
66
  function visit(node ) {
@@ -100,12 +70,27 @@ function planTreeIdentity(root ) {
100
70
  return visit(root);
101
71
  }
102
72
 
103
- // Plan identity for the stage's SQL execution, so stage identity also requires
104
- // the SQL-node identity to agree. Empty string when no SQL/plan.
73
+ // Plan identity for the stage's SQL execution, scoped to only the plan nodes
74
+ // this stage actually ran (`node.stageIds`), not the whole tree: two stages
75
+ // sharing one SQL execution (e.g. a self-join's two Exchange stages) otherwise
76
+ // collapse onto one identity regardless of which part of the plan each ran.
77
+ // Falls back to the coarser whole-tree identity when the stage has no
78
+ // attributed nodes (hand-built snapshots without `stageIds`, or unmatched
79
+ // accumulables).
105
80
  function sqlNodeIdentity(stage , snapshot ) {
106
81
  const execId = stage.sqlExecutionId;
107
82
  if (execId == null) return '';
108
- return planTreeIdentity(snapshot.sql.get(execId)?.planTree ?? null) ?? '';
83
+ const root = snapshot.sql.get(execId)?.planTree ?? null;
84
+ if (!root) return '';
85
+ const fingerprints = [];
86
+ (function collect(node ) {
87
+ if (node.stageIds?.includes(stage.id)) {
88
+ fingerprints.push(JSON.stringify([normalizeStageName(node.name ?? ''), normalizeDetail(node.detail ?? '')]));
89
+ }
90
+ for (const child of node.children ?? []) collect(child);
91
+ })(root);
92
+ if (fingerprints.length === 0) return planTreeIdentity(root) ?? '';
93
+ return cyrb53(JSON.stringify(fingerprints.sort()));
109
94
  }
110
95
 
111
96
  export function stageIdentity(stage , snapshot ) {
@@ -113,7 +98,7 @@ export function stageIdentity(stage , snapshot ) {
113
98
  }
114
99
 
115
100
  function identityIndex(snapshot ) {
116
- const byIdentity = new Map (); // identity -> stageId[]
101
+ const byIdentity = new Map ();
117
102
  for (const [id, stage] of snapshot.stages) {
118
103
  const key = stageIdentity(stage, snapshot);
119
104
  const ids = byIdentity.get(key);
@@ -134,24 +119,35 @@ export function matchStages(baseSnap , candSnap
134
119
  const pairs = [];
135
120
  const matchedIdentities = new Set ();
136
121
  const collisionIdentities = new Set ();
137
- // A collision is a single-run property: any identity that more than one stage
138
- // in the SAME run normalizes to. Record them per-run, independent of whether
139
- // the other run shares that identity (a colliding stage with no counterpart
140
- // still makes the run ambiguous).
122
+ // A collision is a single-run property: more than one stage in the SAME run
123
+ // shares an identity. Recorded per-run regardless of whether the other run
124
+ // shares it too. Some collisions get resolved into `pairs` below and some
125
+ // don't, so this set means "was ambiguous", not "stayed unpaired".
141
126
  for (const idx of [baseIdx, candIdx])
142
127
  for (const [identity, ids] of idx) if (ids.length > 1) collisionIdentities.add(identity);
143
- // Pair only identities that map to exactly one stage on BOTH sides.
144
- // Deterministic order: sort identities lexically.
128
+ // An identity colliding equally on both sides has no genuine ambiguity about
129
+ // *count*, so pair its stages off positionally by sorted id rather than
130
+ // dropping them. This is exact when the identity actually distinguishes
131
+ // stages (e.g. self-comparing a run: every stage matches itself). It's a
132
+ // best-effort guess when the identity is coarse (no SQL/attribution) and
133
+ // the two sides are genuinely different runs -- two unrelated same-named
134
+ // stages could get cross-paired. Accepted tradeoff: dropping them instead
135
+ // would also sacrifice the exact-self-comparison case, which matters more.
145
136
  for (const identity of [...baseIdx.keys()].sort()) {
146
- if (!candIdx.has(identity)) continue;
137
+ const candIds = candIdx.get(identity);
138
+ if (!candIds) continue;
147
139
  const baseIds = baseIdx.get(identity) ;
148
- const candIds = candIdx.get(identity) ;
149
- if (baseIds.length > 1 || candIds.length > 1) continue; // collision already recorded above
150
- pairs.push({ identity, baseId: baseIds[0], candId: candIds[0] });
140
+ if (baseIds.length !== candIds.length) continue;
141
+ const sortedBaseIds = [...baseIds].sort((a, b) => a - b);
142
+ const sortedCandIds = [...candIds].sort((a, b) => a - b);
143
+ for (let i = 0; i < sortedBaseIds.length; i++) {
144
+ pairs.push({ identity, baseId: sortedBaseIds[i], candId: sortedCandIds[i] });
145
+ }
151
146
  matchedIdentities.add(identity);
152
147
  }
153
148
  const total = baseSnap.stages.size + candSnap.stages.size;
154
- const coverage = total === 0 ? 0 : (2 * pairs.length) / total;
149
+ // Both runs stage-less: nothing to compare, not "nothing matched".
150
+ const coverage = total === 0 ? 1 : (2 * pairs.length) / total;
155
151
  return { pairs, matchedIdentities, collisionIdentities, coverage };
156
152
  }
157
153
 
@@ -329,18 +325,19 @@ function namesConflict(a , b )
329
325
  return a?.name != null && b?.name != null && a.name !== b.name;
330
326
  }
331
327
 
332
- // A pair's skew ratio is null when the stage's duration wasn't measurable
333
- // (see `ratio` below), matching CompareRunsResult['stageSkew']'s nullable
334
- // baseline/candidate/delta fields.
328
+ // A pair's skew ratio is null when the stage's duration wasn't measurable (see
329
+ // `stageSkewRatio`). `baseId`/`candId` are carried through so a consumer can
330
+ // key rows uniquely: two pairs can share one `identity` (a same-run collision
331
+ // resolved positionally in matchStages), but never the same baseId+candId.
335
332
  function stageSkewDeltas(
336
333
  baseSnap ,
337
334
  candSnap ,
338
335
  match ,
339
- ) {
336
+ ) {
340
337
  return match.pairs.map((p) => {
341
338
  const b = stageSkewRatio(baseSnap.stages.get(p.baseId) );
342
339
  const c = stageSkewRatio(candSnap.stages.get(p.candId) );
343
- return { identity: p.identity, baseline: b, candidate: c, delta: b != null && c != null ? c - b : null };
340
+ return { identity: p.identity, baseId: p.baseId, candId: p.candId, baseline: b, candidate: c, delta: b != null && c != null ? c - b : null };
344
341
  }).sort((x, y) => x.identity < y.identity ? -1 : x.identity > y.identity ? 1 : 0);
345
342
  }
346
343
 
@@ -379,15 +376,13 @@ function stageList(snap )
379
376
  return [...snap.stages].map(([id, s]) => ({ id, name: s.name ?? `Stage ${id}`, metrics: perStageMetrics(s) }));
380
377
  }
381
378
 
382
- // Shared by every buildComparison caller below: captureSnapshot only ever
383
- // copies this into a fresh Map internally, never mutates the caller's
384
- // reference, so one shared empty Map is safe in place of a fresh allocation
385
- // per call.
379
+ // captureSnapshot copies this into a fresh Map internally and never mutates the
380
+ // caller's reference, so one shared empty Map is safe here.
386
381
  const EMPTY_TASK_DATA = new Map ();
387
382
 
388
383
  // Wraps the analyze()-to-compareRuns() snapshot-building sequence shared by
389
384
  // the CLI's --baseline path and mcp-tools.ts's compareRuns tool: both need
390
- // captureSnapshot (with an empty taskDataCache the interactive drill-down
385
+ // captureSnapshot (with an empty taskDataCache, the interactive drill-down
391
386
  // cache, never read by compareRuns/matchStages/metricDeltas/findingsDelta)
392
387
  // for each side before diffing them.
393
388
  export function buildComparison(
@@ -400,6 +395,14 @@ export function buildComparison(
400
395
  );
401
396
  }
402
397
 
398
+ // matchStages' coverage is a Dice coefficient: (2 * pairs.length) / (baseCount
399
+ // + candCount). Below 0.5, more than half of each run's stages went unpaired,
400
+ // so the stage-level rows (stageSkew, baseStages/candStages) mostly show
401
+ // unrelated work side by side rather than the same stage before/after -- the
402
+ // comparison is dominated by guesswork, not genuine pairing. 0.5 is thus the
403
+ // natural midpoint for "more matched than not," not an arbitrary tuning knob.
404
+ const LOW_COVERAGE_THRESHOLD = 0.5;
405
+
403
406
  export function compareRuns(
404
407
  baseline ,
405
408
  candidate ,
@@ -410,10 +413,21 @@ export function compareRuns(
410
413
  // is the normal way to label an A/B experiment, so a mismatch must not block
411
414
  // the (matching-free, name-independent) deltas. Surface it as `low` instead.
412
415
  const namesDiffer = namesConflict(baseSnap.app, candSnap.app);
416
+ // Coverage is the other half of the signal: identical names on two runs that
417
+ // barely share any stages are just as misleading as differing names on two
418
+ // runs that match well, so either condition alone drops confidence to `low`.
419
+ const lowCoverage = match.coverage < LOW_COVERAGE_THRESHOLD;
420
+ const reason = namesDiffer && lowCoverage
421
+ ? `Run names differ and only ${(match.coverage * 100).toFixed(0)}% of stages matched, so deltas may compare different work.`
422
+ : namesDiffer
423
+ ? 'Run names differ, so deltas may compare different work.'
424
+ : lowCoverage
425
+ ? `Only ${(match.coverage * 100).toFixed(0)}% of stages matched between runs, so per-stage rows mostly compare unrelated work.`
426
+ : null;
413
427
  return {
414
428
  baselineLabel: baseline.label, candidateLabel: candidate.label,
415
- confidence: namesDiffer ? 'low' : 'ok',
416
- reason: namesDiffer ? 'Run names differ, so deltas may compare different work.' : null,
429
+ confidence: namesDiffer || lowCoverage ? 'low' : 'ok',
430
+ reason,
417
431
  matchedCoverage: match.coverage,
418
432
  metrics: metricDeltas(baseSnap, candSnap),
419
433
  findings: findingsDelta(baseSnap, candSnap),
@@ -440,7 +454,7 @@ function renderFindingsSection(title , findings )
440
454
  export function renderComparisonMarkdown(comparison ) {
441
455
  const lines = ['', '## Comparison to baseline', ''];
442
456
  if (comparison.confidence === 'low') {
443
- lines.push(`- confidence: low ${comparison.reason}`);
457
+ lines.push(`- confidence: low, ${comparison.reason}`);
444
458
  lines.push('');
445
459
  }
446
460
  lines.push(`- matched stage coverage: ${(comparison.matchedCoverage * 100).toFixed(1)}%`);
@@ -1,4 +1,4 @@
1
- // §4 What-if executor-scaling simulator (SparkLens ExecutorWallclockAnalyzer).
1
+ // §4 What-if executor-scaling simulator.
2
2
  // DESIGN SPIKE: the makespan model is a first-cut approximation and is NOT
3
3
  // validated against ground truth (a single event log only ever observes one
4
4
  // scale). Every prediction ships with a Model Error confidence indicator.
@@ -42,13 +42,11 @@ export function simulateScaling({ app, stages, runAggregates, executorsAdded }
42
42
 
43
43
  {
44
44
  const perStage = runAggregates?.perStage ?? {};
45
- // `app ?? {}`: computeTotalCores just falls back to the executor-derived core sum when
46
- // `resources` is absent, and every caller here already tolerates a null `app` (malformed
47
- // logs with no ApplicationStart); `app!` would crash on `app.resources` in that case.
45
+ // `app ?? {}`: computeTotalCores falls back to the executor-derived core sum
46
+ // when `resources` is absent, and a null `app` (malformed log, no
47
+ // ApplicationStart) is real here; `app!` would crash on `app.resources`.
48
48
  // `executorsAdded` cast: computeTotalCores only reads `totalCores`, present on
49
- // ExecutorAddedEvent (the only kind real callers pass here) but not ExecutorRemovedEvent,
50
- // so the union as a whole is a structural mismatch against computeTotalCores's
51
- // `{totalCores?}` shape.
49
+ // ExecutorAddedEvent (the only kind passed here) but not on the union type.
52
50
  const baselineCores = computeTotalCores(app ?? {}, executorsAdded );
53
51
  const wc = computeWallClock(app, stages );
54
52
  const observedActiveMs = wc.stagesActive;
@@ -67,7 +67,7 @@ export function applySnapshot(
67
67
  appModel.jobs = new Map(snapshot.jobs);
68
68
  appModel.runAggregates = snapshot.runAggregates ?? null;
69
69
  // Fail-closed on unknown schema versions: a future incompatible ledger is
70
- // never trusted as V1 data (fixes: schemaVersion must be validated at read).
70
+ // never trusted as V1 data.
71
71
  appModel.evidenceAvailability = isSupportedEvidenceAvailability(snapshot.evidenceAvailability)
72
72
  ? snapshot.evidenceAvailability
73
73
  : null;
@@ -31,10 +31,9 @@ export function sniffCodec(bytes )
31
31
  // it (fzstd's `decompress()`, fflate's `gunzipSync()`) allocates that entire
32
32
  // size as a single ArrayBuffer up front, which can exceed what the browser
33
33
  // will allocate for a multi-GB event log ("Array buffer allocation failed").
34
- // fflate's Gunzip and fzstd's Decompress are untyped vendor JS (plain
35
- // prototype classes, not `class` declarations), so TS can't infer a
36
- // construct signature for them; this local shape is just enough to type
37
- // the two call sites below without touching the vendored files.
34
+ // fflate's Gunzip and fzstd's Decompress are untyped vendor JS, so TS can't
35
+ // infer a construct signature for them; this local shape types the call sites
36
+ // without touching the vendored files.
38
37
 
39
38
 
40
39
 
@@ -68,8 +67,7 @@ export async function runParseFromUrl(
68
67
  state ,
69
68
  { fetchImpl = fetch, emit = (msg ) => self.postMessage(msg) } = {}
70
69
  ) {
71
- // buildProxyRequestUrl is still-untyped JS (./shs-request.js, a plain
72
- // untyped .js sibling); `request`'s real shape isn't pinned down here.
70
+ // buildProxyRequestUrl is untyped JS; `request`'s real shape isn't pinned here.
73
71
  const url = buildProxyRequestUrl(request );
74
72
 
75
73
  let res;
@@ -61,12 +61,10 @@ async function readArchiveBytes(upstream , maxBytes , idleTimeou
61
61
  const reader = upstream.body .getReader();
62
62
  // Grown geometrically instead of preallocated at `maxBytes`: a typical
63
63
  // archive is nowhere near the cap, so starting small (or at the declared
64
- // content-length, when trustworthy) and doubling on demand keeps peak
65
- // memory close to the actual download size. Chunks are copied straight into
66
- // this buffer as they arrive rather than buffered in an array and copied
67
- // once at the end, so the previous ~2x-of-total peak (chunk array + a
68
- // freshly allocated same-size output buffer, both live during the final
69
- // copy) no longer happens.
64
+ // content-length, when trustworthy) and doubling on demand keeps peak memory
65
+ // close to the actual download size. Chunks are copied straight into this
66
+ // buffer as they arrive rather than buffered in an array and copied once at
67
+ // the end, which would keep ~2x-of-total live during the final copy.
70
68
  let buf = new Uint8Array(Number.isFinite(declared) && declared > 0 ? Math.min(declared, maxBytes) : 65536);
71
69
  let total = 0;
72
70
  for (;;) {
@@ -96,11 +94,9 @@ async function readArchiveBytes(upstream , maxBytes , idleTimeou
96
94
  return buf.subarray(0, total);
97
95
  }
98
96
 
99
- // ./proxy.js is a plain, untyped .js file whose exported fetchShsEventLog
100
- // TS can only infer a loose shape for. Its real (verified by reading
101
- // proxy.js) contract is this
102
- // discriminated union (either branch, never a mix), so it's asserted here
103
- // once at the boundary rather than widening every downstream read.
97
+ // fetchShsEventLog is untyped JS; its real contract is this discriminated union
98
+ // (either branch, never a mix), asserted here once at the boundary rather than
99
+ // widening every downstream read.
104
100
 
105
101
 
106
102
  export async function resolveFromShs(
@@ -108,7 +104,7 @@ export async function resolveFromShs(
108
104
  appId ,
109
105
  attemptId ,
110
106
  opts = {},
111
- ) {
107
+ ) {
112
108
  const { fetchImpl = fetch, maxArchiveBytes = DEFAULT_MAX_ARCHIVE_BYTES, idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS } = opts;
113
109
  const validated = validateShsRequest({ baseUrl: shsBaseUrl, appId, attemptId: attemptId ?? '' });
114
110
  if (!validated.request) throw mcpError('access-or-upstream-failure', 'Invalid SHS request parameters.');
@@ -117,5 +113,5 @@ export async function resolveFromShs(
117
113
  const zipBytes = await readArchiveBytes(fetched.upstream, maxArchiveBytes, idleTimeoutMs);
118
114
  const { appModel, skippedLines } = await collectShsAppModel(zipBytes);
119
115
  appModel.evidenceAvailability = deriveEvidenceAvailability(appModel, { skippedLines });
120
- return appModel;
116
+ return { appModel, skippedLines };
121
117
  }
@@ -19,7 +19,7 @@ function trimString(value) {
19
19
  return typeof value === 'string' ? value.trim() : '';
20
20
  }
21
21
 
22
- function normalizeBaseUrl(value) {
22
+ export function normalizeBaseUrl(value) {
23
23
  const baseUrl = trimString(value);
24
24
  if (!baseUrl || baseUrl.includes('?') || baseUrl.includes('#')) return null;
25
25
 
@@ -1,3 +1,5 @@
1
+
2
+
1
3
  // Single source of truth for the packed per-task numeric array: FIELDS (offset
2
4
  // constants), TASK_FIELD_NAMES (display labels), and the finalizeStage hot-loop
3
5
  // push order are all derived from this list. `prop` is the task record's real
@@ -22,6 +24,10 @@ const TASK_FIELD_DESCRIPTORS = [
22
24
 
23
25
  const STRIDE = TASK_FIELD_DESCRIPTORS.length;
24
26
 
27
+ // Mirrors event-handlers.ts's MAX_TASK_SAMPLES (kept as a separate constant to avoid a circular
28
+ // value import between the two modules: see that file's comment).
29
+ const MAX_TASK_SAMPLES = 20;
30
+
25
31
  export const FIELDS = Object.freeze({
26
32
  ...Object.fromEntries(TASK_FIELD_DESCRIPTORS.map((d, i) => [d.key, i])),
27
33
  STRIDE,
@@ -67,6 +73,7 @@ export function finalizeStage(
67
73
  const hostStats = new Map();
68
74
  const executorStats = new Map();
69
75
  const failureReasons = new Map();
76
+ const failedTaskSamples = [];
70
77
  const localityStats = new Map();
71
78
  let peakExecutionMemoryMax = 0;
72
79
 
@@ -75,6 +82,12 @@ export function finalizeStage(
75
82
  if (t.failed) {
76
83
  failedTasks++;
77
84
  if (t.reason) failureReasons.set(t.reason, (failureReasons.get(t.reason) ?? 0) + 1);
85
+ if (failedTaskSamples.length < MAX_TASK_SAMPLES) {
86
+ failedTaskSamples.push({
87
+ taskId: t.taskId, attemptNumber: t.attemptNumber, host: t.host, executorId: t.executorId,
88
+ reason: t.reason, peakExecMem: t.peakExecMem, memSpilled: t.memSpilled, shuffleWrite: t.shuffleWrite,
89
+ } );
90
+ }
78
91
  }
79
92
  if (t.speculative) speculativeTasks++;
80
93
  if (t.host) {
@@ -148,6 +161,7 @@ export function finalizeStage(
148
161
  const data = {
149
162
  ...stage,
150
163
  hostStats: hostStatsArr, executorStats: executorStatsArr, failureReasons: failureReasonsArr, localityStats: localityStatsArr, stragglerCount,
164
+ failedTaskSamples,
151
165
  peakExecutionMemoryMax,
152
166
  taskDurationP50: p50,
153
167
  taskDurationP95: p95,