# Yjs ⇄ KiCad sync review — overview and index Full-stack review (2026-07-02) of the collaborative-editing synchronization between the Y.Doc and the internal state of eeschema / pcbnew. Scope read end to end: - Shared Slot model: `web/pcbjam-shared/src/{kicad-doc,kicad-y,items-wire,kicad-delta,sexpr}.ts` - Runtime binding: `web/standalone/src/wasm/collab/{kicad-binding,sheet-manager,provider,drift-detect,index}.ts` - App wiring: `web/standalone/src/components/WasmTool.tsx` - C++ bridges: `wasm/bindings/{eeschema,pcbnew}_embind.cpp` - Tests: `web/standalone/src/wasm/collab/*.test.ts`, `tests/kicad/*-collab.spec.ts`, `tests/collab/browser-entry.ts` ## How the sync works (context for every finding) The production path is the v2 "items" wire (ysync 0008). C++ keeps a per-uuid scalar snapshot (`itemToJson`) as a diff baseline; a `BOARD_LISTENER` / `SCHEMATIC_LISTENER` treats commits as "something changed" triggers, and a post-settle `flushDiff` (CallAfter + COROUTINE) compares snapshots and emits each changed item as a full s-expr blob. The TS side re-flattens each blob into uuid-keyed items with `parent` links (`itemsWireToDelta`), diffs against the Y.Doc's `kdoc_items` map, and writes item-level updates (`applyDeltaToY`). Remote Y events go the other way: `deltaFromYEvents` → render the item's full subtree s-expr → `kicadCollabApplyItems`, which does an idempotent remove-by-uuid + re-add through a real commit. Seeding is seed-once: the first tab writes `fileToDoc(file)` into the empty room; joiners adopt the doc wholesale. Drift detection periodically compares a scratch save against `yToDoc` and reports (never repairs) divergence. The overall shape — post-settle diff, idempotent full-item upserts, echo suppression by origin tag plus C++ rebaseline — is sound and a good fit for the asyncify/wasm constraints. The findings below are the places where it breaks or leaks. ## Verdict / suggested order of attack **UPDATE 2026-07-03: bugs 01–07 are all FIXED** — see [17](17-fixes-bugs-01-07.md) for the fix record (done in the batch order below), two additional findings (F5: doc 16's F4 was an artifact of bug 01, file-seeded bodies are now editor-normalized; F6: the 0008 "asyncify-fragile envelope parse" was a layer-name bug, now fixed), and the remaining follow-ups. Every repro below now runs as a plain regression test. Misses 08–10 and opts 12–14 (beyond what the dirty-set emit already delivers) remain open. Every bug below has a runnable expected-fail reproduction test, and the v2 e2e port (miss 11) is DONE — see [16](16-repro-suite-results-and-empirical-findings.md) for the suite map and four empirical findings the running system added (notably: bug 03's sending half emits NOTHING, not the bare removal the doc predicted). 1. Fix [01](01-bug-first-tab-listener-never-registered.md) immediately (one line). 2. Then [02](02-bug-footprint-blob-zeroes-pad-nets.md) and [03](03-bug-child-removal-dangling-slot.md) (small, contained, data-corrupting). 3. Fold [04](04-bug-lossy-change-detection.md) and [05](05-bug-rebaseline-swallows-local-edits.md) into one change: blob-hash / dirty-set change detection plus targeted rebaseline. This also delivers most of [12](12-opt-hot-path-full-model-work.md). 4. [06](06-bug-concurrent-seed-duplicates-layout.md) and [07](07-bug-sheet-switch-stale-down-hook.md) are race windows — narrow them after the e2e port ([11](11-miss-no-v2-e2e-coverage.md)) gives regression cover. ## Index ### Bugs | # | File | One-liner | |---|------|-----------| | 01 | [01-bug-first-tab-listener-never-registered.md](01-bug-first-tab-listener-never-registered.md) | Fresh-room seeding tab never registers the C++ change listener → its edits never sync | | 02 | [02-bug-footprint-blob-zeroes-pad-nets.md](02-bug-footprint-blob-zeroes-pad-nets.md) | Footprint blobs strip pad net codes → net data loss propagates to peers, doc, and files | | 03 | [03-bug-child-removal-dangling-slot.md](03-bug-child-removal-dangling-slot.md) | Child-only deletion leaves a dangling `{item}` slot in the parent's Y body → renders throw | | 04 | [04-bug-lossy-change-detection.md](04-bug-lossy-change-detection.md) | Change detection is a lossy scalar projection → rotations, field text edits, pad edits never sync | | 05 | [05-bug-rebaseline-swallows-local-edits.md](05-bug-rebaseline-swallows-local-edits.md) | Global rebaseline after apply can silently drop concurrent local edits and receiver-side cleanup | | 06 | [06-bug-concurrent-seed-duplicates-layout.md](06-bug-concurrent-seed-duplicates-layout.md) | Two clients seeding an empty room concurrently duplicate `kdoc_layout` → corrupt materialization | | 07 | [07-bug-sheet-switch-stale-down-hook.md](07-bug-sheet-switch-stale-down-hook.md) | Sheet switch leaves `onItems` pointing at the old room → cross-room contamination window | ### Design misses | # | File | One-liner | |---|------|-----------| | 08 | [08-miss-layout-state-never-syncs.md](08-miss-layout-state-never-syncs.md) | Non-item state (title block, settings, `lib_symbols`) only syncs at seed | | 09 | [09-miss-undo-not-collab-aware.md](09-miss-undo-not-collab-aware.md) | Ctrl+Z reverts remote work and re-broadcasts it; adopt commits are undo bombs | | 10 | [10-miss-no-repair-path.md](10-miss-no-repair-path.md) | Drift is detected and reported but never repaired | | 11 | [11-miss-no-v2-e2e-coverage.md](11-miss-no-v2-e2e-coverage.md) | Two-tab e2e exercises the legacy wire; the production v2 stack has no end-to-end test | ### Optimizations | # | File | One-liner | |---|------|-----------| | 12 | [12-opt-hot-path-full-model-work.md](12-opt-hot-path-full-model-work.md) | O(full-model) work (incl. zod) on every edit/apply/remote batch | | 13 | [13-opt-parked-dirty-full-sheet-replace.md](13-opt-parked-dirty-full-sheet-replace.md) | Parked-dirty sheet rebind re-applies the whole sheet instead of the delta | | 14 | [14-opt-item-granularity-bandwidth.md](14-opt-item-granularity-bandwidth.md) | Item-level body granularity ships the whole item per nudge; LWW drops concurrent property edits | ### Plan & results | # | File | One-liner | |---|------|-----------| | 15 | [15-plan-repro-tests-and-v2-e2e.md](15-plan-repro-tests-and-v2-e2e.md) | The approved plan: repro tests for bugs 01–07 + the v2 e2e port | | 16 | [16-repro-suite-results-and-empirical-findings.md](16-repro-suite-results-and-empirical-findings.md) | Plan 15 executed (2026-07-03): suite map, verified failure sites, empirical findings F1–F4 | | 17 | [17-fixes-bugs-01-07.md](17-fixes-bugs-01-07.md) | Bugs 01–07 fixed & verified (2026-07-03); findings F5–F6; remaining follow-ups | | 18 | [18-miss08-opts-12-13.md](18-miss08-opts-12-13.md) | Miss 08 (lib_symbols + layout save-sync) + opts 12/13 implemented; opt 14 deferred (2026-07-03) | | 19 | [19-undo-option1-feasibility.md](19-undo-option1-feasibility.md) | Miss-09 option 1 (local-ops-only undo) feasibility research (2026-07-07): ~3–5 days, eeschema UUID-guard port is the core | | 20 | [20-fix-miss09-collab-aware-undo.md](20-fix-miss09-collab-aware-undo.md) | Miss 09 FIXED (2026-07-07): SKIP_UNDO remote applies + eeschema UUID guard + binding-owned removed-item lifetime; 5/5 e2e |