sparkforensics-server 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/public/assets/{AutoscalingChurn-9Zo6IMGS.js → AutoscalingChurn-5hDGIXi4.js} +1 -1
- package/public/assets/{CacheUtilization-MBDfMsKC.js → CacheUtilization-CT2SCnJJ.js} +1 -1
- package/public/assets/{CachingOpportunity-Cfw3wAFa.js → CachingOpportunity-D_O_WkbH.js} +1 -1
- package/public/assets/{ColdStart-BtS12b6_.js → ColdStart-B38OiAOp.js} +1 -1
- package/public/assets/{ConfigAudit-DNPV6EQg.js → ConfigAudit-BoplryCH.js} +1 -1
- package/public/assets/{CoreUsageArea-BObEeenE.js → CoreUsageArea-csgwNxkl.js} +1 -1
- package/public/assets/{Dashboard-CC_BXrNy.js → Dashboard-BgKodJl2.js} +2 -2
- package/public/assets/{DisclosureContext-Ds0hcZFn.js → DisclosureContext-B0siMVdg.js} +1 -1
- package/public/assets/{DuplicatePlanSubtree-vN3zqRsi.js → DuplicatePlanSubtree-BqS2A2j4.js} +1 -1
- package/public/assets/{ExecutorUtilization-CZObSLIj.js → ExecutorUtilization-Dp52QGWo.js} +1 -1
- package/public/assets/{GcPressure-ByJlLOua.js → GcPressure-CT1yzR_s.js} +1 -1
- package/public/assets/{IncompleteRun-MzqV4W8y.js → IncompleteRun-C7Qkjtjn.js} +1 -1
- package/public/assets/{JobFailures-LbMvAwAb.js → JobFailures-WHl3ELfh.js} +1 -1
- package/public/assets/{MemoryUtilization-DH4jYvJK.js → MemoryUtilization-D8IUDHJI.js} +1 -1
- package/public/assets/{OverBroadcast-euk6HKg4.js → OverBroadcast-6oSSAQ_R.js} +1 -1
- package/public/assets/{PartitionHint-DThaMMRT.js → PartitionHint-CUvbnnxy.js} +1 -1
- package/public/assets/{PartitionSizing-yIN-iZzx.js → PartitionSizing-BwdFL-mw.js} +1 -1
- package/public/assets/{PlanExplorer-Dd9WPK4S.js → PlanExplorer-DcxmiQWL.js} +1 -1
- package/public/assets/{PlanGraphRoute-NV5mCXRm.js → PlanGraphRoute-pqnHTt4i.js} +1 -1
- package/public/assets/{RetryWaste-CXH4D-xH.js → RetryWaste-BTbeHkJc.js} +1 -1
- package/public/assets/{ShuffleIO-CW7U68ph.js → ShuffleIO-CRhrYNE0.js} +1 -1
- package/public/assets/{Skew-PL79TppX.js → Skew-DSNXRG9V.js} +1 -1
- package/public/assets/{SlowHost-GBO73hCi.js → SlowHost-B7Lwu2EU.js} +1 -1
- package/public/assets/{SmallFiles-B5cUjEav.js → SmallFiles-C082c2rd.js} +1 -1
- package/public/assets/{SpeculationWaste-mxi3jgb0.js → SpeculationWaste-BS2vpadr.js} +1 -1
- package/public/assets/{Spill-CH1bNpxL.js → Spill-B4DVKHXx.js} +1 -1
- package/public/assets/{StageFailed-YvxC11ii.js → StageFailed-D3ac9Sou.js} +1 -1
- package/public/assets/{StageFindingGroup-B1A0nf-e.js → StageFindingGroup-D9naK109.js} +1 -1
- package/public/assets/{StageHeader-BMNmUy-s.js → StageHeader-pI1nTwVf.js} +1 -1
- package/public/assets/{StageShape-DO3mQ6NS.js → StageShape-eO5le8iG.js} +1 -1
- package/public/assets/{StageSlowness-SwRxfAEx.js → StageSlowness-BaNyfNvL.js} +1 -1
- package/public/assets/{Straggler-DHLTiBY-.js → Straggler-D5Fnok_y.js} +1 -1
- package/public/assets/{TaskFailures-Bj0QxiOc.js → TaskFailures-BWnC-K_G.js} +1 -1
- package/public/assets/{TinyTask-COUQQ5bQ.js → TinyTask-8_5lpq_2.js} +1 -1
- package/public/assets/{UnderBroadcast-RAUbQ6vw.js → UnderBroadcast-DJ0XSwth.js} +1 -1
- package/public/assets/{finding-anchor-Dl8mIHiU.js → finding-anchor-Cys8Z6Yn.js} +1 -1
- package/public/assets/{impact-sort-Eihua9dY.js → impact-sort-BTQndbof.js} +1 -1
- package/public/assets/index-DCeWge12.js +13 -0
- package/public/assets/parser-worker-DmeCnLWU.js +122 -0
- package/public/assets/{plan-graph-vendor-B0sEms9Z.js → plan-graph-vendor-BYl-AzN5.js} +1 -1
- package/public/assets/{react-vendor-BAKt3vny.js → react-vendor-DEdfrRTJ.js} +1 -1
- package/public/assets/{select-DkjFHlXR.js → select-DIYN6D3y.js} +1 -1
- package/public/assets/{usePagedRows-Bo-aSztx.js → usePagedRows-BxvdFnPk.js} +1 -1
- package/public/assets/{useRecentFiles-Bb4-ukVz.js → useRecentFiles-C3Ov47U7.js} +4 -4
- package/public/assets/{useSortMode-lDedD5a3.js → useSortMode-B8SRfAoG.js} +1 -1
- package/public/docs/404.html +2 -2
- package/public/docs/assets/{app.yjczoibf.js → app.D2GqAkhy.js} +1 -1
- package/public/docs/assets/chunks/@localSearchIndexroot.D4OdkQjy.js +1 -0
- package/public/docs/assets/chunks/{VPLocalSearchBox.DOBbPfix.js → VPLocalSearchBox.BS4cSmtW.js} +1 -1
- package/public/docs/assets/chunks/{theme.DFcAl8Ws.js → theme.BuxVpb0H.js} +2 -2
- package/public/docs/assets/contributor-guide_architecture_board-widgets.md.YQcj5EI4.js +1 -0
- package/public/docs/assets/{contributor-guide_architecture_board-widgets.md.D0Kkk_r5.lean.js → contributor-guide_architecture_board-widgets.md.YQcj5EI4.lean.js} +1 -1
- package/public/docs/assets/contributor-guide_architecture_board-widgets_caching-and-memory.md.8bH1HnZp.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_board-widgets_caching-and-memory.md.8bH1HnZp.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_board-widgets_plan-advisor.md.RlzvUmNm.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_board-widgets_plan-advisor.md.RlzvUmNm.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_board-widgets_visual-system-and-templating.md.DYgUpIJB.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_board-widgets_visual-system-and-templating.md.DYgUpIJB.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_detector-contract.md.wj6Et6UD.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_detector-contract.md.wj6Et6UD.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_detector-contract_plan-attribution.md.yNex4bys.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_detector-contract_plan-attribution.md.yNex4bys.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.fkPBXsU2.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_detector-contract_threshold-tables.md.fkPBXsU2.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_drill-down.md.CqZPrdo9.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_drill-down.md.CqZPrdo9.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_drill-down_plan-graph.md.DwOozIEs.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_drill-down_plan-graph.md.DwOozIEs.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.CZv_ZGOp.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.CZv_ZGOp.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.BUucRc0A.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_impact-estimation_caveats-tuning-and-coverage.md.BUucRc0A.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_impact-estimation_stage-union-rollup.md.DdCGtAGu.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_impact-estimation_stage-union-rollup.md.DdCGtAGu.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_index.md.tymxf1Fh.js +1 -0
- package/public/docs/assets/{contributor-guide_architecture_index.md.B9yZieYL.lean.js → contributor-guide_architecture_index.md.tymxf1Fh.lean.js} +1 -1
- package/public/docs/assets/contributor-guide_architecture_overview.md.C1sSvm9i.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_state-and-history.md.DyCBdjuM.js +1 -0
- package/public/docs/assets/{contributor-guide_architecture_state-and-history.md.w4lwybdh.lean.js → contributor-guide_architecture_state-and-history.md.DyCBdjuM.lean.js} +1 -1
- package/public/docs/assets/contributor-guide_architecture_state-and-history_history-server-intake.md.C298hrHZ.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_state-and-history_history-server-intake.md.C298hrHZ.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.uuAKpY0Y.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_state-and-history_run-comparison-internals.md.uuAKpY0Y.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.BOZ1azXG.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.BOZ1azXG.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_widget-rendering_findings-tab.md.BQXycK6G.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_widget-rendering_findings-tab.md.BQXycK6G.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_widget-rendering_investigation-routing.md.BGdiyElu.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_widget-rendering_investigation-routing.md.BGdiyElu.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_widget-rendering_list-sort-mode.md.BuHKgH0C.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_widget-rendering_list-sort-mode.md.BuHKgH0C.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_widget-rendering_reference-section.md.DWqC80bN.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_widget-rendering_reference-section.md.DWqC80bN.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_widget-rendering_row-expand-contract.md.ZT89u9f1.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_widget-rendering_row-expand-contract.md.ZT89u9f1.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.BAnhi2-d.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.BAnhi2-d.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.Dk08VS7n.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_worker-protocol_cli-and-mcp.md.Dk08VS7n.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_worker-protocol_core-and-event-validation.md.DSVFPpWb.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_worker-protocol_core-and-event-validation.md.DSVFPpWb.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_worker-protocol_evidence-availability.md.CjimIDBY.js +6 -0
- package/public/docs/assets/contributor-guide_architecture_worker-protocol_evidence-availability.md.CjimIDBY.lean.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_worker-protocol_evidence-report.md.DVGZOWvW.js +1 -0
- package/public/docs/assets/contributor-guide_architecture_worker-protocol_evidence-report.md.DVGZOWvW.lean.js +1 -0
- package/public/docs/assets/user-guide_alternative-log-retrieval.md.CJpYvHGo.js +4 -0
- package/public/docs/assets/user-guide_alternative-log-retrieval.md.CJpYvHGo.lean.js +1 -0
- package/public/docs/assets/user-guide_alternative-log-retrieval_managed-platforms.md.DvDwHNtC.js +4 -0
- package/public/docs/assets/user-guide_alternative-log-retrieval_managed-platforms.md.DvDwHNtC.lean.js +1 -0
- package/public/docs/assets/user-guide_alternative-log-retrieval_ssh-bastion.md.Gszzlplv.js +1 -0
- package/public/docs/assets/user-guide_alternative-log-retrieval_ssh-bastion.md.Gszzlplv.lean.js +1 -0
- package/public/docs/assets/user-guide_getting-started.md.DYHHHkQR.js +1 -0
- package/public/docs/assets/user-guide_getting-started.md.DYHHHkQR.lean.js +1 -0
- package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.DpnfKz3x.js +22 -0
- package/public/docs/assets/user-guide_getting-started_ci-and-automation.md.DpnfKz3x.lean.js +1 -0
- package/public/docs/assets/user-guide_getting-started_reading-the-dashboard.md.Obf3W3__.js +1 -0
- package/public/docs/assets/user-guide_getting-started_reading-the-dashboard.md.Obf3W3__.lean.js +1 -0
- package/public/docs/assets/user-guide_getting-started_regression-metric-keys.md.B_gzr2Mw.js +1 -0
- package/public/docs/assets/user-guide_getting-started_regression-metric-keys.md.B_gzr2Mw.lean.js +1 -0
- package/public/docs/assets/user-guide_getting-started_tuning-thresholds.md.B_MyjyZm.js +4 -0
- package/public/docs/assets/user-guide_getting-started_tuning-thresholds.md.B_MyjyZm.lean.js +1 -0
- package/public/docs/assets/user-guide_mcp-tools.md.C65SjWUL.js +16 -0
- package/public/docs/assets/user-guide_mcp-tools.md.C65SjWUL.lean.js +1 -0
- package/public/docs/assets/user-guide_mcp-tools_compare-runs.md.CPowlb2q.js +55 -0
- package/public/docs/assets/user-guide_mcp-tools_compare-runs.md.CPowlb2q.lean.js +1 -0
- package/public/docs/assets/user-guide_mcp-tools_diagnose-run.md.BP1vnHA5.js +23 -0
- package/public/docs/assets/user-guide_mcp-tools_diagnose-run.md.BP1vnHA5.lean.js +1 -0
- package/public/docs/assets/user-guide_mcp-tools_evaluate-budgets.md.Dq5Azk8G.js +26 -0
- package/public/docs/assets/user-guide_mcp-tools_evaluate-budgets.md.Dq5Azk8G.lean.js +1 -0
- package/public/docs/assets/user-guide_mcp-tools_get-finding-documentation.md.BtTTHNV6.js +14 -0
- package/public/docs/assets/user-guide_mcp-tools_get-finding-documentation.md.BtTTHNV6.lean.js +1 -0
- package/public/docs/assets/user-guide_mcp-tools_get-finding-evidence.md.rKyu4wts.js +20 -0
- package/public/docs/assets/user-guide_mcp-tools_get-finding-evidence.md.rKyu4wts.lean.js +1 -0
- package/public/docs/assets/user-guide_mcp-tools_get-reference-doc.md.Yac4tH3u.js +5 -0
- package/public/docs/assets/user-guide_mcp-tools_get-reference-doc.md.Yac4tH3u.lean.js +1 -0
- package/public/docs/assets/user-guide_mcp-tools_get-run-summary.md.DpxZ4ZNp.js +15 -0
- package/public/docs/assets/user-guide_mcp-tools_get-run-summary.md.DpxZ4ZNp.lean.js +1 -0
- package/public/docs/assets/user-guide_mcp-tools_list-runs.md.DpQPeCM3.js +12 -0
- package/public/docs/assets/user-guide_mcp-tools_list-runs.md.DpQPeCM3.lean.js +1 -0
- package/public/docs/assets/{user-guide_run-comparison.md.qin0a4IX.js → user-guide_run-comparison.md.B6aNtDYF.js} +1 -1
- package/public/docs/assets/user-guide_run-comparison.md.B6aNtDYF.lean.js +1 -0
- package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.XBzQ1oEU.js +1 -0
- package/public/docs/assets/user-guide_run-comparison_how-stages-are-matched.md.XBzQ1oEU.lean.js +1 -0
- package/public/docs/assets/user-guide_understanding-findings.md.Dqt2H_C1.js +1 -0
- package/public/docs/contributor-guide/architecture/board-widgets/caching-and-memory.html +30 -0
- package/public/docs/contributor-guide/architecture/board-widgets/plan-advisor.html +30 -0
- package/public/docs/contributor-guide/architecture/board-widgets/visual-system-and-templating.html +30 -0
- package/public/docs/contributor-guide/architecture/board-widgets.html +5 -5
- package/public/docs/contributor-guide/architecture/detector-contract/plan-attribution.html +30 -0
- package/public/docs/contributor-guide/architecture/detector-contract/threshold-tables.html +30 -0
- package/public/docs/contributor-guide/architecture/detector-contract.html +5 -5
- package/public/docs/contributor-guide/architecture/drill-down/plan-graph.html +30 -0
- package/public/docs/contributor-guide/architecture/drill-down.html +5 -5
- package/public/docs/contributor-guide/architecture/impact-estimation/caveats-tuning-and-coverage.html +30 -0
- package/public/docs/contributor-guide/architecture/impact-estimation/stage-union-rollup.html +30 -0
- package/public/docs/contributor-guide/architecture/impact-estimation.html +5 -5
- package/public/docs/contributor-guide/architecture/index.html +5 -5
- package/public/docs/contributor-guide/architecture/overview.html +5 -5
- package/public/docs/contributor-guide/architecture/state-and-history/history-server-intake.html +30 -0
- package/public/docs/contributor-guide/architecture/state-and-history/run-comparison-internals.html +30 -0
- package/public/docs/contributor-guide/architecture/state-and-history.html +5 -5
- package/public/docs/contributor-guide/architecture/widget-rendering/findings-tab.html +30 -0
- package/public/docs/contributor-guide/architecture/widget-rendering/investigation-routing.html +30 -0
- package/public/docs/contributor-guide/architecture/widget-rendering/list-sort-mode.html +30 -0
- package/public/docs/contributor-guide/architecture/widget-rendering/reference-section.html +30 -0
- package/public/docs/contributor-guide/architecture/widget-rendering/row-expand-contract.html +30 -0
- package/public/docs/contributor-guide/architecture/widget-rendering.html +5 -5
- package/public/docs/contributor-guide/architecture/worker-protocol/cli-and-mcp.html +30 -0
- package/public/docs/contributor-guide/architecture/worker-protocol/core-and-event-validation.html +30 -0
- package/public/docs/contributor-guide/architecture/worker-protocol/evidence-availability.html +35 -0
- package/public/docs/contributor-guide/architecture/worker-protocol/evidence-report.html +30 -0
- package/public/docs/contributor-guide/architecture/worker-protocol.html +5 -10
- package/public/docs/contributor-guide/contributing.html +4 -4
- package/public/docs/contributor-guide/development-setup.html +4 -4
- package/public/docs/contributor-guide/testing.html +4 -4
- package/public/docs/hashmap.json +1 -1
- package/public/docs/index.html +3 -3
- package/public/docs/tuning-reference/anti-patterns.html +3 -3
- package/public/docs/tuning-reference/aqe.html +3 -3
- package/public/docs/tuning-reference/bottleneck-broadcast-sizing.html +3 -3
- package/public/docs/tuning-reference/bottleneck-cold-start.html +3 -3
- package/public/docs/tuning-reference/bottleneck-duplicate-plan-subtree.html +3 -3
- package/public/docs/tuning-reference/bottleneck-failures.html +3 -3
- package/public/docs/tuning-reference/bottleneck-gc.html +3 -3
- package/public/docs/tuning-reference/bottleneck-job-failure-rate.html +3 -3
- package/public/docs/tuning-reference/bottleneck-memory-utilization.html +3 -3
- package/public/docs/tuning-reference/bottleneck-retry-waste.html +3 -3
- package/public/docs/tuning-reference/bottleneck-shuffle.html +3 -3
- package/public/docs/tuning-reference/bottleneck-skew.html +3 -3
- package/public/docs/tuning-reference/bottleneck-slow-host.html +3 -3
- package/public/docs/tuning-reference/bottleneck-small-files.html +3 -3
- package/public/docs/tuning-reference/bottleneck-spill.html +3 -3
- package/public/docs/tuning-reference/bottleneck-straggler.html +3 -3
- package/public/docs/tuning-reference/bottleneck-tiny-tasks.html +3 -3
- package/public/docs/tuning-reference/bottleneck-utilization.html +3 -3
- package/public/docs/tuning-reference/caching.html +3 -3
- package/public/docs/tuning-reference/cluster-config.html +3 -3
- package/public/docs/tuning-reference/config.html +3 -3
- package/public/docs/tuning-reference/data-formats.html +3 -3
- package/public/docs/tuning-reference/index.html +3 -3
- package/public/docs/tuning-reference/intro.html +3 -3
- package/public/docs/tuning-reference/joins.html +3 -3
- package/public/docs/tuning-reference/memory-model.html +3 -3
- package/public/docs/tuning-reference/metrics.html +3 -3
- package/public/docs/tuning-reference/partitioning.html +3 -3
- package/public/docs/tuning-reference/pyspark.html +3 -3
- package/public/docs/tuning-reference/shuffle.html +3 -3
- package/public/docs/tuning-reference/spark-architecture.html +3 -3
- package/public/docs/tuning-reference/table-formats.html +3 -3
- package/public/docs/user-guide/alternative-log-retrieval/managed-platforms.html +33 -0
- package/public/docs/user-guide/alternative-log-retrieval/ssh-bastion.html +30 -0
- package/public/docs/user-guide/alternative-log-retrieval.html +6 -9
- package/public/docs/user-guide/getting-started/ci-and-automation.html +51 -0
- package/public/docs/user-guide/getting-started/reading-the-dashboard.html +30 -0
- package/public/docs/user-guide/getting-started/regression-metric-keys.html +30 -0
- package/public/docs/user-guide/getting-started/tuning-thresholds.html +33 -0
- package/public/docs/user-guide/getting-started.html +5 -28
- package/public/docs/user-guide/mcp-tools/compare-runs.html +84 -0
- package/public/docs/user-guide/mcp-tools/diagnose-run.html +52 -0
- package/public/docs/user-guide/mcp-tools/evaluate-budgets.html +55 -0
- package/public/docs/user-guide/mcp-tools/get-finding-documentation.html +43 -0
- package/public/docs/user-guide/mcp-tools/get-finding-evidence.html +49 -0
- package/public/docs/user-guide/mcp-tools/get-reference-doc.html +34 -0
- package/public/docs/user-guide/mcp-tools/get-run-summary.html +44 -0
- package/public/docs/user-guide/mcp-tools/list-runs.html +41 -0
- package/public/docs/user-guide/mcp-tools.html +8 -140
- package/public/docs/user-guide/run-comparison/how-stages-are-matched.html +30 -0
- package/public/docs/user-guide/run-comparison.html +5 -5
- package/public/docs/user-guide/understanding-findings.html +5 -5
- package/public/export-template.html +44 -44
- package/public/index.html +4 -4
- package/vendor-core/analyzer.js +1 -1
- package/vendor-core/api-merge-writes.js +80 -0
- package/vendor-core/comparison-output.js +49 -0
- package/vendor-core/core-source-hash.txt +1 -1
- package/vendor-core/delta-commands.js +13 -0
- package/vendor-core/delta-log-paths.js +42 -0
- package/vendor-core/delta-targets.js +127 -0
- package/vendor-core/event-handlers.js +95 -9
- package/vendor-core/event-schemas.js +3 -1
- package/vendor-core/evidence-report.js +13 -4
- package/vendor-core/mcp-server-factory.js +13 -6
- package/vendor-core/mcp-tools.js +44 -35
- package/vendor-core/plan-graph-model.js +1 -1
- package/vendor-core/plan-summary.js +13 -10
- package/vendor-core/run-comparison.js +79 -120
- package/vendor-core/run-output.js +26 -0
- package/vendor-core/stage-alignment.js +262 -0
- package/vendor-core/stage-identity.js +112 -0
- package/vendor-core/types.js +5 -0
- package/vendor-core/write-target-args.js +48 -0
- package/vendor-core/write-targets.js +35 -52
- package/public/assets/index-DaC8A2FS.js +0 -13
- package/public/assets/parser-worker-CfDvPkwF.js +0 -122
- package/public/docs/assets/chunks/@localSearchIndexroot.r9imX8Aq.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_board-widgets.md.D0Kkk_r5.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_detector-contract.md.caA46z-u.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_detector-contract.md.caA46z-u.lean.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_drill-down.md.CdtlKHl-.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_drill-down.md.CdtlKHl-.lean.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.BNp14LtL.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_impact-estimation.md.BNp14LtL.lean.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_index.md.B9yZieYL.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_overview.md.DBGo-bwn.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_state-and-history.md.w4lwybdh.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.CcvqxnZI.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_widget-rendering.md.CcvqxnZI.lean.js +0 -1
- package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.CKeFttKj.js +0 -6
- package/public/docs/assets/contributor-guide_architecture_worker-protocol.md.CKeFttKj.lean.js +0 -1
- package/public/docs/assets/user-guide_alternative-log-retrieval.md.z-8npxNk.js +0 -7
- package/public/docs/assets/user-guide_alternative-log-retrieval.md.z-8npxNk.lean.js +0 -1
- package/public/docs/assets/user-guide_getting-started.md.BJW5oVhe.js +0 -24
- package/public/docs/assets/user-guide_getting-started.md.BJW5oVhe.lean.js +0 -1
- package/public/docs/assets/user-guide_mcp-tools.md.DcLiOa9t.js +0 -148
- package/public/docs/assets/user-guide_mcp-tools.md.DcLiOa9t.lean.js +0 -1
- package/public/docs/assets/user-guide_run-comparison.md.qin0a4IX.lean.js +0 -1
- package/public/docs/assets/user-guide_understanding-findings.md.DzReyCXb.js +0 -1
- /package/public/docs/assets/{contributor-guide_architecture_overview.md.DBGo-bwn.lean.js → contributor-guide_architecture_overview.md.C1sSvm9i.lean.js} +0 -0
- /package/public/docs/assets/{user-guide_understanding-findings.md.DzReyCXb.lean.js → user-guide_understanding-findings.md.Dqt2H_C1.lean.js} +0 -0
package/public/index.html
CHANGED
|
@@ -23,12 +23,12 @@
|
|
|
23
23
|
document.documentElement.setAttribute('data-theme', 'light');
|
|
24
24
|
} catch (_) {}
|
|
25
25
|
</script>
|
|
26
|
-
<script type="module" crossorigin src="./assets/index-
|
|
26
|
+
<script type="module" crossorigin src="./assets/index-DCeWge12.js"></script>
|
|
27
27
|
<link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-hePW80VL.js">
|
|
28
28
|
<link rel="modulepreload" crossorigin href="./assets/charts-vendor-0VMaLVLN.js">
|
|
29
|
-
<link rel="modulepreload" crossorigin href="./assets/plan-graph-vendor-
|
|
30
|
-
<link rel="modulepreload" crossorigin href="./assets/react-vendor-
|
|
31
|
-
<link rel="modulepreload" crossorigin href="./assets/DisclosureContext-
|
|
29
|
+
<link rel="modulepreload" crossorigin href="./assets/plan-graph-vendor-BYl-AzN5.js">
|
|
30
|
+
<link rel="modulepreload" crossorigin href="./assets/react-vendor-DEdfrRTJ.js">
|
|
31
|
+
<link rel="modulepreload" crossorigin href="./assets/DisclosureContext-B0siMVdg.js">
|
|
32
32
|
<link rel="stylesheet" crossorigin href="./assets/plan-graph-vendor-DLioOiRN.css">
|
|
33
33
|
<link rel="stylesheet" crossorigin href="./assets/index-BcqTLKFp.css">
|
|
34
34
|
</head>
|
package/vendor-core/analyzer.js
CHANGED
|
@@ -90,7 +90,7 @@ export function findingId(f ) {
|
|
|
90
90
|
|
|
91
91
|
// skew (either branch) and straggler both claim the stage's replayed tail recovery
|
|
92
92
|
// (tailReplayRecoveryMs via tailRecoveryMs): the same slow-task tail reported by two detectors
|
|
93
|
-
// (see "Overlap caveat: skew / straggler" in impact-estimation.md). skew's branch only changes
|
|
93
|
+
// (see "Overlap caveat: skew / straggler" in impact-estimation/caveats-tuning-and-coverage.md). skew's branch only changes
|
|
94
94
|
// the fallback single-task delta on a stage without the replay, so every skew + straggler pair
|
|
95
95
|
// on a stage is flagged. Flags both sides via validationRequired (rather than suppressing
|
|
96
96
|
// either) so neither finding's own diagnostic value is lost; the flag rides the same
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// DeltaTable API merges run no command node: each internal query is its own root execution, tied
|
|
2
|
+
// together only by a "MERGE operation" description and adjacent ids. One DeltaMerge write is
|
|
3
|
+
// reported per run of such executions, with a path only when no other merge can be mixed into it.
|
|
4
|
+
import { soleLogPath } from './delta-log-paths.js';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const API_MERGE_COMMAND = 'DeltaMerge';
|
|
11
|
+
|
|
12
|
+
// Delta labels its MERGE sub-queries "MERGE operation - <phase>", prefixed "Delta: " in some.
|
|
13
|
+
const MERGE_DESCRIPTION = /^(?:Delta: )?MERGE operation\b/;
|
|
14
|
+
|
|
15
|
+
// The sub-queries that write: "writing new files ...", "Rewriting N files". A group without one
|
|
16
|
+
// only scanned, and a scan may read the merge's source rather than its target.
|
|
17
|
+
const MERGE_WRITE_PHASE = /\b(?:re)?writing\b/i;
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
// Selected executions have a MERGE description. A SQL MERGE's own sub-queries carry the same
|
|
22
|
+
// description but belong to the MergeIntoCommand execution they share a root with, which is
|
|
23
|
+
// attributed through its own node (resolveDeltaCommandTarget), so they are left out. A sub-query
|
|
24
|
+
// of an API merge may have another of its executions as its root, so a root alone does not exclude.
|
|
25
|
+
function selectMergeExecutions(sql ) {
|
|
26
|
+
const isSqlMergeRoot = (id ) =>
|
|
27
|
+
id !== undefined && /^Execute\s+MergeIntoCommand\b/.test(sql.get(id)?.planTree?.name ?? '');
|
|
28
|
+
return [...sql.values()]
|
|
29
|
+
.filter((exec) => MERGE_DESCRIPTION.test(exec.description ?? '') && !isSqlMergeRoot(exec.rootExecutionId))
|
|
30
|
+
.sort((a, b) => a.id - b.id);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A run of consecutive execution ids. Nothing in the log ties the members of one merge together,
|
|
34
|
+
// so adjacency is a guess, and a wrong one fuses or splits merges. Each way it can go wrong is
|
|
35
|
+
// marked `unclean` instead of attributed:
|
|
36
|
+
// - the ids of two merges that ran concurrently interleave, so their start-time spans overlap;
|
|
37
|
+
// - a start time is missing, so the span is unknown.
|
|
38
|
+
// - no member writes, so the one path it names may be a source's.
|
|
39
|
+
// Merges that ran back to back with no other execution between them still fuse into one run; the
|
|
40
|
+
// run then names two paths and gets no target.
|
|
41
|
+
function groupMerges(selected ) {
|
|
42
|
+
const groups = [];
|
|
43
|
+
for (const exec of selected) {
|
|
44
|
+
const last = groups[groups.length - 1];
|
|
45
|
+
if (last && last.members[last.members.length - 1].id + 1 === exec.id) last.members.push(exec);
|
|
46
|
+
else groups.push({ members: [exec], unclean: null });
|
|
47
|
+
}
|
|
48
|
+
const spans = groups.map(({ members }) => {
|
|
49
|
+
const starts = members.map((m) => m.startTime);
|
|
50
|
+
return starts.every((t) => typeof t === 'number') ? [Math.min(...starts), Math.max(...starts)] : null;
|
|
51
|
+
});
|
|
52
|
+
groups.forEach((group, i) => {
|
|
53
|
+
const mine = spans[i];
|
|
54
|
+
if (mine === null) { group.unclean = 'start time missing'; return; }
|
|
55
|
+
const overlaps = spans.some((other, j) => j !== i && other !== null && other[0] <= mine[1] && mine[0] <= other[1]);
|
|
56
|
+
if (overlaps) group.unclean = 'overlaps another merge in time';
|
|
57
|
+
else if (!group.members.some((m) => MERGE_WRITE_PHASE.test(m.description ?? ''))) group.unclean = 'no write phase';
|
|
58
|
+
});
|
|
59
|
+
return groups;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** One DeltaMerge write per run of API merge executions; null target unless one path is certain. */
|
|
63
|
+
export function apiMergeWrites(sql ) {
|
|
64
|
+
return groupMerges(selectMergeExecutions(sql)).map((group) => {
|
|
65
|
+
const first = group.members[0].id;
|
|
66
|
+
const last = group.members[group.members.length - 1].id;
|
|
67
|
+
const resolved = group.unclean === null ? soleLogPath(group.members) : null;
|
|
68
|
+
const span = first === last ? `${first}` : `${first}-${last}`;
|
|
69
|
+
return {
|
|
70
|
+
sqlExecutionId: first,
|
|
71
|
+
nodeId: null,
|
|
72
|
+
command: API_MERGE_COMMAND,
|
|
73
|
+
recognized: true,
|
|
74
|
+
kind: resolved?.kind ?? null,
|
|
75
|
+
target: resolved?.target ?? null,
|
|
76
|
+
outputRows: null,
|
|
77
|
+
raw: `Delta MERGE operation, executions ${span}${group.unclean ? ` (${group.unclean})` : ''}`,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { buildComparison, } from './run-comparison.js';
|
|
2
|
+
import { comparisonVerdict, } from './comparison-verdict.js';
|
|
3
|
+
import { redactComparison } from './redact.js';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// The comparison a surface reports: the CLI's `comparison` object and the shared part of MCP
|
|
9
|
+
// compare_runs. A field added here reaches every surface.
|
|
10
|
+
// `stagePairs` is the one large field (a row per paired stage), so the `summary` view leaves it out.
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
/** `full` carries every field (the CLI); `summary` leaves out `stagePairs` (MCP, which returns them
|
|
18
|
+
* only when asked). */
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
/** The reported projection of a comparison. Key order is the CLI's JSON order. */
|
|
22
|
+
export function comparisonOutput(comparison , view = 'full') {
|
|
23
|
+
return {
|
|
24
|
+
verdict: comparisonVerdict(comparison),
|
|
25
|
+
confidence: comparison.confidence,
|
|
26
|
+
reason: comparison.reason,
|
|
27
|
+
matchedCoverage: comparison.matchedCoverage,
|
|
28
|
+
runtimeCoverage: comparison.runtimeCoverage,
|
|
29
|
+
metrics: comparison.metrics,
|
|
30
|
+
findings: comparison.findings,
|
|
31
|
+
comparisonSchemaVersion: comparison.comparisonSchemaVersion,
|
|
32
|
+
...(view === 'full' ? { stagePairs: comparison.stagePairs } : {}),
|
|
33
|
+
unmatched: comparison.unmatched,
|
|
34
|
+
replanned: comparison.replanned,
|
|
35
|
+
bookkeepingStageIds: comparison.bookkeepingStageIds,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Diffs the two runs and applies `redact` here, so no surface can skip it (stage names carry raw
|
|
40
|
+
* Spark text that can embed a host or IP). `comparison` is the (redacted) raw result for budgets
|
|
41
|
+
* and Markdown; `output` is the projection to serialize. */
|
|
42
|
+
export function buildComparisonOutput(
|
|
43
|
+
baseline , candidate ,
|
|
44
|
+
{ redact, normalizePath, view } = {},
|
|
45
|
+
) {
|
|
46
|
+
const built = buildComparison(baseline, candidate, { normalizePath });
|
|
47
|
+
const comparison = redact ? redactComparison(built) : built;
|
|
48
|
+
return { comparison, output: comparisonOutput(comparison, view) };
|
|
49
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
6e0ac29f74ae092b14c5700dfa86d3273fce7d1af59005c6e2e450a91da92d5e
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Delta write commands whose root node is a bare name in sparkPlanInfo on real logs. The parser keeps
|
|
2
|
+
// their `Arguments:` line (event-handlers.ts) and delta-targets.ts reads their target from it; the
|
|
3
|
+
// handler table there is typed by this list, so a command added here must get a handler.
|
|
4
|
+
export const DELTA_COMMANDS = [
|
|
5
|
+
'MergeIntoCommand', 'UpdateCommand', 'DeleteCommand', 'WriteIntoDelta', 'WriteIntoDeltaCommand',
|
|
6
|
+
'SaveIntoDataSourceCommand',
|
|
7
|
+
] ;
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export function isDeltaCommand(name ) {
|
|
12
|
+
return (DELTA_COMMANDS ).includes(name);
|
|
13
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The `<table path>/_delta_log` paths that plans name: the only trace of which Delta table a command
|
|
2
|
+
// or API merge touched when its plan carries no target. A scan may read a source too, so callers
|
|
3
|
+
// take a path only when exactly one is named.
|
|
4
|
+
import { walkPlanTree } from './plan-tree-walk.js';
|
|
5
|
+
import { isCut, } from './write-target-args.js';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// A URI or absolute path before `/_delta_log`, as printed in `... State #0 - <path>/_delta_log[cols]`
|
|
9
|
+
// scans and `DeltaLogFileIndex [<path>/_delta_log/<file>, ...]`.
|
|
10
|
+
const DELTA_LOG_PATH = /((?:[A-Za-z][A-Za-z0-9+.-]*:\/{1,3}|\/)[^\s,[\]()#]+?)\/_delta_log(?=[/[\],\s)]|$)/g;
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
// Every distinct table path named in the plans; `unreliable` when a plan is missing or a path cut.
|
|
15
|
+
function deltaLogPaths(executions ) {
|
|
16
|
+
const paths = new Set ();
|
|
17
|
+
let unreliable = false;
|
|
18
|
+
for (const exec of executions) {
|
|
19
|
+
if (!exec.planTree) { unreliable = true; continue; }
|
|
20
|
+
walkPlanTree(exec.planTree, (node) => {
|
|
21
|
+
for (const text of [node.name, node.detail ?? '']) {
|
|
22
|
+
const mentions = text.split('/_delta_log').length - 1;
|
|
23
|
+
if (mentions === 0) continue;
|
|
24
|
+
const matches = [...text.matchAll(DELTA_LOG_PATH)];
|
|
25
|
+
// A `/_delta_log` that is not read as a path is a table this set cannot name.
|
|
26
|
+
if (matches.length < mentions) unreliable = true;
|
|
27
|
+
for (const m of matches) {
|
|
28
|
+
if (isCut(m[1])) unreliable = true;
|
|
29
|
+
else paths.add(m[1].replace(/\/+$/, ''));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}, { dedupe: true });
|
|
33
|
+
}
|
|
34
|
+
return { paths, unreliable };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The one table path the plans of `executions` name, or null for none, several or an unreadable plan. */
|
|
38
|
+
export function soleLogPath(executions ) {
|
|
39
|
+
const { paths, unreliable } = deltaLogPaths(executions);
|
|
40
|
+
const [path] = paths;
|
|
41
|
+
return !unreliable && paths.size === 1 ? { kind: 'path', target: path } : null;
|
|
42
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Delta write targets that the plan node's simpleString does not carry. A Delta command node is a
|
|
2
|
+
// bare name in sparkPlanInfo on real logs; its target is read from the `Arguments:` line the parser
|
|
3
|
+
// keeps (SqlExecution.commandArguments, one reader per command in COMMANDS), and for a command that
|
|
4
|
+
// always scans its target (MERGE, UPDATE, DELETE) from the single `_delta_log` path of the executions
|
|
5
|
+
// sharing its root. A write command may read another Delta table as its source, so it never takes
|
|
6
|
+
// that step. Otherwise the target is null: a rule that cannot name exactly one target names none.
|
|
7
|
+
import { isDeltaCommand, } from './delta-commands.js';
|
|
8
|
+
import { soleLogPath } from './delta-log-paths.js';
|
|
9
|
+
import { isCut, splitArgs, tableName, } from './write-target-args.js';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
/** Executions grouped by rootExecutionId, each group excluding the root itself. */
|
|
18
|
+
export function indexByRoot(sql ) {
|
|
19
|
+
const byRoot = new Map ();
|
|
20
|
+
for (const exec of sql.values()) {
|
|
21
|
+
if (exec.rootExecutionId === undefined || exec.rootExecutionId === exec.id) continue;
|
|
22
|
+
const list = byRoot.get(exec.rootExecutionId);
|
|
23
|
+
if (list) list.push(exec);
|
|
24
|
+
else byRoot.set(exec.rootExecutionId, [exec]);
|
|
25
|
+
}
|
|
26
|
+
return byRoot;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The text after `<Command>\nArguments: `, or null when the execution kept none for this command.
|
|
30
|
+
function argumentsOf(exec , command ) {
|
|
31
|
+
const kept = exec.commandArguments;
|
|
32
|
+
const head = `${command}\nArguments: `;
|
|
33
|
+
return kept !== undefined && kept.startsWith(head) ? kept.slice(head.length) : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// A catalog table is printed as its quoted identifier, `catalog`.`db`.`t`, as one top-level
|
|
37
|
+
// argument. Two or more such arguments (or none) leave the table unnamed. The session catalog is
|
|
38
|
+
// implied by a Hive or V1 table name, so it is dropped: `spark_catalog`.`db`.`t` is db.t.
|
|
39
|
+
const QUOTED_TABLE = /^`[^`]+`(?:\.`[^`]+`){1,2}$/;
|
|
40
|
+
|
|
41
|
+
function tableFromArguments(args ) {
|
|
42
|
+
const candidates = splitArgs(args).filter((arg) => QUOTED_TABLE.test(arg.text));
|
|
43
|
+
if (candidates.length !== 1 || !candidates[0].terminated) return null;
|
|
44
|
+
const name = tableName(candidates[0].text);
|
|
45
|
+
if (!name || isCut(name)) return null;
|
|
46
|
+
const parts = name.split('.');
|
|
47
|
+
return { kind: 'table', target: parts.length === 3 && parts[0] === 'spark_catalog' ? parts.slice(1).join('.') : name };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// SaveIntoDataSourceCommand prints `<provider>@<hash>, [k=v, k2=v2], <mode>`. The target is the
|
|
51
|
+
// one `path` option of a Delta provider.
|
|
52
|
+
function pathFromSaveOptions(args ) {
|
|
53
|
+
const options = splitArgs(args)[1];
|
|
54
|
+
if (!options?.terminated || !/^\[[\s\S]*\]$/.test(options.text)) return null;
|
|
55
|
+
const body = options.text.slice(1, -1);
|
|
56
|
+
const keys = [...body.matchAll(/(?:^|, )([A-Za-z0-9_.-]+)=/g)];
|
|
57
|
+
const paths = keys.flatMap((key, i) => {
|
|
58
|
+
if (key[1].toLowerCase() !== 'path') return [];
|
|
59
|
+
return [body.slice(key.index + key[0].length, i + 1 < keys.length ? keys[i + 1].index : body.length)];
|
|
60
|
+
});
|
|
61
|
+
if (paths.length !== 1 || isCut(paths[0]) || /\*{5,}\(redacted\)/.test(paths[0])) return null;
|
|
62
|
+
return { kind: 'path', target: paths[0] };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
const tableCommand = { fromArguments: tableFromArguments, scansTarget: false };
|
|
73
|
+
const scanningTableCommand = { ...tableCommand, scansTarget: true };
|
|
74
|
+
|
|
75
|
+
// One entry per Delta command: a new command is added here (and to DELTA_COMMANDS), not in the resolver.
|
|
76
|
+
const COMMANDS = {
|
|
77
|
+
MergeIntoCommand: scanningTableCommand,
|
|
78
|
+
UpdateCommand: scanningTableCommand,
|
|
79
|
+
DeleteCommand: scanningTableCommand,
|
|
80
|
+
WriteIntoDelta: tableCommand,
|
|
81
|
+
WriteIntoDeltaCommand: tableCommand,
|
|
82
|
+
// Only the arguments say a save is a Delta one; a JDBC save with a Delta table in its source
|
|
83
|
+
// would otherwise take that table as its target.
|
|
84
|
+
SaveIntoDataSourceCommand: {
|
|
85
|
+
fromArguments: (args) => (args.includes('DeltaDataSource') ? pathFromSaveOptions(args) : null),
|
|
86
|
+
scansTarget: false,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** Target of a Delta command whose simpleString names none; `node` must be the root of `exec`'s plan. */
|
|
91
|
+
export function resolveDeltaCommandTarget(
|
|
92
|
+
command , exec , node , byRoot ,
|
|
93
|
+
) {
|
|
94
|
+
if (!isDeltaCommand(command) || node !== exec.planTree) return null;
|
|
95
|
+
const handler = COMMANDS[command];
|
|
96
|
+
const args = argumentsOf(exec, command);
|
|
97
|
+
const found = args === null ? null : handler.fromArguments(args);
|
|
98
|
+
if (found || !handler.scansTarget) return found;
|
|
99
|
+
// Child executions are the root's own only when it is the root: a command run under another
|
|
100
|
+
// execution shares that execution's children, which may belong to unrelated work.
|
|
101
|
+
return exec.rootExecutionId === exec.id ? soleLogPath(byRoot.get(exec.id) ?? []) : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// An append or overwrite of an existing Delta table (saveAsTable, INSERT INTO or OVERWRITE) is an
|
|
105
|
+
// `AppendDataExecV1` or `OverwriteByExpressionExecV1` whose first argument is the table object:
|
|
106
|
+
// `DeltaTableV2(<session>,<path>,Some(CatalogTable(\nCatalog: c\nDatabase: d\nTable: t\n...)),...)`.
|
|
107
|
+
// Its CatalogTable block names the table; a path-based table has `None` there and only the path.
|
|
108
|
+
const DELTA_TABLE_V2 = /^\w+ DeltaTableV2\([^,\s]*,([^,\n]*),(?:Some\(CatalogTable\(\n(?:Catalog: (\S+)\n)?Database: (\S+)\nTable: (\S+)\n|None,)/;
|
|
109
|
+
const SIMPLE_NAME = /^[A-Za-z0-9_$]+$/;
|
|
110
|
+
|
|
111
|
+
/** Target of a V1-fallback write on a DeltaTableV2 table, or null when the detail has another shape. */
|
|
112
|
+
export function parseDeltaTableV2(detail ) {
|
|
113
|
+
const m = DELTA_TABLE_V2.exec(detail);
|
|
114
|
+
if (!m) return null;
|
|
115
|
+
const [, path, catalog, database, table] = m;
|
|
116
|
+
if (table === undefined) return path && !isCut(path) ? { kind: 'path', target: path } : null;
|
|
117
|
+
if (![database, table, catalog ?? 'spark_catalog'].every((part) => SIMPLE_NAME.test(part))) return null;
|
|
118
|
+
// The session catalog is implied by a V1 name, so it is dropped (as for a Delta command).
|
|
119
|
+
const name = `${database}.${table}`;
|
|
120
|
+
return { kind: 'table', target: catalog === undefined || catalog === 'spark_catalog' ? name : `${catalog}.${name}` };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The append a Delta CTAS or RTAS runs under its own root: its staged table has no name. */
|
|
124
|
+
export function isStagedDeltaWrite(write ) {
|
|
125
|
+
return (write.command === 'AppendDataExecV1' || write.command === 'OverwriteByExpressionExecV1')
|
|
126
|
+
&& /^\w+ \S*DeltaCatalog\$StagedDeltaTableV2@/.test(write.raw);
|
|
127
|
+
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
} from './event-schemas.js';
|
|
21
|
+
import { DELTA_COMMANDS } from './delta-commands.js';
|
|
21
22
|
import { assertNever } from './assert-never.js';
|
|
22
23
|
import { finalizeStage } from './stage-quantiles.js';
|
|
23
24
|
import { MAX_FAILURE_DETAILS_PER_STAGE, extractTaskFailureDetail, taskFailureKey, } from './task-failure.js';
|
|
@@ -198,6 +199,9 @@ const MAX_TASK_SAMPLES = 20;
|
|
|
198
199
|
|
|
199
200
|
|
|
200
201
|
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
201
205
|
|
|
202
206
|
|
|
203
207
|
|
|
@@ -279,6 +283,10 @@ export function buildChunkDecoder() {
|
|
|
279
283
|
// line again after every slice scanned about 10 GB on a 100 MB line.
|
|
280
284
|
let pendingIsSqlEvent = false;
|
|
281
285
|
let keyTail = '';
|
|
286
|
+
// Where the pending line's plan-description value starts, once its key is found but too little
|
|
287
|
+
// of the value has arrived to tell whether it is a small Delta command description (see
|
|
288
|
+
// descriptionDisposition); -1 otherwise.
|
|
289
|
+
let undecidedValueStart = -1;
|
|
282
290
|
let skippingPlanDescription = false;
|
|
283
291
|
// Length of the backslash run the skipped bytes ended with, which escapes a quote at the start
|
|
284
292
|
// of the next chunk when odd.
|
|
@@ -312,11 +320,24 @@ export function buildChunkDecoder() {
|
|
|
312
320
|
keyTail = '';
|
|
313
321
|
appended = pending; // nothing of it was searched while it was shorter than the prefix
|
|
314
322
|
}
|
|
315
|
-
|
|
316
|
-
if (
|
|
323
|
+
let valueStart = undecidedValueStart;
|
|
324
|
+
if (valueStart === -1) {
|
|
325
|
+
const keyAt = findPlanKey(appended);
|
|
326
|
+
if (keyAt === -1) return; // the key may still arrive in a later chunk
|
|
327
|
+
valueStart = keyAt + PLAN_DESCRIPTION_KEY.length;
|
|
328
|
+
}
|
|
329
|
+
if (closingQuoteIndex(pending, valueStart) !== -1) { // complete: stripPlanDescription handles it
|
|
330
|
+
undecidedValueStart = -1;
|
|
331
|
+
pendingSettled = true;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// A small kept description is not skipped: the line completes in a later chunk and
|
|
335
|
+
// stripPlanDescription keeps its arguments line.
|
|
336
|
+
const disposition = descriptionDisposition(pending, valueStart);
|
|
337
|
+
if (disposition === 'wait') { undecidedValueStart = valueStart; return; }
|
|
338
|
+
undecidedValueStart = -1;
|
|
317
339
|
pendingSettled = true;
|
|
318
|
-
|
|
319
|
-
if (closingQuoteIndex(pending, valueStart) !== -1) return; // complete: stripPlanDescription empties it
|
|
340
|
+
if (disposition === 'keep') return;
|
|
320
341
|
// Only a chunk whose last byte is a backslash carries a run over; any other last byte (such
|
|
321
342
|
// as part of a split multibyte char) ends it.
|
|
322
343
|
let run = 0;
|
|
@@ -378,6 +399,7 @@ export function buildChunkDecoder() {
|
|
|
378
399
|
pending = pendingHead = appended = start < text.length ? text.substring(start) : '';
|
|
379
400
|
pendingSettled = false;
|
|
380
401
|
pendingIsSqlEvent = false;
|
|
402
|
+
undecidedValueStart = -1;
|
|
381
403
|
}
|
|
382
404
|
if (!pendingSettled && pending !== '') settlePending(buffer[buffer.length - 1], appended);
|
|
383
405
|
}
|
|
@@ -1109,6 +1131,10 @@ export function startSqlExecution(event
|
|
|
1109
1131
|
sparkPlanInfo,
|
|
1110
1132
|
hadAdaptiveUpdate: false,
|
|
1111
1133
|
};
|
|
1134
|
+
if (event.rootExecutionId !== undefined) exec.rootExecutionId = event.rootExecutionId;
|
|
1135
|
+
if (event.physicalPlanDescription && KEPT_ARGUMENTS.test(event.physicalPlanDescription)) {
|
|
1136
|
+
exec.commandArguments = event.physicalPlanDescription;
|
|
1137
|
+
}
|
|
1112
1138
|
state.sqlExecutions.set(exec.id, exec);
|
|
1113
1139
|
// A restarted execution carries a new plan: its next end must resolve it again.
|
|
1114
1140
|
state.resolvedPlanExecutions.delete(exec.id);
|
|
@@ -1295,13 +1321,32 @@ const KNOWN_EVENT_TYPES = new Set(
|
|
|
1295
1321
|
);
|
|
1296
1322
|
|
|
1297
1323
|
// SQLExecutionStart and SQLAdaptiveExecutionUpdate carry `physicalPlanDescription`, Spark's text
|
|
1298
|
-
// rendering of the plan.
|
|
1299
|
-
//
|
|
1324
|
+
// rendering of the plan. The plan tree comes from sparkPlanInfo, yet on a real 3.5 GB log the
|
|
1325
|
+
// description was 72% of the AQE-update bytes, which were themselves 73% of the log. Cutting its
|
|
1300
1326
|
// string value out before JSON.parse halves the parse cost of those lines.
|
|
1327
|
+
//
|
|
1328
|
+
// One exception: a Delta write command's node prints only its bare name in sparkPlanInfo, and its
|
|
1329
|
+
// target table appears only on its `Arguments:` line of the description. When the description is
|
|
1330
|
+
// that of one of KEPT_ARGUMENTS_COMMANDS (checked on its first bytes, so no other line pays for it),
|
|
1331
|
+
// the value is cut down to that one line, never kept whole. A value that spans decoder chunks is
|
|
1332
|
+
// still dropped unread (buildChunkDecoder), so the write target then falls back to its child paths.
|
|
1301
1333
|
const SQL_UI_EVENT_PREFIX = '{"Event":"org.apache.spark.sql.execution.ui.SparkListenerSQL';
|
|
1302
1334
|
const PLAN_DESCRIPTION_KEY = '"physicalPlanDescription":"';
|
|
1303
1335
|
|
|
1304
|
-
const QUOTE = 0x22, BACKSLASH = 0x5c, NEWLINE = 0x0a;
|
|
1336
|
+
const QUOTE = 0x22, BACKSLASH = 0x5c, NEWLINE = 0x0a, LOWER_N = 0x6e;
|
|
1337
|
+
|
|
1338
|
+
// Delta commands whose root node carries no target in sparkPlanInfo. The description of a
|
|
1339
|
+
// command execution starts `== Physical Plan ==\nExecute <Command> (1)` (JSON-escaped newline).
|
|
1340
|
+
const KEPT_ARGUMENTS_COMMANDS = new Set(DELTA_COMMANDS);
|
|
1341
|
+
// Those whose description is just the command (no child plan printed under it), so it is small
|
|
1342
|
+
// enough to decode whole when it spans decoder chunks. SaveIntoDataSourceCommand prints the whole
|
|
1343
|
+
// query under it, which can be as large as any other plan description.
|
|
1344
|
+
const SMALL_DESCRIPTION_COMMANDS = new Set(
|
|
1345
|
+
[...KEPT_ARGUMENTS_COMMANDS].filter((command) => command !== 'SaveIntoDataSourceCommand'),
|
|
1346
|
+
);
|
|
1347
|
+
const PLAN_HEADER = '== Physical Plan ==\\nExecute ';
|
|
1348
|
+
// The kept form once JSON-parsed: `<Command>\nArguments: <line>`.
|
|
1349
|
+
const KEPT_ARGUMENTS = /^[A-Za-z]+\nArguments: /;
|
|
1305
1350
|
|
|
1306
1351
|
// Index of the closing quote of the JSON string whose content starts at `valueStart`: the first
|
|
1307
1352
|
// quote preceded by an even number of backslashes. -1 when the string is unterminated.
|
|
@@ -1326,7 +1371,48 @@ function closingQuoteAt(buf , limit , carried )
|
|
|
1326
1371
|
return -1;
|
|
1327
1372
|
}
|
|
1328
1373
|
|
|
1329
|
-
//
|
|
1374
|
+
// The command named by a description that starts at text[valueStart] with PLAN_HEADER, when text
|
|
1375
|
+
// holds the whole name; null otherwise.
|
|
1376
|
+
function describedCommand(text , valueStart ) {
|
|
1377
|
+
if (!text.startsWith(PLAN_HEADER, valueStart)) return null;
|
|
1378
|
+
const nameStart = valueStart + PLAN_HEADER.length;
|
|
1379
|
+
let nameEnd = nameStart;
|
|
1380
|
+
while (nameEnd < text.length && /[A-Za-z]/.test(text[nameEnd])) nameEnd++;
|
|
1381
|
+
return nameEnd < text.length ? text.slice(nameStart, nameEnd) : null;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// What to do with an open description value whose first bytes are text[valueStart...]: 'keep' it
|
|
1385
|
+
// (a small Delta command description), 'skip' it, or 'wait' when too little has arrived to tell.
|
|
1386
|
+
function descriptionDisposition(text , valueStart ) {
|
|
1387
|
+
const seen = text.slice(valueStart, valueStart + PLAN_HEADER.length);
|
|
1388
|
+
if (!PLAN_HEADER.startsWith(seen)) return 'skip';
|
|
1389
|
+
if (seen.length < PLAN_HEADER.length) return 'wait';
|
|
1390
|
+
const command = describedCommand(text, valueStart);
|
|
1391
|
+
if (command === null) return 'wait'; // the command name is still arriving
|
|
1392
|
+
return SMALL_DESCRIPTION_COMMANDS.has(command) ? 'keep' : 'skip';
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// The `<Command>\nArguments: <line>` part (still JSON-escaped) of the root command of a plan
|
|
1396
|
+
// description whose value spans line[valueStart, quote), or null when it is not one of
|
|
1397
|
+
// KEPT_ARGUMENTS_COMMANDS. SaveIntoDataSourceCommand is kept only for the Delta source: its other
|
|
1398
|
+
// providers (JDBC) print option maps that this tool has no use for.
|
|
1399
|
+
function keptCommandArguments(line , valueStart , quote ) {
|
|
1400
|
+
const command = describedCommand(line, valueStart);
|
|
1401
|
+
if (command === null || !KEPT_ARGUMENTS_COMMANDS.has(command)) return null;
|
|
1402
|
+
const key = `${command}\\nArguments: `;
|
|
1403
|
+
const at = line.indexOf(key, valueStart + PLAN_HEADER.length + command.length);
|
|
1404
|
+
if (at === -1 || at >= quote) return null;
|
|
1405
|
+
// The line ends at the first escaped newline: a backslash starts a two-char escape.
|
|
1406
|
+
let end = at + key.length;
|
|
1407
|
+
while (end < quote && !(line.charCodeAt(end) === BACKSLASH && line.charCodeAt(end + 1) === LOWER_N)) {
|
|
1408
|
+
end += line.charCodeAt(end) === BACKSLASH ? 2 : 1;
|
|
1409
|
+
}
|
|
1410
|
+
const kept = line.slice(at, Math.min(end, quote));
|
|
1411
|
+
return command === 'SaveIntoDataSourceCommand' && !kept.includes('DeltaDataSource') ? null : kept;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// Returns `line` with the physicalPlanDescription string value emptied (or cut to the kept
|
|
1415
|
+
// arguments line, see keptCommandArguments), or `line` unchanged when
|
|
1330
1416
|
// the key isn't found in Spark's compact form. The key pattern can't match inside another JSON
|
|
1331
1417
|
// string: there its quotes would be backslash-escaped. buildChunkDecoder already empties a value
|
|
1332
1418
|
// that spans chunks, so here the value is empty or within one chunk.
|
|
@@ -1339,7 +1425,7 @@ export function stripPlanDescription(line ) {
|
|
|
1339
1425
|
const quote = closingQuoteIndex(line, valueStart);
|
|
1340
1426
|
// Unterminated string (a truncated line): leave it for JSON.parse to reject.
|
|
1341
1427
|
if (quote === -1) return line;
|
|
1342
|
-
return line.slice(0, valueStart) + line.slice(quote);
|
|
1428
|
+
return line.slice(0, valueStart) + (keptCommandArguments(line, valueStart, quote) ?? '') + line.slice(quote);
|
|
1343
1429
|
}
|
|
1344
1430
|
|
|
1345
1431
|
const TASK_END_PREFIX = '{"Event":"SparkListenerTaskEnd",';
|
|
@@ -321,13 +321,15 @@ export const TaskEndEventSchema = z.object({
|
|
|
321
321
|
export const SqlExecutionStartEventSchema = z.object({
|
|
322
322
|
Event: z.literal('org.apache.spark.sql.execution.ui.SparkListenerSQLExecutionStart'),
|
|
323
323
|
executionId: z.number(),
|
|
324
|
+
rootExecutionId: z.number().optional(),
|
|
324
325
|
description: z.string().optional(),
|
|
326
|
+
physicalPlanDescription: z.string().optional(),
|
|
325
327
|
time: z.number(),
|
|
326
328
|
sparkPlanInfo: SparkPlanInfoFieldSchema,
|
|
327
329
|
});
|
|
328
330
|
|
|
329
331
|
// applyAdaptiveExecutionUpdate: AQE re-plans mid-query and re-emits sparkPlanInfo for the same
|
|
330
|
-
// executionId; last write wins.
|
|
332
|
+
// executionId; last write wins. This schema does not declare physicalPlanDescription: nothing reads
|
|
331
333
|
// it, so zod strips it (dispatchLine already empties it before JSON.parse, see stripPlanDescription).
|
|
332
334
|
export const SqlAdaptiveExecutionUpdateEventSchema = z.object({
|
|
333
335
|
Event: z.literal('org.apache.spark.sql.execution.ui.SparkListenerSQLAdaptiveExecutionUpdate'),
|
|
@@ -405,13 +405,13 @@ function verdictJson(model ) {
|
|
|
405
405
|
// A WeakMap needs no invalidation: once mcp-tools.ts evicts the appModel, this entry is collectible.
|
|
406
406
|
// Each cache is split first by the overrides object the report ran under (one fixed, frozen object
|
|
407
407
|
// per CLI invocation or MCP server process; DEFAULT_THRESHOLDS for the specification's).
|
|
408
|
-
|
|
408
|
+
|
|
409
409
|
const DEFAULT_THRESHOLDS = {};
|
|
410
410
|
const jsonCache = new WeakMap();
|
|
411
411
|
// The redacted report, keyed by the unredacted appModel it was built from.
|
|
412
412
|
const redactedJsonCache = new WeakMap();
|
|
413
413
|
|
|
414
|
-
function cacheFor(cache
|
|
414
|
+
function cacheFor (cache , thresholds ) {
|
|
415
415
|
const key = thresholds ?? DEFAULT_THRESHOLDS;
|
|
416
416
|
let byModel = cache.get(key);
|
|
417
417
|
if (!byModel) {
|
|
@@ -421,14 +421,23 @@ function cacheFor(cache , thresholds
|
|
|
421
421
|
return byModel;
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
-
|
|
424
|
+
const findingsCache = new WeakMap();
|
|
425
|
+
|
|
426
|
+
/** The run's detector catalog and config audit, computed once per run and overrides so the report
|
|
427
|
+
* and the blocks built beside it (run-output.ts) share one analysis. */
|
|
428
|
+
export function runFindings(appModel , thresholds ) {
|
|
429
|
+
const cache = cacheFor(findingsCache, thresholds);
|
|
430
|
+
const cached = cache.get(appModel);
|
|
431
|
+
if (cached) return cached;
|
|
425
432
|
const { app, stages, executors, sql, jobs, runAggregates } = appModel;
|
|
426
433
|
const catalog = analyze(
|
|
427
434
|
app, stages, executors?.added ?? [], executors?.removed ?? [],
|
|
428
435
|
jobs ?? new Map(), sql ?? new Map(),
|
|
429
436
|
runAggregates ?? null, { thresholds },
|
|
430
437
|
);
|
|
431
|
-
|
|
438
|
+
const result = { catalog, config: auditConfig(app) };
|
|
439
|
+
cache.set(appModel, result);
|
|
440
|
+
return result;
|
|
432
441
|
}
|
|
433
442
|
|
|
434
443
|
// Redacts the model and findings before the report derives any text from them, the same order
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from './mcp-tools.js';
|
|
7
7
|
import { listRuns } from './list-runs.js';
|
|
8
8
|
|
|
9
|
+
import { MAX_NORMALIZE_PATTERN_LENGTH, MAX_NORMALIZE_PATTERNS } from './stage-alignment.js';
|
|
9
10
|
|
|
10
11
|
const sourceSchema = z.union([
|
|
11
12
|
z.object({ path: z.string() }),
|
|
@@ -71,7 +72,7 @@ export function createMcpServer({ thresholds }
|
|
|
71
72
|
}, (params) => toolResult(listRuns(params)));
|
|
72
73
|
|
|
73
74
|
server.registerTool('diagnose_run', {
|
|
74
|
-
description: 'Diagnose a Spark run: the dashboard verdict (title, summary, and the top places to look, ranked by potential savings), thresholded findings with remediation text, an impact-ranked fix recommendation rollup, clean-check status,
|
|
75
|
+
description: 'Diagnose a Spark run: the dashboard verdict (title, summary, and the top places to look, ranked by potential savings), thresholded findings with remediation text, an impact-ranked fix recommendation rollup, clean-check status, the checks the log lacked the data to run, and the writeTargets, metrics and effectiveConf blocks of the CLI report. When the server was started with --thresholds, findings and clean checks from a tuned detector carry tunedThresholds, and their impact estimates are uncalibrated.',
|
|
75
76
|
inputSchema: {
|
|
76
77
|
...runRefSchema, redact: z.boolean().optional(),
|
|
77
78
|
include: z.array(z.enum(['summary', 'evidenceAvailability', 'detectors'])).optional(),
|
|
@@ -91,15 +92,19 @@ export function createMcpServer({ thresholds }
|
|
|
91
92
|
));
|
|
92
93
|
|
|
93
94
|
server.registerTool('compare_runs', {
|
|
94
|
-
description: 'Compare two runs: categorized findings delta and
|
|
95
|
+
description: 'Compare two runs: the verdict, metric deltas (metrics), categorized findings delta (findings) and the stage pairs (stagePairs) with the share of executor run time they cover (runtimeCoverage). confidence is ok, low or insufficient. metricDeltas and findingsDelta repeat metrics and findings under their earlier names and will be removed.',
|
|
95
96
|
inputSchema: {
|
|
96
97
|
runIdA: z.string().optional(), sourceA: sourceSchema.optional(),
|
|
97
98
|
...secondRunRefSchema,
|
|
98
99
|
redact: z.boolean().optional(),
|
|
100
|
+
include: z.array(z.enum(['stagePairs'])).optional()
|
|
101
|
+
.describe('Extra blocks to return. stagePairs: one row per paired stage with its deltas, which can be large.'),
|
|
102
|
+
normalizePath: z.array(z.string().max(MAX_NORMALIZE_PATTERN_LENGTH)).max(MAX_NORMALIZE_PATTERNS).optional()
|
|
103
|
+
.describe('Regular expressions whose every match in a plan node\'s text is replaced with a fixed token before stages are paired, so run-specific text (an output directory that differs per run) does not keep the same stage from pairing. A generic pattern, not only for paths. Affects stage pairing, runtimeCoverage and confidence, never findings. A pattern that backtracks catastrophically can stall the server: keep them simple and anchored.'),
|
|
99
104
|
...formatSchema,
|
|
100
105
|
},
|
|
101
|
-
}, ({ runIdA, sourceA, runIdB, sourceB, redact, format }) => toolResultWithMarkdown(
|
|
102
|
-
compareRuns({ runId: runIdA, source: sourceA }, { runId: runIdB, source: sourceB }, { redact, markdown: format === 'md', thresholds }),
|
|
106
|
+
}, ({ runIdA, sourceA, runIdB, sourceB, redact, include, normalizePath, format }) => toolResultWithMarkdown(
|
|
107
|
+
compareRuns({ runId: runIdA, source: sourceA }, { runId: runIdB, source: sourceB }, { redact, markdown: format === 'md', thresholds, normalizePath, include }),
|
|
103
108
|
));
|
|
104
109
|
|
|
105
110
|
server.registerTool('evaluate_budgets', {
|
|
@@ -112,14 +117,16 @@ export function createMcpServer({ thresholds }
|
|
|
112
117
|
sourceB: sourceSchema.optional().describe('Optional candidate run, compared against source/runId as the regression baseline (maxRegressionPct/failOnIntroduced). When given, the absolute budgets (maxRuntimeMs etc.) are evaluated on this run.'),
|
|
113
118
|
runIdB: secondRunRefSchema.runIdB.describe('Same as `sourceB`, referencing an already-resolved run by id.'),
|
|
114
119
|
maxRegressionPct: z.number().optional(), regressionMetric: z.string().optional(), failOnIntroduced: z.string().optional(),
|
|
120
|
+
regressionBudgets: z.array(z.object({ metric: z.string(), maxPct: z.number().nonnegative() })).optional()
|
|
121
|
+
.describe('Further regression budgets, one per metric (the CLI\'s repeated --regression-budget). A metric can be budgeted once across this list and maxRegressionPct/regressionMetric.'),
|
|
115
122
|
},
|
|
116
123
|
}, ({
|
|
117
124
|
source, runId, runIdB, sourceB,
|
|
118
125
|
maxRuntimeMs, maxSpillGb, maxSkewRatio, maxFailedTaskRatePct, minEfficiencyPct,
|
|
119
|
-
maxRegressionPct, regressionMetric, failOnIntroduced,
|
|
126
|
+
maxRegressionPct, regressionMetric, failOnIntroduced, regressionBudgets,
|
|
120
127
|
}) => toolResult(evaluateBudgetsForRun(
|
|
121
128
|
{ source, runId },
|
|
122
|
-
{ maxRuntimeMs, maxSpillGb, maxSkewRatio, maxFailedTaskRatePct, minEfficiencyPct, maxRegressionPct, regressionMetric, failOnIntroduced },
|
|
129
|
+
{ maxRuntimeMs, maxSpillGb, maxSkewRatio, maxFailedTaskRatePct, minEfficiencyPct, maxRegressionPct, regressionMetric, failOnIntroduced, regressionBudgets },
|
|
123
130
|
(runIdB || sourceB) ? { runId: runIdB, source: sourceB } : undefined,
|
|
124
131
|
{ thresholds },
|
|
125
132
|
)));
|