Read README.md first for the architecture. Then this file. The web app has its own
deep-dive in web/README.md.
The previous site generations (v2 and v3) are documented in docs/legacy.md.
The v3 Rust tree (server/, migrate/, ops/, the Cargo workspace) has been deleted from the
working tree; recover it from git history if ever needed. Do not resurrect it piecemeal — the
rules below are the parts that were worth keeping.
SCHEMA_VERSIONis a coordinated cross-repo change.web/lib/schema-version.tsis the in-repo source of truth; the monorepo'sscripts/post-ingest.pyhardcodes the same integer, and itsvortex-bench/src/v3.rsowns the producer wire shape. Bump in lockstep across both repos. The versioned contract lives inCONTRACT.md, andweb/lib/schema-version.test.tsasserts the in-repo anchors agree.measurement_idis internal — never on the wire. It is a deterministic xxhash64 overcommit_shaplus the dim tuple, computed by the monorepo'sscripts/_measurement_id.pyjust before INSERT and used as theON CONFLICTupsert key. Its output is frozen forever: the monorepo's golden-vector test (scripts/tests/test_measurement_id.py) pins it, because a drifted hash would insert duplicates next to the rows already in production.- Numeric
?n=is clamped to 1000;?n=allis the uncapped escape hatch. Pages hydrate from the materialized latest-100 shard artifact by default;?n=allis an explicit opt-in (the chart's full-history zoom-out hop uses it once, and curl power users can request it). The numeric path is bounded byMAX_NUMERIC_COMMIT_WINDOWinweb/lib/window.tsas a DoS-protection floor against?n=99999999. Do NOT raise the numeric cap or remove it without thinking about the DoS surface, and do NOT re-introduce a server-side commit cap on?n=all— visual downsampling happens client-side via LTTB on the visible commit range only. - Don't refetch on every scope change. Once a chart's payload is in memory,
pan/zoom/slider/range-strip all rebuild in place via the in-memory LTTB pass on the cached
payload (
web/components/Chart.tsx). The single exception is the latest-100 to full-history zoom-out path: when the user first zooms past the materialized window, the chart lazy-fetches?n=allonce and replaces the payload in place. - Don't write a server-side classifier for live ingest. The emitter produces structured records directly. Classifying loose name strings at read time was the v2-era weakness every later generation existed to escape; it belongs nowhere in the live pipeline.
All in web/components/Chart.tsx:
- Reverse predecessor walk in the tooltip.
payload.commits[]is sorted oldest-first by SQL -commits[0]is the oldest,commits[N-1]is the newest. For per-row delta the predecessor ofcommits[idx]is atidx - 1. We caught a regression where a "fix" flipped this toidx + 1; the original walk-backward direction is right. pointer-events: autoon the tooltip host. The tooltip is positioned at the cursor; making it pointer-interactive causes a flicker loop. Keep itpointer-events: noneand offset viatransform: translate(12px, 12px).changeevents on the slider. Useinputevents with a small throttle;changeonly fires on release and feels broken.
# The web app needs BENCH_DB_* env vars for a real database — see web/README.md.
cd web && pnpm install && pnpm dev
pnpm test # vitest unit tests
pnpm build # deliberately works without a database, so CI needs no secretsFor the full env-var contract see web/README.md.