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
@@ -0,0 +1,278 @@
1
+ import { existsSync, readdirSync, statSync } from 'node:fs';
2
+ import { resolve as resolvePath, join } from 'node:path';
3
+ import { nodeLazyPeekableFromPath, isRollingLogDirectory } from './cli/collect-run.js';
4
+ import { reassembleRollingEntries } from './parser-worker.js';
5
+ import { peekLogHeader } from './log-header-peek.js';
6
+ import { mcpError } from './mcp-error.js';
7
+ import { normalizeBaseUrl } from './shs-request.js';
8
+ import { DEFAULT_IDLE_TIMEOUT_MS, DEFAULT_MAX_ARCHIVE_BYTES } from './shs-load.js';
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+ // Local-mode-only convention: a plain file named `<name>_<n>.<ext>` is one attempt of a
33
+ // multi-attempt run (e.g. `external-local-1430917381535_1.ndjson`), the `_<n>` disambiguating two
34
+ // otherwise-identical appIds. Deliberately anchored on a file extension so a rolling-log
35
+ // directory's own name (no extension) never matches this convention.
36
+ const LOCAL_ATTEMPT_SUFFIX_RE = /^.+_(\d+)\.[^./\\]+$/;
37
+
38
+ const DEFAULT_MAX_RESULTS = 100;
39
+
40
+ // SHS's own timestamp format ends in a literal "GMT" suffix instead of "Z"/an offset, which
41
+ // Date.parse cannot parse (returns NaN), normalize before storing, so minDate/maxDate filtering
42
+ // (which relies on Date.parse) actually works, and so RunListEntry.startTime has one parseable
43
+ // ISO format across both local and SHS mode.
44
+ function normalizeShsTimestamp(raw ) {
45
+ if (!Number.isNaN(Date.parse(raw))) return new Date(raw).toISOString();
46
+ if (raw.endsWith('GMT')) {
47
+ const iso = `${raw.slice(0, -3)}Z`;
48
+ if (!Number.isNaN(Date.parse(iso))) return new Date(iso).toISOString();
49
+ }
50
+ return undefined; // genuinely unparseable: omit rather than propagate garbage
51
+ }
52
+
53
+ // Shared by local and SHS mode: both mapping steps produce a flat RunListEntry[] before this runs.
54
+ export function applyFiltersAndCap(entries , filters ) {
55
+ // Date.parse of an unparseable string returns NaN, and every comparison against NaN is false,
56
+ // left unchecked, a typo'd minDate/maxDate would silently filter out every run rather than
57
+ // reporting the mistake, indistinguishable from a correctly-filtered "no runs matched" result.
58
+ const min = filters.minDate ? Date.parse(filters.minDate) : undefined;
59
+ if (min != null && Number.isNaN(min)) {
60
+ throw mcpError('invalid-date-filter', `minDate is not a parseable date: ${filters.minDate}`);
61
+ }
62
+ const max = filters.maxDate ? Date.parse(filters.maxDate) : undefined;
63
+ if (max != null && Number.isNaN(max)) {
64
+ throw mcpError('invalid-date-filter', `maxDate is not a parseable date: ${filters.maxDate}`);
65
+ }
66
+
67
+ let filtered = entries;
68
+ if (filters.namePattern) {
69
+ const needle = filters.namePattern.toLowerCase();
70
+ filtered = filtered.filter((e) => e.name.toLowerCase().includes(needle));
71
+ }
72
+ if (min != null) {
73
+ filtered = filtered.filter((e) => e.startTime != null && Date.parse(e.startTime) >= min);
74
+ }
75
+ if (max != null) {
76
+ filtered = filtered.filter((e) => e.startTime != null && Date.parse(e.startTime) <= max);
77
+ }
78
+ const maxResults = filters.maxResults ?? DEFAULT_MAX_RESULTS;
79
+ const capped = filtered.length <= maxResults
80
+ ? { runs: filtered, truncated: false }
81
+ : { runs: filtered.slice(0, maxResults), truncated: true };
82
+ return filters.redact ? { ...capped, runs: redactRunListEntries(capped.runs) } : capped;
83
+ }
84
+
85
+ // Local counterpart to redact.ts's redactAppIdentity, sized for a listing of many distinct apps
86
+ // rather than the single app that tool works over: redactAppIdentity has no way to keep two
87
+ // entries sharing an appId in sync, so this builds its own stable per-distinct-appId pseudonym map
88
+ // (numeric-aware sort, same scheme as redact.ts's buildMap) across the whole result set, two
89
+ // attempts of the same app redact to the same identity, and applies it to every identity-bearing
90
+ // field: appId, name (a listing's human-readable name is as identifying as the id itself), and the
91
+ // source's own path/appId.
92
+ function redactRunListEntries(entries ) {
93
+ const appIds = new Set ();
94
+ for (const e of entries) appIds.add(e.appId);
95
+ const sorted = [...appIds].sort((a, b) => a.localeCompare(b, 'en', { numeric: true }));
96
+ const pseudonyms = new Map(sorted.map((id, i) => [id, `app-${i + 1}`]));
97
+ return entries.map((e) => {
98
+ const pseudonym = pseudonyms.get(e.appId) ?? e.appId;
99
+ const source = 'path' in e.source
100
+ ? { ...e.source, path: pseudonym }
101
+ : { ...e.source, appId: pseudonym };
102
+ return { ...e, appId: pseudonym, name: pseudonym, source };
103
+ });
104
+ }
105
+
106
+ export async function listRunsLocal(input ) {
107
+ const dir = resolvePath(input.dir);
108
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) {
109
+ throw mcpError('directory-not-found', `No such directory: ${dir}`);
110
+ }
111
+
112
+ let names ;
113
+ try {
114
+ names = readdirSync(dir);
115
+ } catch {
116
+ throw mcpError('directory-not-found', `Could not read directory: ${dir}`);
117
+ }
118
+
119
+ // `path` is the entry's own source-worthy location (a file, or a rolling-log dir); `peekPath` is
120
+ // the single file peekLogHeader actually reads (the rolling dir's earliest segment). `attemptId`
121
+ // is only ever set for the plain-file case (see LOCAL_ATTEMPT_SUFFIX_RE), a rolling-log
122
+ // directory's name has no file extension, so it never matches the convention.
123
+ const candidates = [];
124
+ for (const name of names) {
125
+ const full = join(dir, name);
126
+ let st;
127
+ try {
128
+ st = statSync(full);
129
+ } catch {
130
+ continue; // vanished between readdir and stat: skip
131
+ }
132
+ if (st.isFile()) {
133
+ const attemptMatch = name.match(LOCAL_ATTEMPT_SUFFIX_RE);
134
+ candidates.push({ path: full, peekPath: full, ...(attemptMatch ? { attemptId: attemptMatch[1] } : {}) });
135
+ } else if (st.isDirectory()) {
136
+ let isRolling = false;
137
+ try {
138
+ isRolling = isRollingLogDirectory(full);
139
+ } catch {
140
+ continue; // permission denied or other error reading the directory: skip this candidate
141
+ }
142
+ if (!isRolling) continue; // not a rolling log directory: skip
143
+ let subNames ;
144
+ try {
145
+ subNames = readdirSync(full);
146
+ } catch {
147
+ continue;
148
+ }
149
+ let ordered ;
150
+ try {
151
+ ordered = reassembleRollingEntries(subNames);
152
+ } catch {
153
+ continue; // malformed rolling dir (index gap, etc.): skip like any unpeekable candidate
154
+ }
155
+ if (ordered.length === 0) continue;
156
+ candidates.push({ path: full, peekPath: join(full, ordered[0]) });
157
+ }
158
+ // Anything else (a plain non-rolling subdirectory, a socket, ...): not a candidate, skip.
159
+ }
160
+
161
+ const entries = [];
162
+ for (const c of candidates) {
163
+ let peeked;
164
+ try {
165
+ peeked = await peekLogHeader(nodeLazyPeekableFromPath(c.peekPath));
166
+ } catch {
167
+ continue; // corrupt/unreadable candidate: one bad file shouldn't fail the whole listing
168
+ }
169
+ if (!peeked || !peeked.appId) continue; // no usable appId: not enough to hand back as a candidate
170
+ entries.push({
171
+ appId: peeked.appId,
172
+ name: peeked.name ?? '',
173
+ ...(peeked.sparkVersion ? { sparkVersion: peeked.sparkVersion } : {}),
174
+ ...(peeked.startTimeMs != null ? { startTime: new Date(peeked.startTimeMs).toISOString() } : {}),
175
+ source: { path: c.path, ...(c.attemptId ? { attemptId: c.attemptId } : {}) },
176
+ });
177
+ }
178
+
179
+ // readdir order is arbitrary, and applyFiltersAndCap keeps the first maxResults, without a sort
180
+ // a directory of thousands of logs would hand back an arbitrary 100 rather than the newest 100.
181
+ // Newest-first also matches SHS mode, which inherits that order from SHS's own attempt ordering.
182
+ entries.sort((a, b) => {
183
+ if (a.startTime == null && b.startTime == null) return 0;
184
+ if (a.startTime == null) return 1; // undated entries sort last, after every dated one
185
+ if (b.startTime == null) return -1;
186
+ return Date.parse(b.startTime) - Date.parse(a.startTime);
187
+ });
188
+
189
+ return applyFiltersAndCap(entries, input);
190
+ }
191
+
192
+ export async function listRunsShs(
193
+ input ,
194
+ opts ,
195
+ ) {
196
+ const fetchImpl = opts?.fetchImpl ?? fetch;
197
+ const normalized = normalizeBaseUrl(input.shsBaseUrl);
198
+ if (!normalized) {
199
+ throw mcpError('invalid-shs-base-url', 'Enter an absolute HTTP(S) base URL without credentials, query, or fragment.');
200
+ }
201
+
202
+ const url = new URL('api/v1/applications', normalized);
203
+ if (input.minDate) url.searchParams.set('minDate', input.minDate);
204
+ if (input.maxDate) url.searchParams.set('maxDate', input.maxDate);
205
+
206
+ let res ;
207
+ try {
208
+ // An unresponsive SHS would otherwise hang the tool call forever. A timed-out signal rejects
209
+ // the fetch with an AbortError, which this same catch turns into access-or-upstream-failure.
210
+ res = await fetchImpl(url.toString(), { signal: AbortSignal.timeout(DEFAULT_IDLE_TIMEOUT_MS) });
211
+ } catch (e) {
212
+ throw mcpError('access-or-upstream-failure', `Could not reach ${normalized}: ${e instanceof Error ? e.message : String(e)}`);
213
+ }
214
+ if (!res.ok) {
215
+ throw mcpError('access-or-upstream-failure', `SHS applications list request failed with status ${res.status}.`);
216
+ }
217
+ // Best-effort size guard: this endpoint returns a modest JSON listing, so a declared length over
218
+ // the archive cap means something is wrong. Absent (e.g. chunked) header: skip the check rather
219
+ // than pull in a streaming reader for a listing.
220
+ const declaredLength = Number.parseInt(res.headers.get('content-length') ?? '', 10);
221
+ if (Number.isFinite(declaredLength) && declaredLength > DEFAULT_MAX_ARCHIVE_BYTES) {
222
+ throw mcpError('access-or-upstream-failure', 'SHS applications list response exceeds the byte cap.');
223
+ }
224
+
225
+ let payload ;
226
+ try {
227
+ payload = await res.json();
228
+ } catch {
229
+ throw mcpError('access-or-upstream-failure', 'SHS applications list response was not valid JSON.');
230
+ }
231
+ if (!Array.isArray(payload)) {
232
+ throw mcpError('access-or-upstream-failure', 'SHS applications list response was not an array.');
233
+ }
234
+
235
+ const entries = [];
236
+ for (const app of payload ) {
237
+ if (!app || typeof app !== 'object') continue;
238
+ const id = typeof app.id === 'string' ? app.id : null;
239
+ if (!id) continue;
240
+ const name = typeof app.name === 'string' ? app.name : '';
241
+ const attempts = Array.isArray(app.attempts) ? app.attempts : [];
242
+ const attempt = attempts[0]; // SHS's own ordering: most recent attempt first
243
+ if (!attempt) continue;
244
+ const sparkVersion = typeof attempt.appSparkVersion === 'string' ? attempt.appSparkVersion : undefined;
245
+ const startTime = typeof attempt.startTime === 'string' ? normalizeShsTimestamp(attempt.startTime) : undefined;
246
+ const durationMs = typeof attempt.duration === 'number' ? attempt.duration : undefined;
247
+ const attemptId = typeof attempt.attemptId === 'string' ? attempt.attemptId : undefined;
248
+ entries.push({
249
+ appId: id,
250
+ name,
251
+ ...(sparkVersion ? { sparkVersion } : {}),
252
+ ...(startTime ? { startTime } : {}),
253
+ ...(durationMs != null ? { durationMs } : {}),
254
+ source: { shsBaseUrl: normalized, appId: id, ...(attemptId ? { attemptId } : {}) },
255
+ });
256
+ }
257
+
258
+ return applyFiltersAndCap(entries, input);
259
+ }
260
+
261
+ export async function listRuns(
262
+ input ,
263
+ opts ,
264
+ ) {
265
+ if (input.dir) {
266
+ return listRunsLocal({
267
+ dir: input.dir, namePattern: input.namePattern, minDate: input.minDate, maxDate: input.maxDate, maxResults: input.maxResults, redact: input.redact,
268
+ });
269
+ }
270
+ if (input.shsBaseUrl) {
271
+ return listRunsShs(
272
+ { shsBaseUrl: input.shsBaseUrl, namePattern: input.namePattern, minDate: input.minDate, maxDate: input.maxDate, maxResults: input.maxResults, redact: input.redact },
273
+ opts,
274
+ );
275
+ }
276
+ throw mcpError('access-or-upstream-failure', 'Provide either dir or shsBaseUrl.');
277
+ }
278
+
@@ -1,19 +1,13 @@
1
- // Plain JS (like proxy.js): scripts/vendor-core.mjs copies it byte-for-byte
2
- // into vendor-core/, so this exact file exists at both
3
- // vendor-core/load-vendored.js (published install) and
4
- // core/src/load-vendored.js (monorepo dev). Each published package's bin
5
- // entry point bootstraps by locating *this* file first (a tiny fixed-name
6
- // existsSync/join check, unavoidably duplicated per entry point since
7
- // nothing can resolve it for them), then uses the exports below for every
8
- // other core module, so the actual resolution logic lives in exactly one
9
- // place instead of being re-implemented per entry point.
1
+ // Plain JS: scripts/vendor-core.mjs copies it byte-for-byte into vendor-core/, so this file exists
2
+ // at both vendor-core/load-vendored.js (published) and core/src/load-vendored.js (dev). Each
3
+ // package's bin bootstraps by locating this file first, then uses the exports below for every other
4
+ // core module, so the resolution logic lives in one place instead of per entry point.
10
5
  import { existsSync } from 'node:fs';
11
6
  import { join } from 'node:path';
12
7
  import { pathToFileURL } from 'node:url';
13
8
 
14
- // moduleName is a path relative to core/src without extension, e.g.
15
- // 'cli/collect-run' or 'shs-load'. Pass srcExt: 'js' for modules that are
16
- // already plain JS in core/src (e.g. 'proxy') rather than TypeScript.
9
+ // moduleName is a path relative to core/src without extension, e.g. 'cli/collect-run'. Pass
10
+ // srcExt: 'js' for modules already plain JS in core/src (e.g. 'proxy') rather than TypeScript.
17
11
  export function resolveVendored(pkgDir, moduleName, { srcExt = 'ts' } = {}) {
18
12
  const vendored = join(pkgDir, 'vendor-core', `${moduleName}.js`);
19
13
  return existsSync(vendored) ? vendored : join(pkgDir, '..', 'core', 'src', `${moduleName}.${srcExt}`);
@@ -0,0 +1,81 @@
1
+ import { streamFile } from './parser-worker.js';
2
+ import { buildChunkDecoder } from './event-handlers.js';
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+ // list_runs peeks every candidate file in a shared directory: bound the work per file so one
18
+ // huge or malformed log can't blow out a directory scan. 50 lines is comfortably past where
19
+ // LogStart/ApplicationStart appear in a real log (they're always among the first few events).
20
+ const MAX_PEEK_LINES = 50;
21
+ // Small on purpose: unlike the full parse path (CHUNK_SIZE = 512KB, tuned for progress-bar
22
+ // granularity on multi-GB files), a peek wants the *first* read to already be enough: reading in
23
+ // 64KB steps means the 50-line cap trips after very little decompression work, not after one
24
+ // giant chunk decodes thousands of lines at once.
25
+ const PEEK_CHUNK_BYTES = 64 * 1024;
26
+
27
+ // Thrown from inside the streamFile->decoder->onChunk callback chain to unwind out of the
28
+ // (possibly still-looping) decompression stream the instant enough header lines are seen: the
29
+ // rest of the file is never requested from `file.slice()` after this.
30
+ class PeekComplete extends Error {}
31
+
32
+ export async function peekLogHeader(file ) {
33
+ if (file.size === 0) return null;
34
+
35
+ const decoder = buildChunkDecoder();
36
+ let linesRead = 0;
37
+ let sawLogStart = false;
38
+ let sawAppStart = false;
39
+ const result = { appId: null, name: null, sparkVersion: null, startTimeMs: null };
40
+
41
+ const processLine = (line ) => {
42
+ linesRead++;
43
+ let event = null;
44
+ try {
45
+ event = JSON.parse(line);
46
+ } catch {
47
+ // Corrupt/partial line: same silent-skip as dispatchLine's state.skippedLines++ path.
48
+ }
49
+ if (event) {
50
+ if (event.Event === 'SparkListenerLogStart') {
51
+ if (typeof event['Spark Version'] === 'string') result.sparkVersion = event['Spark Version'];
52
+ sawLogStart = true;
53
+ } else if (event.Event === 'SparkListenerApplicationStart') {
54
+ if (typeof event['App ID'] === 'string') result.appId = event['App ID'];
55
+ if (typeof event['App Name'] === 'string') result.name = event['App Name'];
56
+ if (typeof event['Timestamp'] === 'number') result.startTimeMs = event['Timestamp'];
57
+ // ApplicationStart's own Spark Version is only a fallback: a preceding LogStart always
58
+ // wins, mirroring event-handlers.ts's startApplication precedence
59
+ // (state.pendingSparkVersion ?? event['Spark Version']).
60
+ if (!sawLogStart && typeof event['Spark Version'] === 'string') result.sparkVersion = event['Spark Version'];
61
+ sawAppStart = true;
62
+ }
63
+ }
64
+ if ((sawLogStart && sawAppStart) || linesRead >= MAX_PEEK_LINES) throw new PeekComplete();
65
+ };
66
+
67
+ const feed = (bytes ) => {
68
+ for (const line of decoder.decode(bytes)) processLine(line);
69
+ };
70
+
71
+ try {
72
+ await streamFile(file, feed, PEEK_CHUNK_BYTES);
73
+ // A trailing header line with no terminating newline sits in the decoder's pending buffer
74
+ // until flushed, same as parser-worker.ts's runParse/runParseFiles do after their streamFile call.
75
+ for (const line of decoder.flush()) processLine(line);
76
+ } catch (e) {
77
+ if (!(e instanceof PeekComplete)) return null; // unsupported/corrupt codec: same silent-skip as any unpeekable candidate
78
+ }
79
+
80
+ return sawAppStart ? result : null;
81
+ }
@@ -35,12 +35,10 @@ function decompressLz4Sequence(input , outSize ) {
35
35
  return out;
36
36
  }
37
37
 
38
- // Streaming counterpart to decodeLz4Block: push arbitrary byte slices, and each
39
- // fully-received LZ4Block block is decompressed and handed to `onChunk` as it
40
- // completes, so the decompressed output is never fully buffered (bounded to one
41
- // block at a time). Partial trailing bytes are retained until the next push.
42
- // `onChunk` must consume its argument synchronously (it aliases the internal
43
- // buffer for RAW blocks and is not retained across the next push).
38
+ // Streaming counterpart to decodeLz4Block: push byte slices, each complete LZ4Block block is
39
+ // decompressed and handed to `onChunk` as it completes, so output is never fully buffered. Partial
40
+ // trailing bytes are retained until the next push. `onChunk` must consume synchronously (it aliases
41
+ // the internal buffer for RAW blocks, not retained across the next push).
44
42
  export function createLz4BlockDecoder(
45
43
  onChunk ,
46
44
  ) {
@@ -2,8 +2,9 @@ import { z } from 'zod';
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
 
4
4
  import {
5
- resolveOrCreateRun, diagnoseRun, getRunSummary, compareRuns, getFindingEvidence, evaluateBudgetsForRun,
5
+ resolveOrCreateRun, diagnoseRun, getRunSummary, compareRuns, getFindingEvidence, getFindingDocumentation, getReferenceDoc, evaluateBudgetsForRun,
6
6
  } from './mcp-tools.js';
7
+ import { listRuns } from './list-runs.js';
7
8
 
8
9
  const sourceSchema = z.union([
9
10
  z.object({ path: z.string() }),
@@ -14,6 +15,19 @@ const runRefSchema = { source: sourceSchema.optional(), runId: z.string().option
14
15
  const secondRunRefSchema = { sourceB: sourceSchema.optional(), runIdB: z.string().optional() };
15
16
  const formatSchema = { format: z.enum(['json', 'md']).optional() };
16
17
 
18
+ // list_runs takes a bare shsBaseUrl (no appId/attemptId, unlike sourceSchema's SHS branch, since
19
+ // it's listing candidates rather than resolving one), so it can't reuse sourceSchema: refine
20
+ // against both dir and shsBaseUrl being set instead, so one doesn't silently win over the other.
21
+ const listRunsInputSchema = z.object({
22
+ dir: z.string().optional(),
23
+ shsBaseUrl: z.string().optional(),
24
+ namePattern: z.string().optional(),
25
+ minDate: z.string().optional(),
26
+ maxDate: z.string().optional(),
27
+ maxResults: z.number().int().positive().optional(),
28
+ redact: z.boolean().optional(),
29
+ }).refine((v) => !(v.dir && v.shsBaseUrl), { message: 'Provide only one of dir or shsBaseUrl, not both.' });
30
+
17
31
  function toCallToolResult(text , structuredContent ) {
18
32
  return { content: [{ type: 'text' , text }], structuredContent };
19
33
  }
@@ -26,11 +40,9 @@ function toolErrorResult(error )
26
40
  };
27
41
  }
28
42
 
29
- // diagnoseRun/compareRuns may additionally carry a `markdown` field (only
30
- // when the caller asked for `format: 'md'`); that field never belongs in
31
- // structuredContent (its shape must stay stable regardless of `format`), so
32
- // it's stripped here, and — when present — becomes content[0].text instead
33
- // of the usual JSON.stringify.
43
+ // diagnoseRun/compareRuns may carry a `markdown` field (only when the caller asked for
44
+ // format: 'md'); it never belongs in structuredContent (whose shape must stay stable regardless of
45
+ // format), so it's stripped here and, when present, becomes content[0].text instead of JSON.stringify.
34
46
  function toolResultWithMarkdown (promise ) {
35
47
  return promise.then(
36
48
  (value) => {
@@ -41,8 +53,7 @@ function toolResultWithMarkdown (promise
41
53
  );
42
54
  }
43
55
 
44
- // Counterpart for the 3 tools that never produce a markdown field: its own
45
- // mapping (not a delegation through toolResultWithMarkdown), so there's no
56
+ // Counterpart for the tools that never produce a markdown field: its own mapping, so there's no
46
57
  // need to cast T to pretend it might carry a `markdown` field it never does.
47
58
  function toolResult (promise ) {
48
59
  return promise.then((value) => toCallToolResult(JSON.stringify(value), value ), toolErrorResult);
@@ -51,6 +62,11 @@ function toolResult (promise )
51
62
  export function createMcpServer() {
52
63
  const server = new McpServer({ name: 'sparkforensics', version: '1.0.0' });
53
64
 
65
+ server.registerTool('list_runs', {
66
+ description: 'List candidate Spark event-log runs from a local directory or a Spark History Server, before diagnosing one with the other tools.',
67
+ inputSchema: listRunsInputSchema,
68
+ }, (params) => toolResult(listRuns(params)));
69
+
54
70
  server.registerTool('diagnose_run', {
55
71
  description: 'Diagnose a Spark run: thresholded findings with remediation text, an impact-ranked fix recommendation rollup, and clean-check status.',
56
72
  inputSchema: {
@@ -111,5 +127,19 @@ export function createMcpServer() {
111
127
  Promise.resolve().then(() => getFindingEvidence(runId, findingId, { redact })),
112
128
  ));
113
129
 
130
+ server.registerTool('get_finding_documentation', {
131
+ description: 'Detection and tuning reference documentation for one finding type (not tied to a specific run), so a client without browser access can read the same background material the web view links to.',
132
+ inputSchema: { type: z.string() },
133
+ }, ({ type }) => toolResult(
134
+ Promise.resolve().then(() => getFindingDocumentation(type)),
135
+ ));
136
+
137
+ server.registerTool('get_reference_doc', {
138
+ description: 'Full tuning-reference chapter or bottleneck markdown by doc anchor (e.g. "#joins", "#bottleneck-skew", "#metric-task-duration"), so a client without browser access can read the same reference the web view shows.',
139
+ inputSchema: { anchor: z.string() },
140
+ }, ({ anchor }) => toolResult(
141
+ Promise.resolve().then(() => getReferenceDoc(anchor)),
142
+ ));
143
+
114
144
  return server;
115
145
  }