sparkforensics-mcp 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/sparkforensics-mcp.mjs +41 -10
- package/package.json +1 -1
- package/vendor-core/allocation.js +106 -0
- package/vendor-core/analyzer.js +168 -60
- package/vendor-core/check-coverage.js +88 -0
- package/vendor-core/cli/budgets.js +54 -27
- package/vendor-core/cli/collect-run.js +84 -32
- package/vendor-core/cli/regression-budgets.js +83 -0
- package/vendor-core/cli/threshold-config.js +28 -0
- package/vendor-core/comparison-verdict.js +177 -0
- package/vendor-core/core-source-hash.txt +1 -0
- package/vendor-core/core-usage-locality.js +56 -2
- package/vendor-core/detector-docs.js +58 -0
- package/vendor-core/detectors.js +1094 -500
- package/vendor-core/docs-config.js +0 -36
- package/vendor-core/docs-content/chapters/nav-index.json +31 -0
- package/vendor-core/docs-content/detection/cache.md +3 -2
- package/vendor-core/docs-content/detection/cfg.md +9 -8
- package/vendor-core/docs-content/detection/chrn.md +1 -2
- package/vendor-core/docs-content/detection/cold.md +4 -2
- package/vendor-core/docs-content/detection/cstor.md +9 -0
- package/vendor-core/docs-content/detection/fail.md +3 -2
- package/vendor-core/docs-content/detection/gc.md +3 -2
- package/vendor-core/docs-content/detection/host.md +2 -1
- package/vendor-core/docs-content/detection/local.md +1 -1
- package/vendor-core/docs-content/detection/mem.md +5 -2
- package/vendor-core/docs-content/detection/plan.md +2 -1
- package/vendor-core/docs-content/detection/sfail.md +2 -1
- package/vendor-core/docs-content/detection/shape.md +5 -4
- package/vendor-core/docs-content/detection/skew.md +3 -1
- package/vendor-core/docs-content/detection/slow.md +2 -2
- package/vendor-core/docs-content/detection/spec.md +2 -3
- package/vendor-core/docs-content/detection/spill.md +1 -1
- package/vendor-core/docs-site-config.js +3 -0
- package/vendor-core/effective-conf.js +107 -0
- package/vendor-core/efficiency-model.js +8 -6
- package/vendor-core/event-handlers.js +321 -44
- package/vendor-core/event-schemas.js +23 -0
- package/vendor-core/evidence-report.js +432 -115
- package/vendor-core/export-data.js +79 -6
- package/vendor-core/finding-action-label.js +9 -88
- package/vendor-core/finding-filter-predicate.js +9 -0
- package/vendor-core/finding-generic-recommendation.js +26 -105
- package/vendor-core/finding-names.js +28 -45
- package/vendor-core/finding-presentation.js +368 -0
- package/vendor-core/finding-tag-help.js +110 -0
- package/vendor-core/finding-types.js +373 -0
- package/vendor-core/findings-of-type.js +11 -0
- package/vendor-core/format-utils.js +96 -30
- package/vendor-core/html-export.js +51 -0
- package/vendor-core/impact-band.js +21 -8
- package/vendor-core/impact-estimator.js +25 -520
- package/vendor-core/impact-format.js +115 -0
- package/vendor-core/impact-model.js +197 -0
- package/vendor-core/ingest.js +6 -2
- package/vendor-core/intervals.js +13 -0
- package/vendor-core/list-runs.js +7 -5
- package/vendor-core/load-vendored.js +70 -5
- package/vendor-core/mcp-server-factory.js +14 -10
- package/vendor-core/mcp-tools.js +105 -45
- package/vendor-core/model-assembler.js +35 -1
- package/vendor-core/occupancy.js +1 -1
- package/vendor-core/parser-worker.js +2 -2
- package/vendor-core/plan-graph-model.js +3 -2
- package/vendor-core/plan-node-detail.js +1 -1
- package/vendor-core/proxy.js +3 -1
- package/vendor-core/python-stage.js +25 -0
- package/vendor-core/recommendation-rollup.js +70 -3
- package/vendor-core/redact.js +96 -37
- package/vendor-core/remediation.js +20 -0
- package/vendor-core/run-comparison.js +73 -29
- package/vendor-core/run-interpretation.js +291 -0
- package/vendor-core/run-metrics.js +198 -0
- package/vendor-core/run-outcome.js +74 -0
- package/vendor-core/run-payload.js +17 -0
- package/vendor-core/run-shape.js +40 -0
- package/vendor-core/run-totals.js +24 -0
- package/vendor-core/run-verdict.js +352 -0
- package/vendor-core/scaling-sim.js +4 -5
- package/vendor-core/scorecard-estimates.js +63 -0
- package/vendor-core/session-snapshot.js +7 -0
- package/vendor-core/shs-schemas.js +2 -2
- package/vendor-core/spark-memory.js +17 -0
- package/vendor-core/sql-stages.js +11 -0
- package/vendor-core/stage-plan-nodes.js +18 -0
- package/vendor-core/stage-quantiles.js +6 -0
- package/vendor-core/threshold-overrides.js +160 -0
- package/vendor-core/threshold-summary.js +11 -33
- package/vendor-core/types.js +54 -42
- package/vendor-core/wall-clock.js +1 -12
- package/vendor-core/wasted-core-hours.js +12 -9
- package/vendor-core/write-targets.js +312 -0
|
@@ -18,6 +18,8 @@ export const LOCALITY_TIERS = ['PROCESS_LOCAL', 'NODE_LOCAL', 'RACK_LO
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
export function computeLocalityAreaSeries(
|
|
@@ -25,7 +27,7 @@ export function computeLocalityAreaSeries(
|
|
|
25
27
|
{ bucketWidthMs = 60_000, tiers = LOCALITY_TIERS } = {},
|
|
26
28
|
) {
|
|
27
29
|
const valid = stages.filter(s => (s.completedAt ?? 0) > (s.submittedAt ?? 0) && (s.executorRunTime ?? 0) > 0);
|
|
28
|
-
if (valid.length === 0) return { labels: [], series: {} };
|
|
30
|
+
if (valid.length === 0) return { labels: [], series: {}, endTime: 0 };
|
|
29
31
|
const startTime = valid.reduce((m, s) => Math.min(m, s.submittedAt ?? m), Infinity);
|
|
30
32
|
const endTime = valid.reduce((m, s) => Math.max(m, s.completedAt ?? m), -Infinity);
|
|
31
33
|
const nBuckets = Math.max(1, Math.ceil((endTime - startTime) / bucketWidthMs));
|
|
@@ -65,5 +67,57 @@ export function computeLocalityAreaSeries(
|
|
|
65
67
|
|
|
66
68
|
const labels = [];
|
|
67
69
|
for (let b = 0; b < nBuckets; b++) labels.push(startTime + b * bucketWidthMs);
|
|
68
|
-
return { labels, series };
|
|
70
|
+
return { labels, series, endTime };
|
|
69
71
|
}
|
|
72
|
+
|
|
73
|
+
/** How many time buckets the Core Usage by Locality chart aims for across a run. */
|
|
74
|
+
export const LOCALITY_CHART_TARGET_BUCKETS = 60;
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
/** The Core Usage by Locality chart's points and its "busy at the peak" figure, shared by the
|
|
87
|
+
* dashboard widget and the CLI/MCP run summary. Buckets are at least a minute wide; a last bucket
|
|
88
|
+
* the stages only partly cover is rescaled to the covered part, and each bucket's idle cores are
|
|
89
|
+
* the peak minus its busy cores. */
|
|
90
|
+
export function buildLocalityChart(
|
|
91
|
+
stages ,
|
|
92
|
+
app ,
|
|
93
|
+
targetBuckets = LOCALITY_CHART_TARGET_BUCKETS,
|
|
94
|
+
) {
|
|
95
|
+
const hasActivity = stages.some((s) => (s.executorRunTime ?? 0) > 0 && (s.completedAt ?? 0) > (s.submittedAt ?? 0));
|
|
96
|
+
if (!hasActivity) return { hasActivity: false };
|
|
97
|
+
|
|
98
|
+
const start = app?.startTime ?? 0;
|
|
99
|
+
const end = app?.endTime ?? start;
|
|
100
|
+
const bucketWidthMs = Math.max(60_000, Math.ceil(Math.max(1, end - start) / targetBuckets));
|
|
101
|
+
const { labels, series, endTime: seriesEnd } = computeLocalityAreaSeries(stages, { bucketWidthMs });
|
|
102
|
+
const order = [...LOCALITY_TIERS.filter((t) => series[t]), ...(series.OTHER ? ['OTHER'] : []), 'idle'];
|
|
103
|
+
|
|
104
|
+
const points = labels.map((t, i) => {
|
|
105
|
+
const point = { t: Math.round((t - start) / 1000) };
|
|
106
|
+
// The series averages each bucket over its full width, so a last bucket
|
|
107
|
+
// that runs past the series' own end (or a run shorter than one bucket)
|
|
108
|
+
// reads diluted: a 10s stage in a 60s bucket showed well under its busy
|
|
109
|
+
// cores. Rescale to the part of the bucket the series actually covers.
|
|
110
|
+
const coveredMs = Math.min(bucketWidthMs, seriesEnd - t);
|
|
111
|
+
const scale = bucketWidthMs / coveredMs;
|
|
112
|
+
for (const tier of order) if (tier !== 'idle') point[tier] = (series[tier]?.[i] ?? 0) * scale;
|
|
113
|
+
return point;
|
|
114
|
+
});
|
|
115
|
+
const busyTiers = order.filter((t) => t !== 'idle');
|
|
116
|
+
const busyTotal = (p ) => busyTiers.reduce((sum, t) => sum + p[t], 0);
|
|
117
|
+
const peakCores = points.reduce((max, p) => Math.max(max, busyTotal(p)), 0);
|
|
118
|
+
// Same rule as the core series (peak busy minus each bucket's busy), redone
|
|
119
|
+
// on the rescaled values so the stack still tops out at the peak.
|
|
120
|
+
for (const p of points) p.idle = Math.max(0, peakCores - busyTotal(p));
|
|
121
|
+
return { hasActivity: true, order, points, peakCores };
|
|
122
|
+
}
|
|
123
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Type-level doc anchors, read off the detector catalog. Kept out of docs-config.ts so the docs
|
|
2
|
+
// URL helpers (used by every renderer) don't pull in the detectors themselves.
|
|
3
|
+
import { DETECTORS, ENTRY_BY_TYPE } from './detectors.js';
|
|
4
|
+
import { isKnownDocAnchor } from './docs-config.js';
|
|
5
|
+
import { getThresholdSummary } from './threshold-summary.js';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// DETECTORS is static, so this grouping is built once (lazily) instead of re-scanning per
|
|
9
|
+
// docAnchorForType call (called once per TagBadge per render). Keyed by emitted finding type, so
|
|
10
|
+
// broadcastSizing's anchor lands on underBroadcast/overBroadcast.
|
|
11
|
+
let anchorsByTypeCache ;
|
|
12
|
+
|
|
13
|
+
function anchorsByType() {
|
|
14
|
+
if (!anchorsByTypeCache) {
|
|
15
|
+
anchorsByTypeCache = new Map();
|
|
16
|
+
for (const entry of DETECTORS ) {
|
|
17
|
+
for (const type of entry.emits) {
|
|
18
|
+
const anchors = anchorsByTypeCache.get(type) ?? new Set();
|
|
19
|
+
anchors.add(entry.docAnchor);
|
|
20
|
+
anchorsByTypeCache.set(type, anchors);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return anchorsByTypeCache;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Resolves a finding `type` to its documented anchor from the DETECTORS entries that emit it.
|
|
28
|
+
* Returns undefined when entries sharing the type disagree on docAnchor (only configAudit today),
|
|
29
|
+
* or when the resolved anchor isn't in the allowlist (isKnownDocAnchor, the same gate DocsLink uses). */
|
|
30
|
+
export function docAnchorForType(type ) {
|
|
31
|
+
const anchors = anchorsByType().get(type) ?? new Set();
|
|
32
|
+
if (anchors.size !== 1) return undefined;
|
|
33
|
+
const [anchor] = anchors;
|
|
34
|
+
return anchor && isKnownDocAnchor(anchor) ? anchor : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What a renderer shows about one finding type without running its detector. */
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
/** Every emitted finding type's `DetectorInfo`, in `DETECTORS` declaration order of each entry's
|
|
50
|
+
* `emits` list (a type repeated across entries takes its first position, as `ENTRY_BY_TYPE` does).
|
|
51
|
+
* The key order is the widget order's tie-break, so it is part of the result. */
|
|
52
|
+
export function detectorInfoByType() {
|
|
53
|
+
const info = {};
|
|
54
|
+
for (const [type, { order, scope }] of ENTRY_BY_TYPE) {
|
|
55
|
+
info[type] = { order, docAnchor: docAnchorForType(type) ?? null, thresholdSummary: getThresholdSummary(type), scope };
|
|
56
|
+
}
|
|
57
|
+
return info;
|
|
58
|
+
}
|