The Ghost Wire Bug: A Case Study in SVG DOM and Snapshot Corruption
Some bugs are embarrassing. This one was fascinating.
In TAFNE's Schema Editor, the SVG canvas for drawing wiring diagrams, wires are interactive. You can draw them, click them, delete them, and undo everything with Ctrl+Z. Standard stuff.
But after a few weeks of building out the history system, we ran into something strange. Deleted wires kept coming back.
Not visually. Visually, the wire was gone the moment you pressed Delete. But something remained in the DOM. An invisible element. Something with the wire's ID, sitting in the document, doing nothing. Except it was breaking everything.
We called it the zombie wire.
What Was Happening
To make wires clickable, the geometry engine wraps each SVG path in a structure that includes a hitbox, a transparent, wider version of the path that gives users a larger click target. Standard SVG interaction pattern.
The hitbox was generated alongside the wire. It was given the same ID as the wire itself, with a -hit suffix. The problem came from the history system.
TAFNE's undo/redo stores DOM snapshots. When you make a change, draw a wire, delete a component, it captures the current canvas state as a serialized snapshot and pushes it onto the history stack. When you undo, it restores that snapshot.
The snapshot logic used IDs to track elements. When it restored a snapshot, it matched elements in the saved state to elements in the live DOM by their IDs. But the hitbox had its own ID too. A different ID from the wire's visual path, but an ID nonetheless.
Here's where it broke.
When a wire got deleted, the hitbox element wasn't always removed cleanly from the DOM. In some sequences of operations, delete, then add a new wire, then undo, the hitbox from the first wire would persist as an orphaned element. It was invisible. It had no visual representation. But it had an ID. And that ID was now colliding with whatever the history system was trying to restore.
When the snapshot restorer found that ID in the DOM, it thought the element it was trying to restore was already there. So it didn't restore the visual path, it found the invisible hitbox instead and tried to update that. The wire appeared to disappear on undo, because the thing getting updated was a transparent element nobody could see.
We had a ghost. A wire-shaped hole in the undo history, filled by an invisible hitbox squatting on the wrong ID.
What Made It Hard to Find
The bug only manifested under a specific sequence: delete a wire, draw a new one with the same type, then undo twice. Under simpler sequences, the hitbox cleanup was fast enough that the collision didn't occur.
The symptom was also misleading. "Undo doesn't work for wires" is a description that points toward the history system. We spent time auditing the snapshot push/pop logic before we thought to inspect what was actually in the DOM.
When we finally ran the failing sequence with DevTools open and watched the element inspector in real time, the ghost appeared. The visual path was gone. The hitbox was still there, sitting in the SVG tree, invisible and occupying an ID it had no right to.

The Fix
The fix ended up being straightforward once we understood the problem. Two parts.
First: remove IDs from all hitboxes at generation time.
// geometryEngine.js, before
hitbox.setAttribute('id', ${wire.id}-hit);
// After // ID intentionally omitted, hitboxes must not participate in ID-based DOM operations
Hitboxes don't need IDs. Nothing in the codebase needed to look them up by ID. The ID was an artifact of how the geometry engine generated elements, it gave everything an ID by default, which was the wrong default for this use case.
Second: make the snapshot restorer explicitly purge hitboxes before rebuilding state.
// canvasEngine.js, _restoreFullState()
document.querySelectorAll('.wire-hitbox').forEach(el => el.remove());
// Now restore the snapshot, clean slate, no ghost elements
When undo fires, the restorer clears all hitboxes first. Then it restores the snapshot. Then the geometry engine regenerates hitboxes from the restored wires. Every undo starts from a clean DOM, not a DOM that might have orphaned elements from a previous state.
What Changed in the Broader Codebase
This bug prompted a few related changes:
The wire path promotion logic in _runGeometryPipeline was refactored to use CSS selectors (:not(.wire-hitbox)) when promoting visual paths. The old code selected by element type, which could match hitboxes in edge cases. The selector is now explicit about what it's looking for.
We also audited every place in the codebase where elements were given IDs to confirm that hitboxes were the only category that shouldn't have them. They were.
The Broader Lesson
The bug came from an implicit assumption: that IDs were safe to assign liberally as long as they were unique at creation time. The problem is that uniqueness at creation time isn't the same as uniqueness across the full lifecycle of an element, especially in a system with history snapshots.
Elements that need to be invisible and non-participating in DOM operations shouldn't have IDs. If they do, they become candidates for accidental collision with anything that uses IDs to find or track elements.
The more specific lesson: when a history system uses IDs as identity keys, every element that has an ID becomes part of the history contract, whether you intended it to or not. Hitboxes were never meant to be part of that contract. Making the exclusion explicit, both at creation and at restore time, is what finally killed the zombie.
The fix is about 15 lines across two files. The understanding took considerably longer.
TAFNE Schema Editor is open source: github.com/carnworkstudios/TAFNE