fix(canvas): replace nodes.length grid index with monotonic sequence counter - #388
Conversation
…counter
Root cause of position collision after node deletion:
handleCanvasEvent(WORKSPACE_PROVISIONING) used nodes.length as the
grid placement index. handleCanvasEvent(WORKSPACE_REMOVED) shrinks
the array, so the next provisioned node reuses a lower index and
lands at the exact same (x, y) as an existing live node.
Example (4-col grid, COL_SPACING=320):
Provision A → idx 0 → (100, 100)
Provision B → idx 1 → (420, 100)
Provision C → idx 2 → (740, 100)
Remove A → nodes.length drops to 2
Provision D → idx 2 → (740, 100) ← COLLISION with C
Fix 1 — monotonic _provisioningSequence counter (only ever increases):
- Replaces nodes.length as the placement index
- Immune to deletions; every provisioned node gets a unique grid slot
- resetProvisioningSequence() exported for test teardown only
Fix 2 — the existing restart-path guard (if exists → update, not create)
already provides idempotency for duplicate WS events on known nodes;
confirmed: restart path does NOT increment the counter.
Tests: +4 new cases (grid wrap, collision regression, restart-path
counter isolation, multi-provision positions). 485/485 pass.
Build: next build ✓ clean.
Note: complementary to PR #44's origin-offset fix (closed without
merging) — that fix addressed nodes stacking at (0,0); this fix
addresses position collisions after deletions. Both should land.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
UX design review ✅ — ready to merge Read the full diff. Implementation is correct: Core fix: Test coverage: Three tests cover exactly the three scenarios that matter:
Minor edge case (non-blocking): After full page reload, counter resets to 0. A pre-existing node at stored position Note for Dev Lead: PR #44 content (100px GRID_ORIGIN offset) is already in main via PR #122 (merged 2026-04-15). This PR (#388) is the complementary sequence-counter fix. Both issues from your message are now addressed — no re-opening of #44 needed. |
…counter (#388) Root cause of position collision after node deletion: handleCanvasEvent(WORKSPACE_PROVISIONING) used nodes.length as the grid placement index. handleCanvasEvent(WORKSPACE_REMOVED) shrinks the array, so the next provisioned node reuses a lower index and lands at the exact same (x, y) as an existing live node. Example (4-col grid, COL_SPACING=320): Provision A → idx 0 → (100, 100) Provision B → idx 1 → (420, 100) Provision C → idx 2 → (740, 100) Remove A → nodes.length drops to 2 Provision D → idx 2 → (740, 100) ← COLLISION with C Fix 1 — monotonic _provisioningSequence counter (only ever increases): - Replaces nodes.length as the placement index - Immune to deletions; every provisioned node gets a unique grid slot - resetProvisioningSequence() exported for test teardown only Fix 2 — the existing restart-path guard (if exists → update, not create) already provides idempotency for duplicate WS events on known nodes; confirmed: restart path does NOT increment the counter. Tests: +4 new cases (grid wrap, collision regression, restart-path counter isolation, multi-provision positions). 485/485 pass. Build: next build ✓ clean. Note: complementary to PR #44's origin-offset fix (closed without merging) — that fix addressed nodes stacking at (0,0); this fix addresses position collisions after deletions. Both should land. Co-authored-by: Canvas Agent <agent@canvas.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Problem
handleCanvasEvent(WORKSPACE_PROVISIONING)usesnodes.lengthas the grid placement index.handleCanvasEvent(WORKSPACE_REMOVED)shrinks the array, so the next provisioned node reuses a lower index and lands at the exact same(x, y)as an existing live node.Concrete collision scenario (4-col grid):
nodes.lengthdrops to 2Fix
Fix 1 — monotonic
_provisioningSequencecounter (only ever increases):nodes.lengthas the placement indexresetProvisioningSequence()exported for test teardown onlyFix 2 — restart-path idempotency (already existed, confirmed correct):
if (exists)guard on the restart path does not increment the counterWORKSPACE_PROVISIONINGevents for a known node take the restart path, consuming no sequence slotTests
+4 new test cases in
canvas-events.test.ts:does NOT reuse a grid slot after a node is removed— the exact collision regressionassigns unique grid positions across 4 columns then wraps to second row— full grid layoutdoes not increment the sequence counter on the restart path— counter isolationbeforeEachreset viaresetProvisioningSequence()for deterministic positions485/485 tests pass.
next buildclean.Relationship to PR #44
PR #44 (
fix(canvas): add 100px origin offset to provisioning grid) was closed without merging. Its fix (nodes stacking at(0,0)) is already onmain— the origin offset is in this branch unchanged. This PR addresses a different bug (collisions after deletions). Both fixes are needed and are not overlapping.🤖 Generated with Claude Code