# Bug 04 — Change detection is a lossy scalar projection; a whole class of edits silently never syncs **Severity:** high (broad, silent non-replication of everyday edits) **Status:** FIXED 2026-07-03 — see [17](17-fixes-bugs-01-07.md) (batch 3: listener dirty-set → unconditional v2 blob emit) ## Where - `wasm/bindings/eeschema_embind.cpp:121-180` — `itemToJson` (the diff key), `:286-297` `snapshotItems` iterating `screen->Items()` only - `wasm/bindings/pcbnew_embind.cpp:144-169` — `forEachTopItem` (what is iterated), `:176-252` — `itemToJson` - `flushDiff` in both files — the v2 blob emit fires **only** for uuids whose scalar json differs from the baseline ## The structural problem The v2 items wire carries lossless per-item s-expr blobs, but its **trigger** is still the legacy scalar snapshot diff. `itemToJson` projects each item to a handful of fields (id/type/x/y/layer + a few per-type extras). Any edit that doesn't change the projection produces an empty diff → **no emit on either wire**, even though the listener fired. Drift-detect eventually *reports* the divergence; nothing repairs it ([10-miss-no-repair-path.md](10-miss-no-repair-path.md)). ## Known-missed edits eeschema: - **Symbol rotate / mirror** — `GetPosition()` unchanged → invisible. - **Reference / value / any field text edit** — fields are not in `screen->Items()` (they live inside the symbol), and the symbol's json carries no field text (`SCH_SYMBOL` is not an `EDA_TEXT`) → invisible. This is arguably the most common schematic edit after moving things. - **Label / text rotation** — spins about the anchor; position unchanged → invisible. - Stroke color and similar cosmetic properties not in the projection. pcbnew: - **Pad property edits** (size, shape, drill, net via pad dialog) — pads are deliberately not visited by `forEachTopItem` → invisible. - **Zone properties** (net, hatch, priority, fill settings) — only `Outline(0)` points are compared → invisible. Holes / additional outlines are also outside the projection. - **Graphic shape endpoint drags** — `Drawings` items' json is position-only. Dragging the *end* point of a segment leaves `GetPosition()` (the start) unchanged → invisible. (When the *start* moves, the change IS detected and the v2 blob replace is correct; the legacy `SetPosition` semantics would have translated instead of reshaping, but the legacy wire is dead in production.) - **Footprint rotation** syncs only *by accident*: the field children's absolute positions move, which lifts the parent blob. A footprint whose fields sit exactly on the rotation anchor would not sync its rotation. ## Why fixing this properly is cheap Two ingredients already exist: 1. The listener callbacks receive the **touched-item vectors** (`OnSchItemsChanged(…, std::vector&)`, `OnBoardItemsChanged(…)`, `OnBoardCompositeUpdate(…)`) — currently ignored ("the listener is just a trigger"). Collect the uuids into a dirty set at trigger time. 2. The blob serializer (`itemBlob` / `blobForItem`) is the lossless comparison unit. Post-settle, instead of diffing the full scalar snapshot, for each **dirty root** (child uuids lifted to their parent, as `liftBlob` already does) compare the current blob — or a hash of it — against the last-emitted blob hash, and emit on mismatch. This: - catches every serializer-visible property (rotation, field text, pad edits, zone settings) by construction; - keeps the post-settle convergence property (the blob is taken after cleanup); - shrinks the per-edit cost from O(all items) to O(dirty items) — the main lever of [12-opt-hot-path-full-model-work.md](12-opt-hot-path-full-model-work.md); - lets the scalar snapshot/baseline machinery (and the legacy wire emit) retire. Removals still need the baseline uuid set (a disappeared uuid can't be blobbed); keep a uuid→(parent, blob-hash) map as the baseline instead of uuid→json. Interaction with [05-bug-rebaseline-swallows-local-edits.md](05-bug-rebaseline-swallows-local-edits.md): moving the baseline to uuid→hash makes the targeted post-apply rebaseline natural — update hashes only for the uuids the apply touched. ## Verification Per-tool e2e matrix of the missed edit list above (rotate symbol, edit value text, edit pad size, drag shape endpoint, change zone net), asserting the peer converges and drift-detect stays quiet. Repro matrix (2026-07-03, each `test.fail` — see [16](16-repro-suite-results-and-empirical-findings.md)): `tests/kicad/ysync-repros-pcbnew.spec.ts` (anchor-centred footprint rotation — a second fixture footprint with every child ON the anchor, so the "syncs by accident" escape hatch is closed; pad resize; gr_line endpoint drag) and `tests/kicad/ysync-repros-eeschema.spec.ts` (symbol rotation, Value-field edit). Each case proves the edit LANDED (save poll) before expecting the emit; runtime-confirmed: every one lands and none emits. Zone-net is the one matrix row without a hook yet.