Compose two concurrent transactions, step through them, and watch the anomaly your isolation level allows.
Point it at a schedule and it runs the thing itself. The version chains, the snapshot bounds, the dependency edges and the cycle are all read out of a working MVCC engine, one operation at a time, so a claim on screen is the engine's answer rather than an illustration of one.
The part worth checking is that the engine is not tuned to agree with the picture. Every anomaly in Martin Kleppmann's Hermitage suite is a golden test, all seventy cells of his published matrix are reproduced, and each expectation was also driven against a real PostgreSQL 18.4 in Docker with concurrent psql sessions as a second, independent oracle.
- Build a schedule, load one of the fifteen Hermitage scenarios, or paste SQL and let the engine parse it.
- The engine executes it operation by operation against an MVCC store with real snapshots and first-committer-wins SSI.
- Every step returns the full state: versions, per transaction visibility, dependency edges, and any cycle in them.
- The three panels render that one step, so the timeline, the version chains and the graph cannot disagree with each other.
- Change the level or reorder an operation and the whole thing re-runs.
The same schedule, one setting apart. Write skew commits at repeatable read and leaves the database inconsistent. At serializable the engine detects the pivot and aborts it with the error PostgreSQL actually raises.
Every cell of the published matrix, computed live. Seven engine and level combinations against ten anomalies, run on demand rather than transcribed. A cell that disagrees with the published table is marked in red instead of hidden, and every cell opens the schedule that produced it.
The labels disagree with the behaviour, and it shows. PostgreSQL's repeatable read is snapshot isolation. MySQL's is weaker again and loses an update where PostgreSQL raises a serialization error. Running one schedule against different engine profiles puts that contradiction side by side.
Anomalies are encoded by form, not by colour. Cycle edges thicken and gain a dash animation, a pivot node is ringed in a dash, and versions a transaction cannot see are hatched. All of it survives greyscale, and the three transaction colours were measured at 64.2 CIE76 separation under protanopia rather than picked by eye.
Reorder an operation and watch the cycle appear or vanish. The schedule is editable in place, and it round trips through the URL so a composed one can be shared.
An article that argues with a working engine underneath it. Six sections, each carrying a live figure the reader drives. Nothing is bound to scroll position: every figure owns its own schedule and step index, and the control is always a button the reader presses.
| Layer | Choice |
|---|---|
| Engine | Python 3.13. MVCC store, isolation levels as data, dependency graph, anomaly detection |
| Engine API | FastAPI on uvicorn. Runs a schedule and returns every step |
| SQL | sqlglot, parsing the supported subset into operations |
| Frontend | Next.js 16 on React 19, TypeScript strict |
| Components | Base UI primitives, styled locally |
| Graph layout | dagre |
| Version chains | TanStack Table |
| Icons | lucide-react |
| Checks | pytest and hypothesis on the engine, Vitest, Playwright and axe on the UI |
The timeline and the graph are hand written SVG, and the layout is not. React Flow draws edges only between nodes it has measured, and a custom node type never reports its dimensions on React 19.2, so the nodes stay hidden and the edge layer comes up empty. Every node here carries a transaction hue and a cycle ring, so a custom type is not optional. Rather than pin React back, the graph draws its own edges. dagre still owns every coordinate, because a layout algorithm is exactly the kind of thing that should not be hand rolled.
Write-write conflicts block rather than error. A blocked transaction queues and its later statements wait, which is what a real engine does and what every Hermitage schedule assumes. Without it the published schedules cannot be replayed at all.
The panels are built so they cannot disagree. Nothing here proves by test that the version table matches the graph. Instead every panel takes the same step object, holds no index of its own and never refetches, so disagreement is not a bug that can occur.
Dark mode keeps the transaction fills fixed. The three step 9 tokens are identical in both themes, so a transaction never changes hue. Only the ground, the text ramp and the cycle halo flip, because a dark halo on a dark ground is invisible.
- Python 3.13 and uv
- Node 20 or newer
Start the engine:
cd engine
uv sync
uv run uvicorn isolate.api:app --port 8000
Then the frontend, in a second terminal:
cd frontend
npm install
npm run dev
The frontend reads NEXT_PUBLIC_ENGINE_URL and falls back to http://127.0.0.1:8000. Set it in
frontend/.env.local if the engine runs anywhere else.
Keep the frontend on port 3000. The engine's CORS allowlist names that origin, so a frontend served anywhere else is blocked in the browser while every request still succeeds from the terminal.
engine/ MVCC store, isolation levels, dependency graph, anomaly detection, FastAPI
frontend/ The article, the workbench, the three panels, the matrix
Engine, 207 tests including the Hermitage goldens and hypothesis generated schedules:
cd engine
uv run pytest
Frontend, 51 unit and component tests:
cd frontend
npm run typecheck
npm run lint
npm run test
Then the browser suite, 98 checks against a running engine. The audit walks every route at 1440 and 375 and fails on nested containers, offscreen elements, hover styles on things that cannot be clicked, missing pointer cursors, sideways scroll, axe violations, and computed WCAG contrast in both themes. The rest drive the controls with a real pointer, because a control that renders correctly and does nothing looks identical in a screenshot:
npm run test:e2e
![The workbench showing Hermitage's G2-item schedule at repeatable read. Ten operations listed left, T1 in indigo and T2 in green. The timeline has both transactions committed at step 10 of 10. The version table shows key 1 dead at value 10 and live at 11, key 2 dead at 20 and live at 21, with snapshot xmin 1, xmax 3, xip [1, 2]. The dependency graph draws two dashed rw edges between T1 and T2 forming a cycle, labelled G2-item. Final state 1 => 11, 2 => 21.](/baxsm/isolate/raw/main/frontend/public/readme/workbench.png)


