The canonical Craft.js state from real saves shows that layout shells
(Section, BackgroundSection, HeroSimple, FeaturesGrid, ColumnLayout,
CTASection, FormContainer, Navbar, Footer) all serialize with
isCanvas:false. Only Container instances are canvases. The shells use
internal <Element canvas id="..."> linkedNodes for their drop targets.
Our previous CANVAS_TYPES set claimed all those shells were canvases,
which made Craft.js's toNodeTree walker hit an uncaught Invariant —
the shell asserted "I'm a canvas" but its render ignores data.nodes,
so the walker would chase phantom children.
ColumnLayout's render uses <Element id="col-0" is={Container} canvas>
which expects the columns to live in linkedNodes, not data.nodes. The
AI nests its column containers as direct children, so they'd land in
data.nodes — Craft.js's render ignores them (the layout draws fresh
empty Elements), but the orphaned children remain in state with
parent: <columnlayout-id>. Any subsequent toNodeTree walk then trips
on this inconsistency and the uncaught Invariant kills the editor.
Normalizer added in two places — treeToState (for scope=site/page
replaces) and buildNodeTree (for scope=section inserts and patch ops):
when we see a ColumnLayout with direct children, move them into
linkedNodes keyed col-0/col-1/col-2..., clear data.nodes, set the
column nodes' isCanvas to true (they hold content), and sync the
"columns" prop to the actual count.