Three processes on three lanes, events as dots, messages as arrows between them. Every event gets stamped by all three clocks at once, and the toggle decides which stamp you see: a wall-clock timestamp, a Lamport scalar, or the full vector.
The rules are the textbook ones, and they fit in four lines:
local / send : V[self] += 1
receive : V[self] += 1, then V = componentwise max(V, message)
a → b iff V(a) ≤ V(b) componentwise and V(a) ≠ V(b)
That last line is an iff, in both directions, and everything here is built
to make the second direction matter. A Lamport scalar keeps only the forward
half — a → b still implies L(a) < L(b) — so it can order every pair and
prove none of them. Switch to it on the crosstalk history and the readout
says the quiet part: an opinion about all 78 pairs, and proof for 42.
The question at the bottom
Every history writes to one key, and the strip in the corner answers “what
is cart?” three times, once per clock. It is the same question a replica
answers on every read, and the three answers are not the same answer:
- wall returns one value, because a timestamp is a total order.
- lamport returns one value, for the same reason — and on the default history it is the other value. Two scalars, two confident winners, one discarded write each, and they discard different ones.
- vector returns the writes nothing else dominates. When that set has two elements it says so: siblings, unresolved, the application’s problem. This is Dynamo’s answer, and it is the only honest one, because there is nothing in the history that ranks those two writes.
The struck-through value in each row is a write that existed, was acknowledged, and is gone. Last-write-wins is not a tie-break; it is a silent delete that looks like success.
What the controls are for
Hover any event and the picture splits into its light cone — what precedes it in teal, what follows in violet, and in amber the events that are neither, which is the set this process cannot know about and cannot be known by. Click two events for the verdict. When they are ordered, the witness is drawn: the actual chain of program steps and messages carrying one to the other, in white, because “before” is a path and not a number comparison. When they are concurrent, the two coordinates that prove it get named — A is ahead on P0, B is ahead on P1 — along with the orders the other two clocks invent anyway.
Wall mode is the one you should drag. Each lane can be pulled sideways,
which skews that process’s clock and changes nothing else about the history.
Pull it far enough and messages start arriving before they were sent, drawn
in red and labelled as such, while the timestamps stay perfectly, uselessly
ordered — and the cart winner flips, because on the default history the two
writes are one millisecond apart and the survivor is decided by whose clock
runs fast.
The readout carries the bill: 8 bytes per message for a scalar, 8×n for a
vector. That linear term is the entire argument against vector clocks in
production, and the reason real systems prune entries, cap them per replica,
or move to dotted version vectors. The handoff history is the other side of
it — a token relayed around the ring, zero concurrent pairs out of 120, all
three clocks in agreement, and 40 bytes a message buying you nothing at all.
Reuse
src/precede.js is framework-free and has no canvas in it. simulate(scn)
runs a history and stamps every event; relation(a, b), causalPath, cone
and census ask questions of the result; registerVerdicts is the
last-write-wins-versus-siblings comparison on its own. The four histories in
SCENARIOS are generators of n, so any of them can be rebuilt at 3 to 5
processes.
node scripts/screenshot-demo.mjs [out] [--shot=wall] re-shoots the
thumbnail and the wall-clock frame in media/.