buildDeployMap is the pure tree-computation core inside useOrgDeployState.
Export it and add isolated tests covering:
§1 Empty projections → empty map
§2 Single node, no parent, non-provisioning → unlocked root
§3 Single node, no parent, provisioning → deploying root
§4 Single node with existing parent → non-root, unlocked
§5 parentId points to absent node → treated as root
§6 Root (non-provisioning) + child → both unlocked
§7 Root (provisioning) + child → root deploying, child locked
§8 Three-level tree: provisioning grandparent → parent → child
§9 DeletingIds on non-root → isLockedChild=true
§10 DeletingIds on root → root locked, child unlocked
§11 Two independent roots, only one provisioning
§12 Root with 2 provisioning descendants → count=2
§13 Non-root with provisioning status → isActivelyProvisioning=true
§14 Deep 5-level chain, no provisioning → all unlocked
§15 Deleting + provisioning: deleting takes isLockedChild
§16 Child of provisioning root → isLockedChild=true
§17 Deep chain (5 levels), no provisioning → all unlocked
§18 Deep chain, middle node provisioning → single deploying root
§19 parentId → ghost parent → treated as root
Key insight from §18: findRoot walks the parent chain via byId only, so
a node's subtree root is determined by which parent in byId is absent.
A provisioning node nested deep in a tree contributes to its nearest
byId-ancestor's provCount, not its own.
Issue: #742 (buildDeployMap unit tests, #2071 follow-up).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>