Решили: структуру префаб-варианта правим на PrefabUtility.LoadPrefabContents, а близнецов различаем по GetCorrespondingObjectFromSource, не по имени. Перенос узла между двумя точками, стоящими в одном месте, идёт с сохранением ЛОКАЛЬНЫХ координат, а не мировых.

Почему: мировые координаты через SetParent(worldPositionStays: true) пересчитываются, и при совпадающих родителях дают тот же результат с точностью до float — но уже не тот же, а «почти». Локальный перенос при совпадающих родителях точен по построению, и это проверяемо одним числом: мир кисти 0.16771, -0.03415 до и после.

Грабли:

  • API оверрайдов не работает на ассете, загруженном LoadAssetAtPath. GetAddedGameObjects и GetRemovedGameObjects требуют живой инстанс и падают с «not a Prefab instance». Живым инстансом считается корень LoadPrefabContents — у варианта он Connected и знает свой источник; на самом ассете тот же вызов бросает исключение.
  • RevertRemovedGameObject принимает РОДИТЕЛЯ в инстансе первым, узел из базы вторым. Обратный порядок компилируется и падает в рантайме уже на проверке, а не на сигнатуре.
  • IsAddedGameObjectOverride при этом работает и на contents — из-за этого расхождение легко принять за случайный сбой инструмента, а не за правило «нужен инстанс».

Владелец правды: BoneUnit_Storybook.prefab (структура варианта), RigMigrate.cs (перенос путей).