As grtjn points out, comparing XML can get pretty complicated, particularly if your XML includes mixed content, or if you care about element ordering, or you have elements that can occur more than once. If those don't apply, then it's possible to take some shortcuts.
In your example, it might be good enough to add attributes to your path lists, and then compare everything (elements and attributes) based only on their string content.
The path list has to include attributes. You can take the union of different axises like this, or whatever criteria you have to include attributes and leaf elements and exclude text nodes and non-leaf elements.
local:path-to-node($left/child::*/child::*/(self::*[text()]|attribute::*))
And your path-to-node function has to be able to cope with being passed attributes. So you'd have to make some changes to your path-to-node function to make sure attributes are named correctly (with an '@'), and (if applicable) don't get a position marker
declare function local:path-to-node($node as node()){
let $attr := typeswitch ($node) case attribute() return '@' default return ()
return string-join(('', $node/ancestor::*/name(.), $attr||name($node)), '/')
};
Depending how complicated your path-to-node function is, it might be easier to have two of them.
fn:distinct-values((
local:path-to-element-node($left/child::*/child::*),
local:path-to-attribute-node($left/child::*/child::*/@*)
))