mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 22:10:34 +00:00
The move mirror re-pointed a parent edge for every shape a :mov-objects change named, so the fold followed a move the document refused. Two measured consequences: moving a shape into its own subtree left the fold disagreeing with a rebuild, and moving a shape under its own descendant closed a cycle in the fold's parent edges, after which the fold had not returned eight seconds later with app.graph.overlay.sync/subtree-eids on the stack. The gate is all-or-nothing for the whole change, which is the half a per-shape mirror cannot see: process-change :mov-objects proceeds only when the change names a shape, the parent exists, and every named shape passes its own is-valid-move?, so one invalid shape in a batch of ten moves none of them. The mirror now asks the same question of the running state, in the change's own order. It asks rather than answers, because a predicate written again in the applier would be a second definition of which moves are legal, and the two would drift the first time Penpot changes a nesting rule. So calculate-invalid-targets and the predicate move out of the change method into valid-move? at the namespace level, taking the objects view and the two flags, with the method's own call site delegating to them and no behaviour changed. The objects view is what the answer takes rather than a container: the decision walks the moved shape's subtree down :shapes and the destination's chain up :parent-id, reading :component-id and :shape-ref off every shape it meets, and the overlay holds all of those, which is why the same predicate answers for an index. subtree-eids gains the guard the cycle case needs, so a cyclic index fails loudly instead of spinning: the walk tracks the shapes it has entered and returns. Six new tests, and each mutation of the repair fails by name rather than in an unrelated test: dropping the gate fails seven assertions across the three gate tests, weakening every? to some fails the three that pin the change-level reading, asking the predicate about the wrong destination fails the same seven, and removing the walk guard fails the two that pin the cycle. backend-tests.graph-overlay-sync-test: 29 tests, 228 assertions, 0 failures. Entry: pp:graph:decide-a-move-the-way-the-document-does open -> done AI-assisted-by: deepseek-flash