# Pass 2 / W2.1 -- OpenTofu roots/modules (container-layer elimination)

**Author:** W2.1 (Phase-2 sonnet worker, `SCOPE-AND-EXECUTION-PLAN.md` Section 4).
**Date:** 2026-08-09. READ-ONLY planning; no mutation. Baseline consumed in full:
`SCOPE-AND-EXECUTION-PLAN.md`, `pass0-admin-report.md` (Option 1 CONFIRMED at the Phase-0
gate; cross-DC handling (a) CONFIRMED), `pass1-admin-report.md` (planning change-set; root
topology left explicitly OPEN for this worker to design, Phase 4 to ratify -- Section 7 item 1
+ Section 1 check 2). All citations below are from direct reads this session of
`opentofu/main.tf`, `opentofu/variables.tf`, `opentofu/vr1-dc0-substrate/main.tf`,
`opentofu/vr1-dc1-substrate/main.tf`, and every file under `opentofu/modules/*`.

---

## 0. What exists today (ground truth, for the disposition table's "current use" column)

Three roots, 16 module call sites, 12 distinct module types:

- **Outer root** (`opentofu/main.tf`, 623 lines, `qemu:///system` local to vcloud): DC storage
  pools (x3, incl. office1), office1-network, office1-opnsense, 3 mesh-triangle legs, netem,
  base-image, voffice1 (cloudinit-vm), 2 site-wan uplink NATs, **`vvr1_dc0`/`vvr1_dc1`**
  (cloudinit-vm -- the containment VMs, `:410-519`/`:537-623`).
- **Inner roots** (`opentofu/vr1-dc0-substrate/main.tf`, `vr1-dc1-substrate/main.tf`,
  qemu+ssh into `vvr1-dcN`): inner storage pool, 6 planes (dc-planes), wan-bridge, DC edge
  (opnsense-edge), 12 node VMs (node-vm `for_each`: 9 D-121 role nodes + juju-01 + maas-01 +
  tailscale-01).
- **12 module types**, all in `opentofu/modules/*`: `base-image`, `cloudinit-vm`, `dc-planes`,
  `dc-storage-pool`, `maas-vm-host`, `mesh-link`, `netem-link`, `node-vm`, `office1-network`,
  `opnsense-edge`, `site-wan`, `wan-bridge`.

---

## 1. Per-module disposition table

| Module | Current call sites (path:line) | Option-1 disposition | Reusable-module shape / notes |
|---|---|---|---|
| `cloudinit-vm` | `voffice1` (`main.tf:175`), `vvr1_dc0` (`:410`), `vvr1_dc1` (`:537`) | **PARTIAL COLLAPSE + NEW USE.** The two containment-VM instantiations (`vvr1_dc0`/`vvr1_dc1`) DELETE with the containment layer. The module BODY is unchanged and gains a NEW instantiation: the `vr1-dcN-client` VM (pass0's Option-1 shape: ~4/8192/80, metal-admin + transit legs, `expose_nested_virt=false`) is "L1, same module type as voffice1/edges" (pass1 Section 5). `voffice1`'s call is untouched (D-114, out of scope, pass1 check 1). | Stays a leaf module; no interface change needed. The client VM's `network_names`/`network_config` need a metal-admin plane ref (new -- containment VMs never touched a plane directly) instead of an uplink NIC. |
| `dc-planes` | `vr1_dc0_planes` (inner `:34-40`), `vr1_dc1_planes` (inner `:32-38`) | **RE-HOME, body unchanged.** Moves from the inner (qemu+ssh) root to the flat per-DC root, still one call per DC, same `planes` map / `mtu` / `domain_suffix` inputs (D-139/D-143 own the CIDR *values*, not this module). | No shape change. Becomes a direct child of the new `dc-site` composing module (Section 3). |
| `dc-storage-pool` | `vr1_dc0_storage`/`vr1_dc1_storage` (outer `:35-39`/`:49-53`, held ONLY the containment VM's own disk), `office1_storage` (outer `:59-63`), `inner_storage` x2 (inner `:26-30`/`:23-27`, held the 12 node disks + edge disk) | **COLLAPSES 2-per-DC -> 1-per-DC.** The outer/inner split existed only because Model B had two disks per DC (containment VM's own + the pool inside it). With no containment VM there is one disk surface per DC: the current `inner_storage` call (renamed) becomes THE per-DC pool, sized to hold node disks + edge disk + the client VM's disk. `office1_storage` is untouched (D-114). | Module body unchanged (a `libvirt_pool type=dir`). Net effect: 4 DC-scoped pool calls today -> 2 (one per DC) + `office1_storage` unchanged = 3 total, down from 5. |
| `maas-vm-host` | **NONE** -- authored but never instantiated (REFUTED for DC use by measurement 2026-07-20; `main.tf:6-19` of the module itself; retained pending the SEC-013 retire-or-keep ruling) | **NO CHANGE -- orthogonal to container-elim.** MAAS discovery under Option 1 is still PXE-enlistment + per-machine `power_type=virsh` (pass1 check 7, D-103/D-123 amendments), never this module. Flattening does not resurrect it. | Flag only: this pass is a natural moment to close the SEC-013 retire-or-keep question, but that ruling belongs to whoever owns SEC-013, not this pass -- listed so it is not silently forgotten, not acted on here. |
| `mesh-link` | `mesh_vr1_dc0_vr1_dc1`, `mesh_vr1_dc0_office1`, `mesh_vr1_dc1_office1` (outer `:125-141`) | **STAYS, all 3 legs, module and call sites unchanged.** This resolves pass1's open item 6 at the module-survey level: the mesh triangle is not containment-layer at all (it wires DC<->DC and DC<->Office1 L2 segments, independent of who lives at each end). What changes is the CONSUMER on the two office1-legs: `vvr1_dc0`/`vvr1_dc1` NIC1 today -> the `vr1-dcN-client` VM's transit NIC under Option 1 (client VM inherits the D-138/D-124 transit-leg role). The dc0<->dc1 leg (netem target) is untouched either way. | No module change. The *purpose* of the surviving transit leg (ssh -J / Office1-originated flows / SEC-010-successor-(ii) endpoint) is still an open placement question (pass1 Section 7 item 4) -- a consumer-side decision, not a module-shape one. |
| `netem-link` | `netem_vr1_dc0_vr1_dc1` (outer `:353-358`) | **STAYS, untouched.** Targets the dc0<->dc1 mesh bridge directly (`virbr5`), runs local `tc` on vcloud (D-128) -- has no dependency on either DC's internal shape. Confirmed non-consumer, same class as `lib-net.sh` in pass0's row 15. | No change. |
| `node-vm` | `vr1_dc0_node`/`vr1_dc1_node` `for_each` (inner `:250-265`/`:214-229`) | **RE-HOME, body unchanged.** The single largest re-home by resource count (12 domains/DC): the `for_each` map + `locals` block move verbatim from the inner root into the flat per-DC root, attaching to the same `dc-planes` outputs (now created locally, not over qemu+ssh). MAC re-measurement is owed post-apply regardless (pass1 Section 6 item 8) -- a data concern, not a module-shape one. | No shape change. Becomes a direct child of `dc-site`. |
| `office1-network` | `office1_network` (outer `:76-80`) | **STAYS, untouched.** D-114/Stage-2 territory (pass1 check 1). | No change. |
| `opnsense-edge` | `office1_opnsense` (outer `:99-117`), `vr1_dc0_opnsense`/`vr1_dc1_opnsense` (inner `:60-73`/`:56-69`) | **RE-HOME (DC calls only), body unchanged, one input rewired.** The two DC-edge calls move to the flat per-DC root. `wan_network_name` currently takes `module.vr1_dc0_wan.network_name` (the now-dead `wan-bridge`); it becomes `module.vr1_dc0_uplink.network_name` (the outer root's `site-wan` NAT) directly -- literally the same string-typed input, no module-interface change, just a different upstream module feeding it (D-125 bridge-in removed, "direct-NAT" per pass0 Section 4). `office1_opnsense` untouched. | No shape change to the module itself. Precedent for cross-root string-valued network references already exists in this repo: `office1_opnsense`'s `wan_network_name = "office1-wan"` (`main.tf:112`) is a bare literal today, not even a module reference -- the DC edges' rewired input is the SAME pattern, just sourced from a real module output instead of a literal. |
| `site-wan` | `vr1_dc0_uplink`/`vr1_dc1_uplink` (outer `:379-384`/`:391-396`) | **STAYS, unchanged, gains a consumer.** Same 2 calls, same `172.30.2.0/24`/`172.30.3.0/24` (ruled literals, D-125 note at `variables.tf:246-251` -- IPAM identity untouched). Its output now feeds the DC edge's WAN *directly* (see `opnsense-edge` row) instead of via `wan-bridge`. | No change. This module absorbs the egress duty `wan-bridge` used to hand off. |
| `wan-bridge` | `vr1_dc0_wan`/`vr1_dc1_wan` (inner `:51-56`/`:45-50`) | **COLLAPSES / DELETED.** Its entire reason to exist (D-125 bridge-in) was that the containment VM's only routed leg was the SEC-010 FORWARD-dropped transit, so a NAT-mode WAN inside it would have had no egress (OBS-3). With no containment VM, the DC edge attaches directly to the outer `site-wan` NAT -- there is nothing left for this module to bridge. Confirmed dead per pass0 row 3 and pass1's B.4 "VANISHES" list. | Retire the module directory (or leave in place, unreferenced, per this repo's append-only bias -- Phase 4's call, not this pass's). Its host-bridge (`br-vr1-dcN-wan`) and the bootstrap's `--host-nodes` bridge-verify duty vanish with it. |
| `base-image` | `ubuntu_noble_base` (outer `:165-173`) | **STAYS, unchanged, gains consumers.** One call today, consumed by `voffice1` + both `vvr1_dcN`. Under Option 1 it is consumed by `voffice1` + both `vr1-dcN-client` VMs (same base image, same `backing_store` copy-on-write pattern) -- one fewer distinct consumer class (containment VMs gone) but the SAME call. | No change. |

**Summary count:** of 12 module types, **1 collapses** (`wan-bridge`), **1 is dead/orthogonal**
(`maas-vm-host`), **3 stay untouched** (`office1-network`, `mesh-link`, `netem-link`), **1
stays with a rewired input** (`site-wan`), and **6 re-home with unchanged bodies**
(`cloudinit-vm`, `dc-planes`, `dc-storage-pool`, `node-vm`, `opnsense-edge`, `base-image`).
**Zero modules need their HCL rewritten** -- every disposition is a re-homing / re-wiring /
deletion of call sites, never a module-body change. This is the direct payoff of the modules
already being written provider-agnostically (D-119's own design intent) plus the fact that
flattening does not change *what* gets built, only *where the provider dials*.

---

## 2. THE ROOT-TOPOLOGY FORK -- analysis + recommendation

**Carried open from Phase 1** (`pass1-admin-report.md` Section 1 check 2, Section 7 item 1):
merged single flat root vs. shared-outer + per-DC roots. Pass 1 established the sequence and
the (a)-control ordering hold under EITHER shape, and explicitly assigned this worker to
DESIGN it (Phase 4 ratifies).

### 2.1 The two candidate shapes

- **(A) One merged root.** Collapse outer + both inner roots into a single `opentofu/`
  directory/state: mesh, uplinks, office1, voffice1, BOTH DCs' pools/planes/edges/12-node
  fleets/client VMs, all in one state file, ideally driven by a single `for_each` over a
  `var.sites` map (`{vr1-dc0 = {...}, vr1-dc1 = {...}}`).
- **(B) Shared-outer + per-DC-flat roots (RECOMMENDED).** Keep today's 3-root shape, but
  delete the qemu+ssh indirection: the outer root keeps everything that is genuinely
  cross-DC/shared (mesh, uplinks, office1, voffice1, base-image); each DC's current INNER root
  becomes a **flat** root -- same directory, but its `provider "libvirt"` block changes from
  `qemu+ssh://...@${transit_ip}/system?keyfile=...` (inner `:12-22`, dc1 `:11-19`) to the outer
  root's own local `qemu:///system` (`variables.tf:1-4`) -- and it additionally gains that DC's
  `vr1-dcN-client` VM (moved out of the outer root, see Section 2.4).

### 2.2 Evaluation against the four named axes

**State blast radius.** (A) puts every domain of both DCs -- the full node fleet, both edges,
both client VMs' credentials-bearing config -- in ONE state file. An untargeted mistake
(the exact incident class CLAUDE.md's hard rule 4 preamble names: *"an ad-hoc `juju
destroy-model` was run instead of the D-061 teardown scripts and took the dc0 controller
down"*) threatens both DCs at once. (B) makes DC0's and DC1's states structurally
un-reachable from each other -- a mistake in DC1's root cannot touch a DC0 resource because it
is not in DC1's state file, full stop, with no reliance on `-target` discipline being followed
correctly every time. **(B) wins decisively** -- this axis is where this repo has first-hand
incident history to weigh against.

**Destroy scoping / the teardown primitive.** D-122's original intent was "site-down = one
`virsh destroy vvr1-dc0`" -- ONE OBJECT, ONE COMMAND. Under (B), the closest re-earn is
literally `cd opentofu/vr1-dc0-flat/ && tofu destroy` (or a thin wrapper script) -- one
directory, one command, matching the ORIGINAL intent almost exactly, and matching this repo's
EXISTING convention (the inner roots are already per-DC directories; `dc-dc-teardown-
rollback.md` Path A is already organized around "pick a root"). Under (A), site-down requires
a generated `-target=module.xxx` list (one entry per plane/node/edge/client-VM resource for
that DC) -- a new artifact to build AND keep in sync with the node roster, and a bare
`tofu destroy` run without it wipes BOTH DCs (again, the CLAUDE.md-named incident shape).
**(B) wins** -- lower delta from D-122's intent, reuses an existing convention, smaller new
surface to build and test.

**Cross-DC (a)-isolation ordering.** Pass 1 Section 3's ordering invariant: the control must be
verified "before ANY flat substrate apply that can make any two DCs' planes co-resident...
since under a merged single root the FIRST apply may create both DCs' planes at once." Under
(A) this is a real, standing risk: the default (untargeted) `tofu apply` of a merged root is
ALREADY a both-DCs-co-resident event on its very first run, so safety depends on the operator
remembering to `-target` down to one DC even for that very first apply. Under (B) this is
structural, not procedural: DC1's planes literally cannot be created by applying DC0's root --
there is no way to accidentally co-create them, so the ordering invariant reduces to "run the
(a) control's `--check` before the first per-DC-root apply of EITHER DC," a much simpler
operator contract. **(B) wins.**

**`for_each`/module reuse.** Largely NEUTRAL. Regardless of root shape, every module body
listed in Section 1 is reused unchanged -- a module call does not care which root instantiates
it. Introducing the `dc-site` composing module (Section 3) gives BOTH shapes the same DRY
benefit: (A) would `for_each` `dc-site` over `var.sites`; (B) has each per-DC root make ONE
`dc-site` call with that DC's own tfvars file. (A) is marginally more DRY at the HCL-file level
(one file instead of two near-identical directories differing only by tfvars), but (B)'s
"two thin directories, one shared module" is not meaningfully more duplicative than (A)'s
"one file with a for_each" once `dc-site` exists -- this repo already carries the inner roots'
current near-duplication (dc0/dc1 substrate `main.tf` files today) and pass1/pass0 both
identify that duplication as tolerable precisely because the MODULE bodies (not the root files)
are the unit of reuse. **Mild edge to (A)**, not decisive against the three axes above.

**Current outer/inner structure.** (B) is the direct, low-delta evolution of what exists today
-- the inner roots ALREADY are per-DC directories; the only structural change is deleting the
qemu+ssh provider indirection (one block per root) and merging the outer `vvr1_dcN`-disk pool
into the (renamed) inner pool. (A) requires collapsing 3 roots/states into 1, which means
re-deriving resource addresses for everything currently in the outer root's DC-scoped blocks
(the `vr1_dc0_storage`/`vr1_dc1_storage` pools, if kept renamed, need `moved{}` blocks -- this
repo has done exactly this kind of state-address migration once before, for a much smaller
rename, in `main.tf:275-298`'s D-119 `moved{}` blocks). **(B) wins** on migration risk and
delta size.

### 2.3 Recommendation

**Recommend (B): shared-outer root + per-DC-flat roots (three roots total).** Every
blast-radius, destroy-scoping, and isolation-ordering axis favors it, each for reasons
specific to THIS repo's own operating discipline and incident history (CLAUDE.md hard rule 4's
own worked example is exactly the failure mode (B) structurally prevents and (A) would
reproduce at a larger scale). (A)'s only edge -- marginally more DRY HCL -- is fully absorbed
by the `dc-site` module proposed in Section 3, which benefits both shapes equally. This
recommendation is **directional, for Phase 4 to ratify** (pass1 Section 7 item 1), not a
ruling.

**What (B) implies for the teardown primitive:** the emergency/gated site-down lever (pass1
Section 6 items 1 + 6) becomes "operate on that DC's root directory" -- `tofu destroy` (gated)
or a `virsh destroy` loop over that root's own state-listed domains (emergency), never touching
the other DC's or the outer root's state. This is the artifact Phase 2/W2.3 or W2.4 should
build against.

**What (B) implies for the (a)-control ordering:** the control's `--check` gate (Stage 1 home,
per pass1 Section 3) precedes "the first `tofu apply` of EITHER per-DC-flat root" -- a single,
simple operator contract, not a per-root-shape-dependent one. No `-target` discipline is
load-bearing for safety under (B) (unlike (A)).

### 2.4 The client-VM grouping call (resolves pass1's W1.2-vs-W1.4 tension, Section 1 check 7)

**Recommend: the `vr1-dcN-client` VM's APPLY site is the per-DC-flat root** (grouped with that
DC's planes/nodes/edge), even though its MODULE TYPE is `cloudinit-vm` (L1, same type as
`voffice1` -- pass1 Section 5's layer model, which classifies by module type, not apply
grouping; both can hold per pass1's own reconciliation). Rationale, specific to (B):

1. **Teardown symmetry.** If the client VM lived in the outer root, destroying "the DC" would
   require touching TWO roots again (client VM in outer, everything else in the per-DC root)
   -- reintroducing exactly the two-step INNER-then-OUTER choreography the flattening exists to
   remove (pass0 Section 1.2's "single largest structural simplification").
2. **Credential-isolation symmetry (SEC-026).** The client VM is a DC-scoped credential-bearing
   host (SEC-028/029 residencies). Co-locating it in the shared outer root's state would put a
   single-DC credential-bearing resource in the SAME state as the other DC's substrate --
   directly opposed to the per-DC isolation SEC-026 exists to guarantee. Per-DC-root placement
   keeps that isolation at the STATE level, not just the network level.
3. **D-138 fidelity.** "The cloud-facing client lives IN the DC" (D-138's ruled principle,
   `docs/design-decisions.md:7118-7124`) is honored more literally by the client VM sharing its
   DC's own tofu root/state than by living in a shared outer one.

---

## 3. Proposed NEW reusable IaC modules

### 3.1 `modules/dc-site` (the composing module Option 1 wants -- HIGHEST LEVERAGE)

Replaces the copy-pasted body of `vr1-dc0-substrate/main.tf` / `vr1-dc1-substrate/main.tf`
(currently ~230-266 lines each, identical shape, differing only in tfvars + dc1's schematic
MAC scheme) with ONE module both per-DC-flat roots call once. Composes, in order:
`dc-storage-pool` (1) -> `dc-planes` (6) -> `opnsense-edge` (1, direct-NAT wired) ->
`node-vm` `for_each` (12: 9 D-121 role nodes + juju-01 + maas-01 + tailscale-01) ->
`cloudinit-vm` (1, the client VM).

**Inputs (sketch, no invented values -- every CIDR/MAC/sizing stays a real per-DC var, same
discipline `variables.tf` already enforces):**

```
site_token          # e.g. "vr1-dc0" -- D-106 naming anchor, threaded into every child call
domain_suffix        # anchor, D-106
underlay_mtu          # Phase-0 measured, no default
pool_path             # host filesystem path for this DC's single storage pool
planes                # map(plane_name -> {cidr}), same shape as var.vr1_dc0_planes today
opnsense_base_path    # prepped nano image path (unchanged from today)
uplink_network_name   # STRING -- the outer root's site-wan NAT name (e.g. "vr1-dc0-uplink"),
                        replaces module.vr1_dc0_wan.network_name; same cross-root string
                        pattern office1_opnsense already uses (main.tf:112)
nodes                 # map(vm_name -> {vcpu, mem, disk_gib, osd_gib?, macs}) -- the 12-entry
                        roster, identical shape to today's vr1_dc0_nodes/vr1_dc1_nodes locals
client_vm             # object: vcpu/mem/disk, ssh_pubkey_path, macs, transit_network_name
                        (STRING -- outer root's mesh_vr1_dcN_office1 network name),
                        metal_admin_ip/transit_ip (NetBox-assigned, no defaults)
```

**Outputs:** plane `network_names` map (pass-through, for verify scripts / a future D-140 Juju
root to consume node/edge addressing), node `domain_names`/`ids` map, client-VM `domain_id`,
edge `domain_id` -- the same shape `dc-planes`/`node-vm`/`cloudinit-vm` already expose today,
just re-exported one level up.

**What it deliberately does NOT own:** the (a) cross-DC isolation control (confirmed a
procedure/L5 artifact, not a tofu module, pass1 Section 3 -- do not build it twice here); the
rack-controller-remainder / D-131 forwarder / credential-bootstrap content of the client VM's
`user_data` (pass1 Section 7 OPEN item 2 -- still unruled; this module's `client_vm` input
should accept a `user_data`/`network_config` string exactly like `cloudinit-vm` does today,
NOT hardcode content that depends on an unruled placement decision).

### 3.2 The per-DC-flat ROOT (not a module -- a root pattern)

`opentofu/vr1-dc0-flat/`, `opentofu/vr1-dc1-flat/` (naming: avoid reusing `-substrate` unqualified
if that name is wanted for the pre-Roosevelt bare-metal target later -- Phase 4's naming call,
flagged not decided here). Each root: one local `provider "libvirt"` block (no keyfile/sshauth/
known_hosts -- that whole trap class, `vr1-dc0-substrate/main.tf:12-22`'s header comment,
disappears with the qemu+ssh dial), one `module "site" { source = "../modules/dc-site" ... }`
call, one tfvars file. This is the direct, low-delta successor described in Section 2.

### 3.3 NOT recommended yet: a dedicated `site-client-vm` wrapper module

The client VM's `cloudinit-vm` call could someday warrant its own thin wrapper (to standardize
its user_data templating once the rack-controller-remainder + D-131 forwarder + credential
bootstrap logic is designed). **Not proposed now**: that content depends on pass1's still-OPEN
Section 7 item 2 (rack-controller-remainder placement), and building the wrapper before that
ruling would either invent the content (hard rule 2 violation) or ship an empty wrapper with no
real interface to validate. Flagged as a likely Phase-2/W2.4 or Phase-4 follow-on once that
placement is ruled, not built here.

### 3.4 The (a) cross-DC host-isolation control -- explicitly OUT of `opentofu/modules/`

Restated from this dimension's view so Phase 2's tools workers do not build it twice: pass1
Section 3 already resolves its artifact kind as a **procedure/L5 verify artifact** (nftables +
`--check` + harness + SEC-NNN row), not an OpenTofu resource. Nothing in this survey changes
that. It sequences BEFORE the first `tofu apply` of either `vr1-dcN-flat` root (Section 2.3).

---

## 4. D-140 alignment (pinned axis -- noted, not folded in)

D-140 (`docs/design-decisions.md:7781-7819`, PINNED 2026-08-02 to the end-of-deployment
review) converts the **Juju layer** (applications/relations -- the `bundle.yaml`-equivalent
56-application/108-relation surface) to OpenTofu-managed resources, sequenced strictly AFTER a
hardened, tested dc0 deployment. It is an **L4 concern** (pass1 Section 5's layer model) and
does not touch any module in Section 1 or the `dc-site` module proposed here -- those are all
L0-L2 substrate. Where D-140 would eventually intersect this pass's deliverable, noted for
later, not designed now:

- It would add a **separate, later root** (e.g. `opentofu/vr1-dcN-juju/`, Juju-provider-backed)
  consuming this pass's `dc-site` OUTPUTS (node hostnames/addresses, once MAAS has assigned
  them -- not available at `dc-site` apply time) as its inputs, not folding into `dc-site`
  itself.
- The root-naming convention adopted in Section 3.2 should stay generic enough
  (`<site_token>-<purpose>`) to admit a future `-juju` root per DC without renaming the
  substrate roots this pass proposes.
- D-140's own review is still owed provider-capability research (`references/opentofu-provider-
  docs.md`) before it can rule (a) model/config-layer-only, (b) full translation, or (c)
  decline -- none of which this pass's substrate-module design depends on or should
  pre-suppose.

---

## 5. Summary for the Phase-2 administrator

- **Root-topology recommendation: (B) shared-outer + per-DC-flat roots** (3 total), NOT a
  merged single root -- every axis (state blast radius, destroy scoping, (a)-control ordering,
  migration delta) favors it for reasons specific to this repo's own incident history; `for_each`
  DRY-ness is the only edge for a merged root and is absorbed by the `dc-site` module either way.
- **Client VM apply-groups with its DC's flat root**, not the outer root (teardown symmetry +
  SEC-026 state-level isolation + D-138 fidelity).
- **Zero module bodies need rewriting.** 6 modules re-home unchanged, 1 (`site-wan`) stays with
  a rewired input, 3 stay fully untouched, 1 (`wan-bridge`) collapses, 1 (`maas-vm-host`) is
  dead/orthogonal.
- **One new module wanted: `modules/dc-site`** (composes pool+planes+edge+12 nodes+client VM;
  interface sketched Section 3.1) -- the single highest-leverage new artifact, replacing today's
  copy-pasted per-DC inner-root bodies.
- **A `site-client-vm` wrapper is explicitly NOT proposed yet** -- blocked on pass1's open
  rack-controller-remainder ruling; building it now would invent content.
- **The (a) isolation control stays OUT of `opentofu/modules/`** (procedure/L5, already ruled
  by pass1) -- restated here so Phase 2's tools workers do not duplicate it.
- **D-140 noted, not folded in**: a later, separate L4 root consuming this design's outputs.
