sparkforensics-cli 0.1.0 → 0.2.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 (360) 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.CndaAS6v.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.DNY8bVcl.js +1 -0
  12. package/export-template/docs/assets/chunks/VPLocalSearchBox.yJbZbsEo.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.Df2VAG9w.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.B-OsL91z.js +1 -0
  24. package/export-template/docs/assets/contributor-guide_architecture_board-widgets.md.B-OsL91z.lean.js +1 -0
  25. package/export-template/docs/assets/contributor-guide_architecture_detector-contract.md.BOeH4d1J.js +1 -0
  26. package/export-template/docs/assets/contributor-guide_architecture_detector-contract.md.BOeH4d1J.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.DYCDPgkh.js +1 -0
  30. package/export-template/docs/assets/contributor-guide_architecture_impact-estimation.md.DYCDPgkh.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.m3S3UdMk.js +1 -0
  36. package/export-template/docs/assets/contributor-guide_architecture_state-and-history.md.m3S3UdMk.lean.js +1 -0
  37. package/export-template/docs/assets/contributor-guide_architecture_widget-rendering.md.DbqPf2OT.js +1 -0
  38. package/export-template/docs/assets/contributor-guide_architecture_widget-rendering.md.DbqPf2OT.lean.js +1 -0
  39. package/export-template/docs/assets/contributor-guide_architecture_worker-protocol.md.B93qJ_tT.js +6 -0
  40. package/export-template/docs/assets/contributor-guide_architecture_worker-protocol.md.B93qJ_tT.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.DXOMCXxn.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.sU3KGarf.js +1 -0
  151. package/export-template/docs/assets/user-guide_alternative-log-retrieval.md.sU3KGarf.lean.js +1 -0
  152. package/export-template/docs/assets/user-guide_getting-started.md.DtEM37MK.js +3 -0
  153. package/export-template/docs/assets/user-guide_getting-started.md.DtEM37MK.lean.js +1 -0
  154. package/export-template/docs/assets/user-guide_mcp-tools.md.C8MiIu7F.js +125 -0
  155. package/export-template/docs/assets/user-guide_mcp-tools.md.C8MiIu7F.lean.js +1 -0
  156. package/export-template/docs/assets/user-guide_run-comparison.md.S0TWWmLY.js +1 -0
  157. package/export-template/docs/assets/user-guide_run-comparison.md.S0TWWmLY.lean.js +1 -0
  158. package/export-template/docs/assets/user-guide_understanding-findings.md.D0R_Y-R2.js +1 -0
  159. package/export-template/docs/assets/user-guide_understanding-findings.md.D0R_Y-R2.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 +115 -0
  216. package/export-template/parser-worker-QqyEE4m9.js +64 -0
  217. package/package.json +16 -3
  218. package/vendor-core/analyzer.js +74 -74
  219. package/vendor-core/cli/budgets.js +13 -27
  220. package/vendor-core/cli/collect-run.js +43 -19
  221. package/vendor-core/core-count.js +25 -27
  222. package/vendor-core/core-locality-ratio.js +4 -11
  223. package/vendor-core/core-time-series.js +6 -12
  224. package/vendor-core/core-usage-locality.js +3 -4
  225. package/vendor-core/detectors.js +256 -375
  226. package/vendor-core/docs-config.js +69 -21
  227. package/vendor-core/docs-content/chapters/01-intro.md +32 -0
  228. package/vendor-core/docs-content/chapters/02-spark-architecture.md +76 -0
  229. package/vendor-core/docs-content/chapters/03-memory-model.md +73 -0
  230. package/vendor-core/docs-content/chapters/04-partitioning.md +65 -0
  231. package/vendor-core/docs-content/chapters/05-joins.md +62 -0
  232. package/vendor-core/docs-content/chapters/06-shuffle.md +59 -0
  233. package/vendor-core/docs-content/chapters/07-data-formats.md +81 -0
  234. package/vendor-core/docs-content/chapters/07b-table-formats.md +56 -0
  235. package/vendor-core/docs-content/chapters/08-caching.md +58 -0
  236. package/vendor-core/docs-content/chapters/09-pyspark.md +78 -0
  237. package/vendor-core/docs-content/chapters/10-aqe.md +167 -0
  238. package/vendor-core/docs-content/chapters/11-cluster-config.md +170 -0
  239. package/vendor-core/docs-content/chapters/12-anti-patterns.md +171 -0
  240. package/vendor-core/docs-content/chapters/14-metrics.md +87 -0
  241. package/vendor-core/docs-content/chapters/15-config.md +93 -0
  242. package/vendor-core/docs-content/chapters/nav-index.json +370 -0
  243. package/vendor-core/docs-content/detection/cache.md +6 -0
  244. package/vendor-core/docs-content/detection/cfg.md +15 -0
  245. package/vendor-core/docs-content/detection/chrn.md +7 -0
  246. package/vendor-core/docs-content/detection/cold.md +4 -0
  247. package/vendor-core/docs-content/detection/cstor.md +4 -0
  248. package/vendor-core/docs-content/detection/fail.md +5 -0
  249. package/vendor-core/docs-content/detection/gc.md +4 -0
  250. package/vendor-core/docs-content/detection/host.md +5 -0
  251. package/vendor-core/docs-content/detection/incmp.md +6 -0
  252. package/vendor-core/docs-content/detection/jobs.md +4 -0
  253. package/vendor-core/docs-content/detection/local.md +7 -0
  254. package/vendor-core/docs-content/detection/mem.md +10 -0
  255. package/vendor-core/docs-content/detection/part.md +5 -0
  256. package/vendor-core/docs-content/detection/plan.md +14 -0
  257. package/vendor-core/docs-content/detection/retry.md +4 -0
  258. package/vendor-core/docs-content/detection/sfail.md +5 -0
  259. package/vendor-core/docs-content/detection/shape.md +5 -0
  260. package/vendor-core/docs-content/detection/shfl.md +4 -0
  261. package/vendor-core/docs-content/detection/skew.md +6 -0
  262. package/vendor-core/docs-content/detection/slow.md +6 -0
  263. package/vendor-core/docs-content/detection/spec.md +7 -0
  264. package/vendor-core/docs-content/detection/spill.md +7 -0
  265. package/vendor-core/docs-content/detection/strag.md +5 -0
  266. package/vendor-core/docs-content/detection/tiny.md +4 -0
  267. package/vendor-core/docs-content/detection/util.md +4 -0
  268. package/vendor-core/docs-content/diagrams/aqe-loop.dark.svg +1 -0
  269. package/vendor-core/docs-content/diagrams/aqe-loop.svg +1 -0
  270. package/vendor-core/docs-content/diagrams/broadcast-vs-shuffle.dark.svg +1 -0
  271. package/vendor-core/docs-content/diagrams/broadcast-vs-shuffle.svg +1 -0
  272. package/vendor-core/docs-content/diagrams/cache-lifecycle.dark.svg +1 -0
  273. package/vendor-core/docs-content/diagrams/cache-lifecycle.svg +1 -0
  274. package/vendor-core/docs-content/diagrams/cold-start-timeline.dark.svg +1 -0
  275. package/vendor-core/docs-content/diagrams/cold-start-timeline.svg +1 -0
  276. package/vendor-core/docs-content/diagrams/columnar-layout.dark.svg +1 -0
  277. package/vendor-core/docs-content/diagrams/columnar-layout.svg +1 -0
  278. package/vendor-core/docs-content/diagrams/container-memory.dark.svg +1 -0
  279. package/vendor-core/docs-content/diagrams/container-memory.svg +1 -0
  280. package/vendor-core/docs-content/diagrams/dag-stages.dark.svg +1 -0
  281. package/vendor-core/docs-content/diagrams/dag-stages.svg +1 -0
  282. package/vendor-core/docs-content/diagrams/driver-executor.dark.svg +1 -0
  283. package/vendor-core/docs-content/diagrams/driver-executor.svg +1 -0
  284. package/vendor-core/docs-content/diagrams/duplicate-plan-subtree.dark.svg +1 -0
  285. package/vendor-core/docs-content/diagrams/duplicate-plan-subtree.svg +1 -0
  286. package/vendor-core/docs-content/diagrams/join-strategy.dark.svg +1 -0
  287. package/vendor-core/docs-content/diagrams/join-strategy.svg +1 -0
  288. package/vendor-core/docs-content/diagrams/memory-borrowing.dark.svg +1 -0
  289. package/vendor-core/docs-content/diagrams/memory-borrowing.svg +1 -0
  290. package/vendor-core/docs-content/diagrams/memory-regions.dark.svg +1 -0
  291. package/vendor-core/docs-content/diagrams/memory-regions.svg +1 -0
  292. package/vendor-core/docs-content/diagrams/repartition-vs-coalesce.dark.svg +1 -0
  293. package/vendor-core/docs-content/diagrams/repartition-vs-coalesce.svg +1 -0
  294. package/vendor-core/docs-content/diagrams/retry-escalation-ladder.dark.svg +1 -0
  295. package/vendor-core/docs-content/diagrams/retry-escalation-ladder.svg +1 -0
  296. package/vendor-core/docs-content/diagrams/shuffle-map-reduce.dark.svg +1 -0
  297. package/vendor-core/docs-content/diagrams/shuffle-map-reduce.svg +1 -0
  298. package/vendor-core/docs-content/diagrams/spill-classification.dark.svg +1 -0
  299. package/vendor-core/docs-content/diagrams/spill-classification.svg +1 -0
  300. package/vendor-core/docs-content/diagrams/udf-execution-models.dark.svg +1 -0
  301. package/vendor-core/docs-content/diagrams/udf-execution-models.svg +1 -0
  302. package/vendor-core/docs-content/tuning/broadcast-sizing.md +78 -0
  303. package/vendor-core/docs-content/tuning/cold-start.md +81 -0
  304. package/vendor-core/docs-content/tuning/duplicate-plan-subtree.md +45 -0
  305. package/vendor-core/docs-content/tuning/failures.md +124 -0
  306. package/vendor-core/docs-content/tuning/gc.md +110 -0
  307. package/vendor-core/docs-content/tuning/job-failure-rate.md +101 -0
  308. package/vendor-core/docs-content/tuning/memory-utilization.md +58 -0
  309. package/vendor-core/docs-content/tuning/retry-waste.md +90 -0
  310. package/vendor-core/docs-content/tuning/shuffle.md +154 -0
  311. package/vendor-core/docs-content/tuning/skew.md +123 -0
  312. package/vendor-core/docs-content/tuning/slow-host.md +117 -0
  313. package/vendor-core/docs-content/tuning/small-files.md +99 -0
  314. package/vendor-core/docs-content/tuning/spill.md +114 -0
  315. package/vendor-core/docs-content/tuning/straggler.md +103 -0
  316. package/vendor-core/docs-content/tuning/tiny-tasks.md +94 -0
  317. package/vendor-core/docs-content/tuning/utilization.md +90 -0
  318. package/vendor-core/docs-site-config.js +10 -17
  319. package/vendor-core/efficiency-model.js +7 -13
  320. package/vendor-core/etl-phases.js +3 -5
  321. package/vendor-core/event-handlers.js +232 -134
  322. package/vendor-core/event-schemas.js +48 -114
  323. package/vendor-core/evidence-availability.js +5 -10
  324. package/vendor-core/evidence-report.js +72 -122
  325. package/vendor-core/export-data.js +48 -0
  326. package/vendor-core/finding-action-label.js +4 -10
  327. package/vendor-core/finding-filter-predicate.js +3 -7
  328. package/vendor-core/finding-generic-recommendation.js +112 -0
  329. package/vendor-core/finding-names.js +51 -0
  330. package/vendor-core/format-utils.js +112 -38
  331. package/vendor-core/impact-band.js +18 -24
  332. package/vendor-core/impact-estimator.js +38 -74
  333. package/vendor-core/ingest.js +7 -13
  334. package/vendor-core/job-groups.js +3 -6
  335. package/vendor-core/list-runs.js +278 -0
  336. package/vendor-core/load-vendored.js +6 -12
  337. package/vendor-core/log-header-peek.js +81 -0
  338. package/vendor-core/lz4-block.js +4 -6
  339. package/vendor-core/mcp-server-factory.js +38 -8
  340. package/vendor-core/mcp-tools.js +105 -76
  341. package/vendor-core/model-assembler.js +8 -16
  342. package/vendor-core/occupancy.js +5 -9
  343. package/vendor-core/parser-worker.js +18 -27
  344. package/vendor-core/plan-dot.js +2 -5
  345. package/vendor-core/plan-duration-attribution.js +78 -29
  346. package/vendor-core/plan-graph-model.js +126 -69
  347. package/vendor-core/plan-node-detail.js +31 -17
  348. package/vendor-core/plan-summary.js +19 -8
  349. package/vendor-core/recommendation-rollup.js +35 -39
  350. package/vendor-core/redact.js +72 -16
  351. package/vendor-core/rolling-log-reassembly.js +4 -6
  352. package/vendor-core/run-comparison.js +65 -70
  353. package/vendor-core/scaling-sim.js +5 -7
  354. package/vendor-core/session-snapshot.js +1 -1
  355. package/vendor-core/shs-fetch.js +4 -6
  356. package/vendor-core/shs-load.js +9 -13
  357. package/vendor-core/shs-request.js +1 -1
  358. package/vendor-core/stage-quantiles.js +14 -0
  359. package/vendor-core/types.js +78 -18
  360. package/vendor-core/wasted-core-hours.js +7 -12
@@ -1,5 +1,6 @@
1
- import { resolve as resolvePath } from 'node:path';
2
- import { existsSync, statSync } from 'node:fs';
1
+ import { resolve as resolvePath, join, dirname } from 'node:path';
2
+ import { existsSync, statSync, readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
3
4
  import { randomUUID } from 'node:crypto';
4
5
  import { collectRun } from './cli/collect-run.js';
5
6
  import { deriveEvidenceAvailability } from './evidence-availability.js';
@@ -11,45 +12,31 @@ import { computeWallClock } from './wall-clock.js';
11
12
  import { analyze } from './analyzer.js';
12
13
  import { buildComparison, renderComparisonMarkdown, } from './run-comparison.js';
13
14
  import { evaluateBudgets, } from './cli/budgets.js';
15
+ import { FINDING_NAMES, titleCase } from './finding-names.js';
16
+ import { docAnchorForType, tuningDocSlugForAnchor, pageForAnchor } from './docs-config.js';
17
+ import { typeTag } from './format-utils.js';
14
18
 
15
19
 
16
20
 
17
- // NOTE on a deviation from the plan's literal text: the plan defines RunRef as
18
- // `{ runId?: string } & Partial<RunSource>` (path/shsBaseUrl/appId/attemptId
19
- // flattened directly onto the object). Every real call site disagrees with
20
- // that shape: resolveOrCreateRun's own destructuring (`{ source, runId } = {}`
21
- // below), both mcp-server-factory.ts call sites (`compareRuns({ runId: runIdA,
22
- // source: sourceA }, ...)`), and every resolveOrCreateRun/compareRuns call in
23
- // tests/mcp-tools.test.js all pass a *nested* `source` key, never a flattened
24
- // path/shsBaseUrl directly on the ref object. Widened to match that real,
25
- // long-standing shape instead of reshaping every working call site (or every
26
- // test) to fit the literal text.
21
+ // RunRef uses a nested `source` key, matching every real call site (resolveOrCreateRun's
22
+ // destructuring, both mcp-server-factory.ts sites, and the tests all pass a nested `source`,
23
+ // never flattened path/shsBaseUrl).
27
24
 
28
25
 
29
26
 
30
-
31
-
32
-
33
-
27
+
34
28
 
35
29
 
36
30
 
37
31
 
38
-
39
-
40
-
41
-
42
-
32
+
33
+
34
+
43
35
 
44
36
 
45
- // The plan's Produces list types compareRuns's return as `CompareRunsResult`
46
- // (run-comparison.ts's raw comparison shape: baselineLabel/candidateLabel/
47
- // stageSkew/baseStages/candStages/...). compareRuns below actually returns a
48
- // distinct, smaller MCP-facing projection of that (runIdA/runIdB/
49
- // findingsDelta/metricDeltas/confidence/reason/matchedCoverage); it never
50
- // had baselineLabel, stageSkew, baseStages, or candStages. Introduced this
51
- // interface to match the real returned shape rather than casting a literal
52
- // that's actually missing several required CompareRunsResult properties.
37
+ // compareRuns returns a smaller MCP-facing projection of CompareRunsResult
38
+ // (runIdA/runIdB/findingsDelta/metricDeltas/confidence/reason/matchedCoverage), not the full raw
39
+ // shape (no baselineLabel/stageSkew/baseStages/candStages).
53
40
 
54
41
 
55
42
 
@@ -65,9 +52,8 @@ function envInt(name , fallback ) {
65
52
  return Number.isFinite(v) && v > 0 ? v : fallback;
66
53
  }
67
54
 
68
- // `touch()` below does a full Map delete+re-insert per cache hit to maintain
69
- // LRU order: O(n) per touch, fine at this small cap but worth re-checking
70
- // the cost if this cap is ever raised significantly.
55
+ // touch() below does a full Map delete+re-insert per cache hit to maintain LRU order: O(n) per
56
+ // touch, fine at this small cap but worth re-checking if the cap is raised significantly.
71
57
  const CACHE_CAP = envInt('SPARKFORENSICS_MCP_CACHE_CAP', 8);
72
58
  const CACHE_TTL_MS = envInt('SPARKFORENSICS_MCP_CACHE_TTL_MS', 15 * 60 * 1000);
73
59
 
@@ -80,10 +66,9 @@ const pendingByCacheKey = new Map
80
66
  export function pathCacheKey(path ) {
81
67
  const resolved = resolvePath(path);
82
68
  if (!existsSync(resolved)) throw mcpError('invalid-event-log', `No such file: ${resolved}`);
83
- // Size narrows a same-millisecond mtime collision; ctime narrows the case
84
- // where a copy tool (rsync --preserve-times, tar) restores an identical
85
- // mtime and size for different content: ctime can't be set by the copying
86
- // tool, so it still reflects the real time the file landed on disk.
69
+ // Size narrows a same-millisecond mtime collision; ctime narrows the case where a copy tool
70
+ // (rsync --preserve-times, tar) restores an identical mtime+size for different content: ctime
71
+ // can't be set by the copying tool, so it still reflects when the file landed on disk.
87
72
  const { mtimeMs, ctimeMs, size } = statSync(resolved);
88
73
  return `path:${resolved}:${mtimeMs}:${ctimeMs}:${size}`;
89
74
  }
@@ -94,8 +79,7 @@ function shsCacheKey({ shsBaseUrl, appId, attemptId }
94
79
 
95
80
  function touch(runId ) {
96
81
  const entry = byRunId.get(runId);
97
- // Defensive only for type-narrowing: every call site below touches a runId
98
- // it (or getCachedAppModel) has already confirmed present.
82
+ // Defensive only for type-narrowing: every call site touches a runId already confirmed present.
99
83
  if (!entry) return;
100
84
  byRunId.delete(runId);
101
85
  entry.lastAccess = Date.now();
@@ -119,8 +103,7 @@ function evictStale() {
119
103
  function evictOverflow() {
120
104
  while (byRunId.size > CACHE_CAP) {
121
105
  const oldestId = byRunId.keys().next().value;
122
- // Defensive only for type-narrowing: the while condition guarantees
123
- // byRunId is non-empty here, so .keys().next() always has a value.
106
+ // Defensive only for type-narrowing: the while condition guarantees byRunId is non-empty here.
124
107
  if (oldestId === undefined) break;
125
108
  const entry = byRunId.get(oldestId);
126
109
  if (entry) deleteRunEntry(oldestId, entry);
@@ -152,11 +135,8 @@ export async function resolveOrCreateRun(
152
135
  }
153
136
  if (!source) throw mcpError('access-or-upstream-failure', 'Provide either source or runId.');
154
137
 
155
- // `'path' in source`, not `source.path`: RunSource's two variants share no
156
- // common property, so a plain `.path` access doesn't type-check on the
157
- // union. Equivalent at runtime to the original truthy check for every real
158
- // caller (a path source's `path` is always a non-empty resolved filesystem
159
- // path; it's never the empty string).
138
+ // `'path' in source`, not `source.path`: RunSource's two variants share no common property, so a
139
+ // plain `.path` access doesn't type-check on the union. Equivalent at runtime for every real caller.
160
140
  const cacheKey = 'path' in source ? pathCacheKey(source.path) : shsCacheKey(source);
161
141
  const cached = byCacheKey.get(cacheKey);
162
142
  if (cached && byRunId.has(cached)) {
@@ -164,11 +144,9 @@ export async function resolveOrCreateRun(
164
144
  return { runId: cached, appModel: byRunId.get(cached) .appModel };
165
145
  }
166
146
 
167
- // Concurrent calls for the same not-yet-cached source (e.g. two tool calls
168
- // in flight for the same path) must await one shared parse rather than each
169
- // racing to insert their own cache entry: otherwise every racer's insert
170
- // after the first is an orphaned entry (unreachable via cacheKey, wasting a
171
- // cache slot until TTL/overflow).
147
+ // Concurrent calls for the same not-yet-cached source must await one shared parse, not each race
148
+ // to insert its own entry: otherwise every racer's insert after the first is orphaned (unreachable
149
+ // via cacheKey, wasting a cache slot until TTL/overflow).
172
150
  const pending = pendingByCacheKey.get(cacheKey);
173
151
  if (pending) return pending;
174
152
 
@@ -176,7 +154,7 @@ export async function resolveOrCreateRun(
176
154
  try {
177
155
  const appModel = 'path' in source
178
156
  ? await resolveFromPath(source.path)
179
- : await resolveFromShs(source.shsBaseUrl, source.appId, source.attemptId, { fetchImpl, maxArchiveBytes, idleTimeoutMs });
157
+ : (await resolveFromShs(source.shsBaseUrl, source.appId, source.attemptId, { fetchImpl, maxArchiveBytes, idleTimeoutMs })).appModel;
180
158
 
181
159
  const newRunId = randomUUID();
182
160
  byRunId.set(newRunId, { appModel, cacheKey, lastAccess: Date.now() });
@@ -212,6 +190,68 @@ export function diagnoseRun(runId , opts
212
190
  };
213
191
  }
214
192
 
193
+ const DOCS_CONTENT_DIR = join(dirname(fileURLToPath(import.meta.url)), 'docs-content');
194
+
195
+ // Both vendored corpora lead with their own heading line; strips the leading '#'/'###' markers, an
196
+ // optional `` `TAG`: `` prefix (the detection doc's heading shape), and a trailing `{#anchor}`,
197
+ // leaving just the human title. The tuning doc's plain `# Title` heading passes through as no-ops.
198
+ function extractDocTitle(markdown ) {
199
+ const firstLine = markdown.split('\n', 1)[0] ?? '';
200
+ return firstLine
201
+ .replace(/^#+\s*/, '')
202
+ .replace(/`[A-Z]+`:\s*/, '')
203
+ .replace(/\s*\{#[a-z0-9-]+\}\s*$/, '')
204
+ .trim();
205
+ }
206
+
207
+
208
+
209
+
210
+
211
+
212
+
213
+
214
+ /** Detection + tuning reference documentation for one finding `type`, independent of any run
215
+ * (documentation is a property of the type: a client fetches it once per type and caches it). */
216
+ export function getFindingDocumentation(type ) {
217
+ const label = FINDING_NAMES[type];
218
+ if (label === undefined) throw mcpError('invalid-type', `Unknown finding type: ${type}`);
219
+
220
+ const tag = typeTag(type);
221
+ const detectionContent = readFileSync(join(DOCS_CONTENT_DIR, 'detection', `${tag.toLowerCase()}.md`), 'utf8');
222
+ const detectionDoc = { tag, title: extractDocTitle(detectionContent), content: detectionContent };
223
+
224
+ const anchor = docAnchorForType(type);
225
+ const slug = anchor ? tuningDocSlugForAnchor(anchor) : null;
226
+ const tuningPath = slug ? join(DOCS_CONTENT_DIR, 'tuning', `${slug}.md`) : null;
227
+ let tuningDoc = null;
228
+ if (tuningPath && anchor && existsSync(tuningPath)) {
229
+ const tuningContent = readFileSync(tuningPath, 'utf8');
230
+ tuningDoc = { anchor, title: extractDocTitle(tuningContent), content: tuningContent };
231
+ }
232
+
233
+ return { type, name: titleCase(label), detectionDoc, tuningDoc };
234
+ }
235
+
236
+ const CHAPTERS_NAV_FILE = join(DOCS_CONTENT_DIR, 'chapters', 'nav-index.json');
237
+
238
+
239
+
240
+ /** Full tuning-reference markdown for one doc anchor, run-independent. Resolves the anchor to its
241
+ * owning page via pageForAnchor (so '#metric-task-duration' returns the 'metrics' page), looks it up
242
+ * in the committed nav-index, and reads the markdown from the same docs-content store the website
243
+ * renders from. The general-chapter counterpart to getFindingDocumentation (keyed by finding type). */
244
+ export function getReferenceDoc(anchor ) {
245
+ const page = pageForAnchor(String(anchor).replace(/^#/, ''));
246
+ const nav = JSON.parse(readFileSync(CHAPTERS_NAV_FILE, 'utf8'))
247
+ ;
248
+ const entry = nav.find((e) => e.anchor === page);
249
+ if (!entry) throw mcpError('invalid-anchor', `Unknown reference anchor: ${anchor}`);
250
+ const dir = entry.store === 'tuning' ? 'tuning' : 'chapters';
251
+ const content = readFileSync(join(DOCS_CONTENT_DIR, dir, `${entry.slug}.md`), 'utf8');
252
+ return { anchor: page, title: entry.title, content };
253
+ }
254
+
215
255
  export function getFindingEvidence(
216
256
  runId , findingId , opts ,
217
257
  ) {
@@ -230,10 +270,9 @@ export function getRunSummary(runId , opts )
230
270
  const appModel = getCachedAppModel(runId);
231
271
  const { app, stages, jobs, sql, executors } = appModel;
232
272
  const durationMs = hasCompleteInterval(app) ? computeWallClock(app, stages).total : null;
233
- // No buildEvidenceReport call here to redact (see the type comment above),
234
- // so reuse redact.ts's app-id + host-token pseudonymization directly via
235
- // redactAppIdentity(). Passing name/sparkVersion through it (not just id)
236
- // matters because app.name is free text and can itself carry a host/IP token.
273
+ // No buildEvidenceReport call here to redact, so reuse redact.ts's app-id + host-token
274
+ // pseudonymization directly. Passing name/sparkVersion (not just id) matters: app.name is free
275
+ // text and can itself carry a host/IP token.
237
276
  const rawApp = { id: app?.id ?? null, name: app?.name ?? null, sparkVersion: app?.sparkVersion ?? null };
238
277
  const redactedApp = opts?.redact ? redactAppIdentity(rawApp) : rawApp;
239
278
  return {
@@ -248,8 +287,8 @@ export function getRunSummary(runId , opts )
248
287
  };
249
288
  }
250
289
 
251
- // Shared by compareRuns and evaluateBudgetsForRun: both need a resolved run's
252
- // finding catalog (same analyze() call shape) before doing anything else with it.
290
+ // Shared by compareRuns and evaluateBudgetsForRun: both need a resolved run's finding catalog
291
+ // (same analyze() call shape) first.
253
292
  async function resolveAndAnalyze(ref ) {
254
293
  const { runId, appModel } = await resolveOrCreateRun(ref);
255
294
  const catalog = analyze(
@@ -267,23 +306,16 @@ export async function compareRuns(
267
306
  { runId: runIdB, appModel: appModelB, catalog: catalogB },
268
307
  ] = await Promise.all([resolveAndAnalyze(a), resolveAndAnalyze(b)]);
269
308
 
270
- // buildComparison's captureSnapshot step uses an empty taskDataCache: that
271
- // arg only feeds the interactive stage-detail drill-down modal, which none
272
- // of compareRuns/matchStages/metricDeltas/findingsDelta/stageSkewDeltas/
273
- // stageList read. Findings/metrics come from `catalog` (already computed
274
- // via `analyze()` above) and from `appModel.stages`/`sql` (which already
275
- // carry plan-tree data), both fully populated regardless. Produces
276
- // identical comparison output to the dashboard's session-cache snapshots;
277
- // the MCP tool just never exposes per-task drill-down, so there's nothing
278
- // to prefetch into it.
309
+ // buildComparison's captureSnapshot uses an empty taskDataCache: that arg only feeds the
310
+ // interactive stage-detail drill-down, which none of compare/matchStages/metricDeltas/findingsDelta
311
+ // read. Findings/metrics come from `catalog` and appModel.stages/sql, both fully populated. Produces
312
+ // identical output to the dashboard; the MCP tool just never exposes per-task drill-down.
279
313
  const built = buildComparison(
280
314
  { label: runIdA, appModel: appModelA, catalog: catalogA },
281
315
  { label: runIdB, appModel: appModelB, catalog: catalogB },
282
316
  );
283
- // Stage names throughout `built` (findings.introduced/resolved[].stages,
284
- // baseStages/candStages[].name) carry raw Spark stage text, which can embed
285
- // a host/IP token as free text — the same residual redactReport() already
286
- // scrubs from the evidence report.
317
+ // Stage names throughout `built` carry raw Spark stage text, which can embed a host/IP token as
318
+ // free text, the same residual redactReport() already scrubs from the evidence report.
287
319
  const result = opts?.redact ? redactComparison(built) : built;
288
320
 
289
321
  return {
@@ -303,12 +335,9 @@ export async function evaluateBudgetsForRun(
303
335
  budgets ,
304
336
  secondary ,
305
337
  ) {
306
- // Mirrors the CLI's own --regression-metric/--max-regression-pct pairing
307
- // guard (bin/sparkforensics-analyze.mjs): unlike the CLI, this tool never
308
- // defaults regressionMetric on the caller's behalf, so seeing it set here
309
- // unambiguously means the caller asked for a regression check and forgot
310
- // the threshold — evaluateBudgets() would otherwise skip the check with no
311
- // signal at all.
338
+ // Mirrors the CLI's --regression-metric/--max-regression-pct pairing guard: unlike the CLI, this
339
+ // tool never defaults regressionMetric, so seeing it set here means the caller asked for a
340
+ // regression check and forgot the threshold, evaluateBudgets() would otherwise skip it silently.
312
341
  if (budgets.regressionMetric !== undefined && budgets.maxRegressionPct === undefined) {
313
342
  throw mcpError('access-or-upstream-failure', 'regressionMetric requires maxRegressionPct.');
314
343
  }
@@ -9,9 +9,8 @@
9
9
 
10
10
 
11
11
 
12
- // Worker-message appModel assembly. Shared by file-load and SHS-URL-load paths,
13
- // which previously carried byte-identical callback blocks. Pure model mutation:
14
- // analysis/render/persist stay in the caller via the onDone/onProgress/onError hooks.
12
+ // Worker-message -> appModel assembly. Shared by the file-load and SHS-URL-load paths. Pure model
13
+ // mutation: analysis/render/persist stay in the caller via the onDone/onProgress/onError hooks.
15
14
  export function createModelCallbacks(
16
15
  appModel ,
17
16
  { onProgress, onDone, onError }
@@ -28,15 +27,10 @@ export function createModelCallbacks(
28
27
  appModel.stages.set(stage.id, stage);
29
28
  },
30
29
  onSql(data ) {
31
- // Full overwrite, not a merge: safe only because `planTree` (set by
32
- // onSqlPlan below, from a separate 'sqlPlan' message) is never present
33
- // on a 'sql' message's data. That in turn relies on
34
- // SparkListenerSQLAdaptiveExecutionUpdate always preceding
35
- // SparkListenerSQLExecutionEnd for the same execution (Spark emits a
36
- // re-plan mid-run, never after the query finishes); see
37
- // applyAdaptiveExecutionUpdate in event-handlers.ts. A future producer
38
- // of 'sql' messages that could arrive after onSqlPlan would need this
39
- // to merge instead of overwrite.
30
+ // Full overwrite, not a merge: safe only because planTree (set by onSqlPlan from a separate
31
+ // 'sqlPlan' message) is never present on a 'sql' message's data. That relies on
32
+ // SQLAdaptiveExecutionUpdate always preceding SQLExecutionEnd (Spark re-plans mid-run, never
33
+ // after the query finishes). A 'sql' producer that could arrive after onSqlPlan would need to merge.
40
34
  const event = data ;
41
35
  appModel.sql.set(event.id, data );
42
36
  for (const stageId of (event.stageIds ?? [])) {
@@ -59,10 +53,8 @@ export function createModelCallbacks(
59
53
  appModel.jobs.set(job.id, job);
60
54
  },
61
55
  onRunAggregates(data ) { appModel.runAggregates = data ; },
62
- // Patch per-stage executorMetrics posted once before `done`: see the
63
- // parser worker's SparkListenerStageExecutorMetrics handler (those events
64
- // arrive after StageCompleted, so the stage message itself carried an
65
- // empty map). `data` is a Map<stageId, Map<execId, metrics>>.
56
+ // Patch per-stage executorMetrics posted once before `done`: those events arrive after
57
+ // StageCompleted, so the stage message itself carried an empty map. `data` is Map<stageId, Map<execId, metrics>>.
66
58
  onStageExecutorMetrics(data ) {
67
59
  if (!(data instanceof Map)) return;
68
60
  const metricsByStage = data ;
@@ -1,9 +1,6 @@
1
- // src/occupancy.ts
2
- // Occupancy-weighted wall-clock attribution (N1 redesign). Replaces the old
3
- // CPM pass over parentIds for per-finding impact estimation. No graph, no
4
- // parentIds traversal: every stage's wall-clock claim is apportioned purely
5
- // from its own observed window and how much it overlapped with other
6
- // stages.
1
+ // Occupancy-weighted wall-clock attribution for per-finding impact: every
2
+ // stage's wall-clock claim is apportioned purely from its own observed window
3
+ // and how much it overlapped with other stages. No graph, no parentIds traversal.
7
4
  import { mergeIntervals } from './wall-clock.js';
8
5
 
9
6
 
@@ -18,9 +15,8 @@ function durationMs(s ) {
18
15
  return (s.completedAt ?? 0) - (s.submittedAt ?? 0);
19
16
  }
20
17
 
21
- // Average-concurrency proxy, held constant across the stage's whole active
22
- // window: no per-task timestamps exist outside the parser worker to do
23
- // better (see the redesign spec's "Explicitly rejected" section).
18
+ // Average-concurrency proxy, held constant across the stage's active window:
19
+ // no per-task timestamps exist outside the parser worker to do better.
24
20
  function coreWeight(s ) {
25
21
  const d = durationMs(s);
26
22
  return d > 0 ? (s.executorRunTime ?? 0) / d : 0;
@@ -24,29 +24,23 @@ export {
24
24
 
25
25
  export { naturalCompare, reassembleRollingEntries } from './rolling-log-reassembly.js';
26
26
 
27
- // 512 KB rather than a larger round number: Spark event logs commonly
28
- // compress ~15-20x, so a coarser chunk decompresses into a burst of tens of
29
- // thousands of lines reported at one unmoving pct before jumping: smaller
30
- // reads give the progress bar more checkpoints to advance through instead.
27
+ // 512 KB, not a larger round number: Spark logs commonly compress ~15-20x, so a
28
+ // coarser chunk decompresses into a burst of tens of thousands of lines at one
29
+ // unmoving pct; smaller reads give the progress bar more checkpoints.
31
30
  const CHUNK_SIZE = 512 * 1024;
32
31
  const MIN_PROGRESS_STEPS = 100;
33
- // Emitting every 2000 lines was the other half of the stair-step: a highly
34
- // compressed chunk can decode thousands of lines in one go, so 2000 was
35
- // rarely a mid-chunk boundary: it just meant a handful of progress
36
- // messages for the whole file, each landing on a different currentPct.
32
+ // Small emit interval: a highly compressed chunk decodes thousands of lines at
33
+ // once, so a large interval yields only a handful of progress messages per file.
37
34
  const PROGRESS_EMIT_LINES = 300;
38
35
 
39
36
 
40
37
 
41
38
 
42
- // Minimal shape streamFile/runParse/runParseFiles actually read off `file`
43
- // (name, size, slice(start,end).arrayBuffer()): narrower than the full DOM
44
- // `File` interface. A real `File` (the browser Worker path, via
45
- // WorkerIncomingMessage below) satisfies this structurally, but so does the
46
- // plain object src/cli/collect-run.ts's nodeFileFromPath builds for Node
47
- // (which has no DOM `File` constructor to build a real one from). Widened
48
- // from the plan's literal `File` to match that real second caller rather
49
- // than forcing a cast at its call site.
39
+ // Minimal shape streamFile/runParse/runParseFiles read off `file` (name, size,
40
+ // slice(start,end).arrayBuffer()): narrower than the full DOM `File`. A real
41
+ // `File` (the browser Worker path) satisfies it structurally, but so does the
42
+ // plain object src/cli/collect-run.ts's nodeFileFromPath builds for Node, which
43
+ // has no DOM `File` constructor.
50
44
 
51
45
 
52
46
 
@@ -60,11 +54,9 @@ const PROGRESS_EMIT_LINES = 300;
60
54
 
61
55
 
62
56
 
63
- // fflate's Gunzip and fzstd's Decompress are untyped vendor JS (plain
64
- // prototype classes, not `class` declarations), so TS can't infer a
65
- // construct signature for them; this local shape is just enough to type
66
- // the two call sites below without touching the vendored files (mirrors
67
- // shs-fetch.ts's identical StreamingDecoder shim).
57
+ // fflate's Gunzip and fzstd's Decompress are untyped vendor JS, so TS can't
58
+ // infer a construct signature for them; this local shape types the call sites
59
+ // without touching the vendored files (mirrors shs-fetch.ts's shim).
68
60
 
69
61
 
70
62
 
@@ -73,7 +65,7 @@ const PROGRESS_EMIT_LINES = 300;
73
65
  // it becomes available. Shared by runParse (single file) and runParseFiles
74
66
  // (rolling multi-file directories): the codec is sniffed per-file since
75
67
  // Spark's file-rolling never spans a compressor's own framing.
76
- async function streamFile(
68
+ export async function streamFile(
77
69
  file ,
78
70
  onChunk ,
79
71
  chunkSize ,
@@ -116,11 +108,10 @@ async function streamFile(
116
108
  }
117
109
 
118
110
  // Parse a dropped `File`, always streaming to honor the core invariant: the
119
- // decompressed task-event stream must never be buffered whole. The file is read
120
- // in `chunkSize` slices; a compressed log (gzip, Zstandard, Spark LZ4Block, or
121
- // Spark's Snappy framing) is inflated incrementally so only one decompressed
122
- // chunk is live at a time, exactly like the uncompressed path.
123
- // `chunkSize` is injectable purely so tests can force multi-chunk streaming.
111
+ // decompressed task-event stream must never be buffered whole. Read in
112
+ // `chunkSize` slices; a compressed log is inflated incrementally so only one
113
+ // decompressed chunk is live at a time, like the uncompressed path.
114
+ // `chunkSize` is injectable so tests can force multi-chunk streaming.
124
115
  export async function runParse(
125
116
  file ,
126
117
  state ,
@@ -1,6 +1,4 @@
1
- // Graphviz DOT export of a resolved planTree (sparkDoctor SqlPlanDotWriter).
2
- // Plain string building, no dependency, no metric annotation yet (deferred,
3
- // matching sparkDoctor's own unimplemented roadmap item).
1
+ // Graphviz DOT export of a resolved planTree. No metric annotation.
4
2
 
5
3
  import { walkPlanTree } from './plan-tree-walk.js';
6
4
 
@@ -17,8 +15,7 @@ export function planTreeToDot(planTree , { title = 'plan' }
17
15
  idOf.set(node, id);
18
16
  const label = node.detail && node.detail !== node.name ? `${node.name}\\n${node.detail}` : node.name;
19
17
  lines.push(` ${id} [label="${esc(label)}"];`);
20
- // Pre-order guarantee: parent's id is already assigned by the time a
21
- // child is visited, so a single walk covers both node labels and edges.
18
+ // Pre-order: parent's id is assigned before any child, so one walk covers labels and edges.
22
19
  if (parent) edges.push(` ${idOf.get(parent)} -> ${id};`);
23
20
  }, { dedupe: true });
24
21
  return [...lines, ...edges, '}'].join('\n');
@@ -1,14 +1,12 @@
1
1
  // Approximate stage wall-time back to plan operators, splitting the tree at
2
2
  // Exchange boundaries into segments and zipping segments (deepest-first) to
3
3
  // submission-ordered stage IDs. No exact ground truth exists in Spark's event
4
- // model: this is inference (dataflint's GraphDurationAttribution approach).
4
+ // model: this is inference.
5
5
  import { walkPlanTree } from './plan-tree-walk.js';
6
6
 
7
7
 
8
8
 
9
9
 
10
- function isExchange(node ) { return /exchange/i.test(node.name); }
11
-
12
10
  function timingMs(node ) {
13
11
  for (const m of (node.metrics ?? [])) {
14
12
  if (m.metricType === 'timing') return m.value;
@@ -42,7 +40,7 @@ export function computeSegments(planTree )
42
40
  if (!parent) {
43
41
  seg = 0;
44
42
  segmentTopology.set(seg, { parentIndex: null, depth: 0, traversalOrder: nextTraversalOrder++ });
45
- } else if (isExchange(parent)) {
43
+ } else if (parent.exchangeRole === 'read') {
46
44
  const parentIndex = segOf.get(parent) ;
47
45
  seg = nextSegment++;
48
46
  segmentTopology.set(seg, {
@@ -116,20 +114,16 @@ function componentDistance(fromIndex , toIndex , segmentTopology
116
114
  return Number.POSITIVE_INFINITY;
117
115
  }
118
116
 
119
- // For *display* purposes only (the "Stage N" label on a segment's group box
120
- // in the plan graph): a segment past zipSegmentsToStages' Math.min(...)
121
- // cutoff has no timing data of its own to pair on, but the physical plan
122
- // still places it structurally near a component that DID win a stage slot
123
- // (most often a small broadcast-builder component immediately upstream of the
124
- // join component that consumes it). Inherit the structurally nearest strictly
125
- // paired component's stage id rather than leaving its box an unexplained
126
- // "Stage —". Distance is the number of parent/child edges in the component
127
- // tree, never numeric id distance. Neighbor search is always against the
128
- // ORIGINAL strict pairs, never against other already-filled components, so
129
- // labels don't drift through a chain of inherited-from-inherited guesses.
130
- // zipSegmentsToStages/attributeStageDurationToPlan are untouched by this and
131
- // keep their own strict pairing: duration attribution must never double-
132
- // count one stage's wall time across two segments.
117
+ // Display-only: the "Stage N" label on a segment's group box. A segment past
118
+ // zipSegmentsToStages' Math.min cutoff has no timing to pair on, but the plan
119
+ // still places it structurally near a component that won a stage slot (often a
120
+ // broadcast-builder just upstream of the join consuming it). Inherit the
121
+ // nearest strictly-paired component's stage id rather than showing "Stage —".
122
+ // Distance counts parent/child edges in the component tree, not numeric id
123
+ // distance. Neighbor search is always against the ORIGINAL strict pairs, not
124
+ // other filled components, so labels don't drift through inherited-from-
125
+ // inherited guesses. Duration attribution keeps its own strict pairing and
126
+ // must never double-count one stage's wall time across two segments.
133
127
  export function mapSegmentsToStagesForDisplay(
134
128
  segments ,
135
129
  stagesById ,
@@ -156,19 +150,10 @@ export function mapSegmentsToStagesForDisplay(
156
150
  return map;
157
151
  }
158
152
 
159
- export function attributeStageDurationToPlan(
160
- planTree ,
161
- stagesById ,
162
- sqlExec ,
153
+ function computeExclusiveSharesForPairs(
154
+ pairs ,
163
155
  ) {
164
156
  const out = new Map ();
165
- const stageIds = sqlExec?.stageIds ?? [];
166
- if (!planTree || stageIds.length === 0) return out;
167
-
168
- const { segments, segmentTopology } = computeSegments(planTree);
169
- const typedStagesById = stagesById ;
170
- const { pairs } = zipSegmentsToStages(segments, typedStagesById, stageIds, segmentTopology);
171
-
172
157
  for (const { nodes, stage } of pairs) {
173
158
  const { submittedAt, completedAt } = stage ;
174
159
  const wall = Math.max(0, completedAt - submittedAt);
@@ -183,3 +168,67 @@ export function attributeStageDurationToPlan(
183
168
  }
184
169
  return out;
185
170
  }
171
+
172
+ export function attributeStageDurationToPlan(
173
+ planTree ,
174
+ stagesById ,
175
+ sqlExec ,
176
+ ) {
177
+ const stageIds = sqlExec?.stageIds ?? [];
178
+ if (!planTree || stageIds.length === 0) return new Map();
179
+ const { segments, segmentTopology } = computeSegments(planTree);
180
+ const typedStagesById = stagesById ;
181
+ const { pairs } = zipSegmentsToStages(segments, typedStagesById, stageIds, segmentTopology);
182
+ return computeExclusiveSharesForPairs(pairs);
183
+ }
184
+
185
+ // Exclusive share plus every descendant present in the same segment's node
186
+ // list. "Same segment" is what keeps this from leaking across an Exchange
187
+ // boundary: computeSegments already cuts a new segment at each write half,
188
+ // so a node's descendants outside its own segment are simply not in
189
+ // `inSegment` and get excluded by construction.
190
+ export function attributeStageDurationToPlanInclusive(
191
+ planTree ,
192
+ stagesById ,
193
+ sqlExec ,
194
+ ) {
195
+ const stageIds = sqlExec?.stageIds ?? [];
196
+ const out = new Map ();
197
+ if (!planTree || stageIds.length === 0) return out;
198
+ const { segments, segmentTopology } = computeSegments(planTree);
199
+ const typedStagesById = stagesById ;
200
+ const { pairs } = zipSegmentsToStages(segments, typedStagesById, stageIds, segmentTopology);
201
+ const exclusive = computeExclusiveSharesForPairs(pairs);
202
+
203
+ for (const { nodes } of pairs) {
204
+ const inSegment = new Set(nodes);
205
+ for (const node of nodes) {
206
+ // `visited` is fresh per top-level node, not shared across the `nodes`
207
+ // loop: a diamond-shared descendant's value, when computed while
208
+ // suppressed by ANOTHER node's own traversal (to avoid double-counting
209
+ // within THAT node's rollup), is not valid to reuse for the shared
210
+ // descendant's own independent top-level entry, those are different
211
+ // questions ("what does this contribute to node X's total" vs. "what
212
+ // is this node's own total in isolation"). This also rules out a
213
+ // node->total memo shared across top-level calls: a cached total
214
+ // computed unsuppressed (as its own top-level entry) would, if reused
215
+ // while this node is a descendant of a DIFFERENT ancestor being
216
+ // suppressed for double-counting, smuggle a shared descendant's share
217
+ // back in twice - see the diamond-shape regression test. Losing
218
+ // memoization across different top-level calls is fine: these trees
219
+ // are small and this was never a performance-critical path.
220
+ const visited = new Set ();
221
+ function inclusiveOf(n ) {
222
+ if (visited.has(n)) return 0;
223
+ visited.add(n);
224
+ let total = exclusive.get(n) ?? 0;
225
+ for (const child of n.children ?? []) {
226
+ if (inSegment.has(child)) total += inclusiveOf(child);
227
+ }
228
+ return total;
229
+ }
230
+ out.set(node, inclusiveOf(node));
231
+ }
232
+ }
233
+ return out;
234
+ }