src/Application/Handlers/UpdateNodeHandler.php:21:            'op' => 'node.update',
src/Application/Handlers/RenameColumnHandler.php:20:            'op' => 'column.rename',
src/Application/Handlers/MoveColumnHandler.php:20:            'op' => 'node.move',
src/Application/Handlers/MoveNodeHandler.php:20:            'op' => 'node.move',
src/Application/Handlers/CreateColumnHandler.php:23:                'op' => 'node.create',
src/Application/Handlers/RenameWorkspaceHandler.php:20:            'op' => 'workspace.rename',
src/Application/Handlers/CreateWorkspaceHandler.php:21:            'op' => 'node.create',
src/Application/Handlers/CreateNodeHandler.php:32:            'op' => 'node.create',
src/Application/Handlers/UpdateTagFilterHandler.php:22:            'op' => 'tagFilter.set',
src/Application/Handlers/AddTagHandler.php:18:                'op' => 'tag.add',
src/Application/Handlers/RemoveTagHandler.php:19:                'op' => 'tag.remove',
src/Application/Handlers/DeleteNodeHandler.php:20:            'op' => 'node.delete',
    /** @param array<string,mixed> $state */
    private function applyOperation(array $state, array $operation): array
    {
        return match ($operation['op']) {
            // v3 generic node/tag operations
            'node.create' => $this->opNodeCreate($state, $operation),
            'node.update' => $this->opNodeUpdate($state, $operation),
            'node.move'   => $this->opNodeMove($state, $operation),
            'node.delete' => $this->opNodeDelete($state, $operation),
            'tag.add'     => $this->opTagAdd($state, $operation),
            'tag.remove'  => $this->opTagRemove($state, $operation),
            // filters persist (renommé)
            'filters.set' => $this->opFiltersSet($state, $operation),
            'tagFilter.set' => $this->opTagFilterSet($state, $operation),
            'workspace.rename' => $this->opWorkspaceRename($state, $operation),
            'column.rename' => $this->opColumnRename($state, $operation),
            default => $state,
        };
    }

    private function opNodeCreate(array $state, array $op): array
    {
        $nodes = &$state['nodes'];
        $parentId = (string) $op['parentId'];
        $index = isset($op['index']) ? (int) $op['index'] : null;
        $nodeId = $op['nodeId'] ?? Identifiers::new();
        if (!isset($nodes[$parentId])) return $state;

        $parent = $nodes[$parentId];
        if (($parent['sys']['shape'] ?? null) !== 'container') {
            throw new BoardInvariantViolation('shape.leaf_has_children', 'Cannot append child to leaf node');
        }

        $shape = $this->extractShape($op['sys'] ?? null);
        $tags = $op['tags'] ?? [];
        if (!is_array($tags)) {
            $tags = [];
        }

        $node = [
            'id' => $nodeId,
