New doc 16: the full repro-suite map (per-bug unit/e2e paths with verified failure sites), the phase-C probe outcome, and four findings only the RUNNING system revealed: - F1: bug 03's sending half emits NOTHING — a child-only delete commit never triggers a flush at all (worse than the doc's predicted bare removed-wire); the GetWidth-assert tracer evidence and the fix implication. - F2: Firefox cannot host two kicad_editor tabs in one context (per-process wasm budget) — bug-01 two-tab repros are Chromium-only. - F3: headless emit WORKS on both pcbnew and eeschema — the legacy two-tab skip rationale and items-bridge localEdit omissions are stale. - F4: drift-detect is strictly ITEM-silent on the green path (no writer- formatting false positives). Cross-updates: 00 index + verdict note; 01/04/05/06/07 Verification sections gain their repro paths; 02 upgraded to runtime-CONFIRMED; 03 gains the F1 empirical correction; 11 (no v2 e2e coverage) CLOSED with a point-by-point status update — only the legacy retirement remains. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPfrVhfYgPPgtawjSssZfn
4.1 KiB
Bug 05 — Global rebaseline after a remote apply can swallow concurrent local edits and receiver-side cleanup
Severity: medium-high (silent permanent peer divergence; probability scales with edit rate × remote traffic) Status: open
Where
wasm/bindings/eeschema_embind.cpp:726-731(doApply) and:846-849(doApplyItems) —rebaseline()at the end of every applywasm/bindings/pcbnew_embind.cpp:811-816,:889-894— samerebaseline()itself: snapshots the entire current model intog_baseline
The race
Applies and flushes are both CallAfter pending events on the same frame handler, so
they are FIFO. This ordering is realistic:
- A remote batch arrives; JS calls
kicadCollabApplyItems→ apply queued. - Before the pending queue drains, the user's in-flight edit commits (e.g. mouse-up of
a drag processed in the same frame).
SCH/BOARD_COMMIT::Pushfires the listener →scheduleFlush→ flush queued behind the apply. - Pending drain: apply runs first.
doApplyItemsapplies the remote items, thenrebaseline()snapshots the whole model — which already contains the user's local edit. - flush runs: diff(current, baseline) is empty. The local edit is never broadcast.
The edit stays in the local editor but never reaches the Y.Doc or peers — permanent, silent divergence. Drift-detect will eventually report it; nothing repairs it (10-miss-no-repair-path.md).
Second casualty: receiver-side cleanup
The convergence argument for the post-settle diff is "the sender broadcasts its FINAL,
post-cleanup geometry; the receiver re-cleaning already-clean geometry is idempotent."
That holds when the receiver's surroundings are identical. When they are not —
concurrent local geometry, e.g. a remotely-moved wire now crossing the receiver's
junction — the receiver's RecalculateConnections produces genuinely new state
(splits/merges the sender never saw). Because rebaseline() runs after Push, that
cleanup is folded into the baseline and never broadcast: the receiver has split
wires, the sender doesn't, and no future diff will notice.
Fix
Make the post-apply rebaseline targeted instead of global: update g_baseline
entries only for the uuids the apply actually added/changed/removed (recompute their
post-apply json/blob-hash; drop removed ones). Everything else the apply's Push
mutated as a side effect — receiver-side cleanup, and any concurrently committed local
edit — then still differs from the baseline and flushes as a normal local diff:
- the swallowed-local-edit race disappears (the edit's uuids weren't touched by the apply, so their baseline entries are still pre-edit);
- receiver-side cleanup is broadcast and both sides converge on it. Re-application on the original sender is idempotent, so the echo is bounded (one extra hop, then empty diffs).
This composes with the blob-hash baseline proposed in 04-bug-lossy-change-detection.md: with a uuid→hash baseline, "targeted" is just updating the hashes for the applied uuids.
Verification
Deterministic interleave test via the existing test hooks: queue an apply
(kicadCollabApplyItems) and a local kicadCollabTestMoveFirst such that the commit
lands between apply-enqueue and apply-run; assert the local move still reaches the
peer. For the cleanup half: two tabs, tab B draws a wire crossing where tab A is about
to move a wire; after A's move syncs, assert both tabs converge on the same segment
set (currently B's split stays local).
Repro (2026-07-03): implemented exactly as the first half above in
tests/kicad/ysync-repros-pcbnew.spec.ts ("a local edit committed while a remote
apply is queued…", test.fail): one JS turn queues TestMoveFirst then
ApplyItems(added footprint) → FIFO drain [move, apply, flush] → runtime-confirmed
that both land and the moved uuid is never emitted. The receiver-side-cleanup half
remains un-reproduced (needs the two-tab wire-crossing setup). See
16.