Appearance
TypeScript core and runtime event validation
How the shared TypeScript core types and validates the events it reads.
All of src/ (browser SPA) and packages/core/src/ (shared analysis logic) is strict TypeScript, except for a few plain-.js files: the two vendored third-party decompressors, packages/core/src/vendor/fflate.js and packages/core/src/vendor/fzstd.js (left untouched deliberately: vendored code, not project code), load-vendored.js (copied byte-for-byte into vendor-core/, so it must run without TS stripping), and the SHS helpers shs-request.js and proxy.js, which the local server imports as plain JS. Every remaining import of a same-repo .ts module uses a .ts specifier (e.g. import { dispatchLine } from './event-handlers.ts'), not .js: the CLI and MCP entrypoints (packages/cli/bin/sparkforensics-analyze.mjs, packages/mcp/bin/sparkforensics-mcp.mjs) run under plain Node's ESM resolver, which cannot remap a .js specifier to a same-named .ts file the way Vite/Vitest's bundler-style resolver can. tsconfig.json sets "allowImportingTsExtensions": true to make this legal.
Event schema validation lives in packages/core/src/event-schemas.ts: one zod schema per SparkListener* event variant the parser understands (17 total: LogStart, ApplicationStart, EnvironmentUpdate, ApplicationEnd, JobStart, JobEnd, StageSubmitted, StageCompleted, StageExecutorMetrics, TaskEnd, the four SQL-execution-UI listener events, ExecutorAdded, ExecutorRemoved, BlockUpdated), combined into SparkEventSchema = z.discriminatedUnion('Event', [...]). processEvent's switch (event-handlers.ts) consumes the resulting SparkEvent union type directly, so a schema change and a handler's expectations can't silently drift apart.
The recursive sparkPlanInfo.children tree carried on SQL-execution-start events is validated by parseSparkPlanInfoTree, an iterative, explicit heap-allocated-stack parser, deliberately not z.lazy(). A real plan tree's depth is unbounded, and attacker-uncontrolled input should never hand zod's own recursive schema resolution an arbitrarily deep structure to walk. The iterative parser shallow-validates one node at a time via z.object(...) and builds the tree itself, capping at MAX_PLAN_DEPTH = 500 (throws past that). This mirrors packages/core/src/plan-tree-walk.ts's walkPlanTree, which uses the same iterative-over-recursive approach for the resolved tree; event-schemas.ts applies it one layer earlier, to the raw JSON before it becomes a tree at all.
There are two external-data boundaries, the two places this codebase parses data it does not control. Both run that data through a schema, and both treat a validation failure the same way: a silent skip, not a distinct error.
dispatchLine(event-handlers.ts): afterJSON.parsesucceeds, a line whoseEventvalue is one of the 17 modeled types but fails that type's own schema incrementsskippedLines. AnEventvalue outside the 17 modeled types is silently ignored without incrementingskippedLines: real Spark logs always carry plenty of ordinary event types this tool does not model (TaskStart,BlockManagerAdded,ExecutorMetricsUpdate, and others), and counting them would tripevidence-availability.ts's fail-closedtrustworthygate on healthy logs. One exception: an AQE update that a later update for the same open execution supersedes is never parsed (deferAdaptiveUpdate, see the detector contract), so a malformed superseded update is not counted.shs-fetch.ts: on a non-OK response from the local SHS proxy, the JSON error envelope is validated againstShsProxyErrorBodySchema(packages/core/src/shs-schemas.ts,{ code: string },.passthrough()). A body that isn't valid JSON, or is JSON but fails that schema, falls back to the genericaccess-or-upstream-failurecode: the same silent-skip treatment as a malformed log line. There is no separate "malformed SHS response" error code.
Neither boundary distinguishes "malformed JSON" from "wrong shape" from "unrecognized variant" in what it reports outward: all three collapse into the same skip/fallback path. That is a deliberate scope decision.
The exhaustiveness convention is packages/core/src/assert-never.ts. assertNever(x: never): never throws at runtime and, more importantly, fails tsc at compile time if x is not actually never, i.e. if some case of a union type isn't handled. Used at the default arm of event-handlers.ts's processEvent switch (over SparkEvent) and analyzer.ts's scope-dispatch switch (over a detector's scope: 'stage' | 'sql' | 'app' | 'config'). It is the project's standard pattern for any future exhaustive switch or dispatch over a closed union: add default: return assertNever(x); (or the closest non-returning equivalent) so that adding a new union member without updating every consumer becomes a compile error instead of a silent runtime gap.