diff --git a/FABRIC-3.md b/FABRIC-3.md
index 8515e78..e1079b4 100644
--- a/FABRIC-3.md
+++ b/FABRIC-3.md
@@ -153,7 +153,11 @@ decisions get added here, not to `FABRIC-2.md`. Follow the same discipline `FABR
its stale POSIX-flavored ownership fields with a pubkey-fingerprint owner + a fast-deny ACL
bit + deliberate reserved slack. Field design done; the actual code edit is not.
-- [ ] Implement writing the block-map to a drive.
+- [x] **CLOSED 2026-08-27 (`FABRIC-3.md` §F.12) — already built, not a separate item.**
+ `blk_set_meta()`'s dirty-tracking already flows through the exact same `cache_writeback()`/
+ `blk_flush()` path ordinary block data uses; the whole devblock (data + metadata) already
+ gets written to the device, real and unstubbed. Blocked only on `WRITE(10)` itself, already
+ modeled in the graph — nothing else stands between this and working.
- [ ] Implement reading/validating the block-map from a drive on insertion.
@@ -1433,10 +1437,10 @@ graph TD
CERTVERIFY["❌ Zuse-signed user-cert verification (M3)
SCOPED 2026-08-27 (§F.7) — X.509/DER, own trust root"]
FIRSTTOUCH["❌ First-touch identity→block-range
allocation (M3) — SCOPED 2026-08-27 (§F.11)"]
BMAPFMT["❌ On-drive block-map format (M3)
SCOPED 2026-08-27 (§F.4) — repurpose blk_meta_t"]
- BMAPWRITE["❌ Write block-map to drive (M3)"]
+ BMAPWRITE["✅ Write block-map to drive (M3)
CLOSED 2026-08-27 (§F.12) — already built, blocked only on W10"]
BMAPREAD["❌ Read/validate block-map on insert (M3)"]
MIGSM["🟡 Migration state machine (M3)
partially answered: ACL decides *when*"]
- UNCLEAN["✅ Unclean-removal handling (M3)
CLOSED 2026-08-27 (§F.10) — same kill path as EJECT, minus flush"]
+ UNCLEAN["❌ Unclean-removal handling (M3)
DESIGN CLOSED 2026-08-27 (§F.10), code not yet written"]
ACLKEY["❓ ACL 'bumps and holes' comparison
semantics — SAME GAP in M5 and D.4"]
WIREBIND["❌ Wire hotplug → capsule_birth_baby() (M5)"]
@@ -1491,8 +1495,8 @@ graph TD
classDef unblocked fill:#a83,stroke:#333,color:#fff
classDef open fill:#666,stroke:#333,color:#fff
classDef partial fill:#883,stroke:#333,color:#fff
- class M6,PH8,EXPIRE,MSGSHAPE,HOTPLUG,UNCLEAN done
- class W10,STALL,FIRSTTOUCH,BMAPWRITE,BMAPREAD,WIREBIND,BINDSTEP,DETACH,MINT,BMAPFMT,CERTVERIFY,RUNCAP blocked
+ class M6,PH8,EXPIRE,MSGSHAPE,HOTPLUG,BMAPWRITE done
+ class W10,STALL,FIRSTTOUCH,BMAPREAD,WIREBIND,BINDSTEP,DETACH,MINT,BMAPFMT,CERTVERIFY,RUNCAP,UNCLEAN blocked
class ACLKEY,MSGMIGRATE,SSDSCOPE,ROUNDTRIP,POLYBLOCK open
class MIGSM partial
```
@@ -1551,6 +1555,10 @@ finished). Dashed arrows = softer "gates/informs" relationships.
mechanism against the current design. A second, unrelated discovery in the same pass —
`blk_meta_t`'s existing chain fields — turned what looked like a fragmentation problem into
a non-issue for free.
+- **`BMAPWRITE` was already done** (§F.12) — `BMAPFMT`'s decision to repurpose the existing
+ `blk_meta_t` accessors instead of a new table meant "write the map" and "write a block" were
+ never actually two operations. The only reason this hasn't been seen working is `WRITE(10)`
+ itself, already the graph's known hub.
**Not yet done:** an ordered plan (which node to attack first, given the graph). Per Captain
Bob's own framing, that's the next pass — "start asking and answering questions iteratively
@@ -2059,6 +2067,36 @@ room (matching the pool-scope clarification in §F.6: the thumb is the default p
node is specifically the *overflow* case) — implied yes by that clarification, not explicitly
re-confirmed here.
+### F.12 — `BMAPWRITE` (closed — already built, blocked only on `WRITE(10)`)
+
+Traced `blk_set_meta()`/`cache_writeback()`/`blk_flush()` fully before assuming any new write
+path was needed. **Finding: this node is already done.** `blk_set_meta()`
+(`block_subsystem.c:1198-1210`) marks the same cache-buffer entry's `meta_dirty` flag that
+ordinary block-data writes use; `cache_writeback()` (`block_subsystem.c:274-285`) — called from
+both `blk_flush()`'s targeted and flush-all paths, the exact function `blk_vm_flush_all()` and
+`SAVE-BUFFERS` already exercise — packs the in-memory `blk_meta_t[]` array back into the
+devblock's metadata region (`meta_to_slice()`) and writes the *whole* 4 KiB devblock (data +
+metadata together, since they share one physical buffer) via `write_devblock_4k()`, real and
+unstubbed all the way down to `dev->write()`.
+
+**Why this closes rather than just narrows:** the M3 item as originally written assumed a
+*separate* block-map structure that would need its own bespoke write mechanism. `BMAPFMT`'s
+decision (§F.4) to repurpose the already-existing, already-wired `blk_meta_t` accessors instead
+of building a new table means "writing the map" and "writing a block" were never actually two
+different operations — the infrastructure that already existed for ordinary block persistence
+already covers metadata identically, with zero new code. The only reason this hasn't been
+observed working end-to-end is the same hard blocker already modeled in the graph: `WRITE(10)`
+doesn't exist yet, so `dev->write()` never actually reaches physical media for a USB-attached
+drive today. Nothing else stands between this and working.
+
+**Handoff note for `BMAPREAD`'s own future pass:** the read side (`cache_load_devblock()`,
+`block_subsystem.c:308-322`) already unpacks and validates metadata (`meta_from_slice()`,
+magic-checked, resets to a clean default on a bad/missing magic) on every devblock load — very
+likely the same "already built, not a separate item" outcome, but not confirmed here; that
+node may have its own distinct question (e.g. whether a home-blocks drive needs an explicit
+validate-on-insert step beyond ordinary lazy per-devblock read-through-cache), left for its own
+pass rather than assumed away.
+
### D.5 — Scope expansion (2026-08-27): identity is common to every VM, not just users
Surfaced while scoping `ACLKEY`, stated directly: *"the whole object is to deliver a