# Phase O — DatPath Unification Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Extract the load-bearing WorldBuilder code we use into acdream's own source tree, route all dat reads through our `DatCollection`, and drop the two `<ProjectReference>` entries that pull in WB. End state: ONE dat reader in the process, ZERO references to `WorldBuilder.*` / `Chorizite.OpenGLSDLBackend.*` namespaces in our source.

**Architecture:** Verbatim copy of ~33 WB files (~7.7K LOC) split across two layers — `src/AcDream.Core/Rendering/Wb/` for pure-CPU helpers (`TextureHelpers`, `SceneryHelpers`, `TerrainUtils`, `TerrainEntry`, `CellSplitDirection`); `src/AcDream.App/Rendering/Wb/` for everything that touches GL (`OpenGLGraphicsDevice`, `ManagedGL*`, `GLSLShader`, `GLHelpers`, `GLStateScope`, `ObjectMeshManager`, particle pipeline, render data structs). `ObjectMeshManager` refactored to take our `DatCollection` directly (with fallback to a thin adapter if the call-site count exceeds 20). Three `internal` types in Chorizite promoted to `public`.

**Tech Stack:** C# .NET 10, Silk.NET.OpenGL, BCnEncoder.Net, Chorizite.DatReaderWriter (NuGet — stays). MIT attribution required for the extracted code per WB's license.

**Spec:** [`docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md`](../specs/2026-05-21-phase-o-dat-path-unification-design.md)
**Audit:** [`docs/research/2026-05-21-phase-o-t1-wb-audit.md`](../../research/2026-05-21-phase-o-t1-wb-audit.md)

---

## File Structure

### Created during this phase

**`src/AcDream.Core/Rendering/Wb/` (new directory):**
- `TextureHelpers.cs` — pure pixel-format decoders (`Fill*` helpers for INDEX16/P8/A8/A8R8G8B8/R8G8B8/R5G6B5/A4R4G4B4/etc.).
- `SceneryHelpers.cs` — stateless scenery transformations (`Displace`, `RotateObj`, `ScaleObj`, `ObjAlign`, `CheckSlope`).
- `TerrainUtils.cs` — terrain math (`GetHeight`, `GetNormal`, `OnRoad`, `CalculateSplitDirection`).
- `TerrainEntry.cs` — packed per-vertex terrain struct (`[MemoryPackable]` attribute STRIPPED).
- `CellSplitDirection.cs` — `SWtoNE` / `SEtoNW` enum.

**`src/AcDream.App/Rendering/Wb/` (existing directory, ~25 files added):**
- `OpenGLGraphicsDevice.cs` — Silk.NET wrapper, GL context, `ProcessGLQueue`.
- `GLSLShader.cs`, `GLHelpers.cs`, `GLStateScope.cs` — shader + state wrappers.
- `ManagedGLTexture.cs`, `ManagedGLTextureArray.cs`, `ManagedGLVertexBuffer.cs`, `ManagedGLIndexBuffer.cs`, `ManagedGLVertexArray.cs`, `ManagedGLFrameBuffer.cs`, `ManagedGLUniformBuffer.cs` — GL resource wrappers (7 files).
- `ObjectMeshManager.cs` — mesh extraction + GPU upload (REFACTORED to take `DatCollection`).
- `ObjectRenderBatch.cs`, `ObjectRenderData.cs` — render-data structs.
- `DebugRenderSettings.cs` — settings struct for `OpenGLGraphicsDevice` ctor.
- Particle batcher + emitter files (count + names confirmed during T4 closure walk).
- `GlobalMeshBuffer.cs`, modern render data structs (count + names confirmed during T4 closure walk).
- `EmbeddedResourceReader.cs`, `TextureFormatExtensions.cs`, `BufferUsageExtensions.cs` — three `internal` types promoted to `public`.

**Repo root:** `NOTICE.md` updated (or created) with WB MIT attribution.

### Modified during this phase

- `src/AcDream.Core/AcDream.Core.csproj` — drop 2 `<ProjectReference>` lines.
- `src/AcDream.App/AcDream.App.csproj` — drop 2 `<ProjectReference>` lines.
- `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs` — drop `_wbDats` field/ctor/dispose; drop `[indoor-upload] NULL_RESULT` block (lines 192-263 currently); simplify ctor to pass `DatCollection` directly to `ObjectMeshManager`.
- `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` — update `using Chorizite.OpenGLSDLBackend.Lib;` → `using AcDream.App.Rendering.Wb;`.
- `src/AcDream.Core/World/WbSceneryAdapter.cs` — update `using WorldBuilder.Shared.Models;` → `using AcDream.Core.Rendering.Wb;`.
- `src/AcDream.Core/World/SceneryGenerator.cs` — update both `using` lines to new namespaces.
- `src/AcDream.Core/Textures/SurfaceDecoder.cs` — update `using Chorizite.OpenGLSDLBackend.Lib;` → `using AcDream.Core.Rendering.Wb;`.
- `tests/AcDream.Core.Tests/Textures/TextureDecodeConformanceTests.cs` — update `using` line.
- `tests/AcDream.Core.Tests/World/WbSceneryAdapterTests.cs` — doc-comment fix only.
- `tests/AcDream.Core.Tests/World/SceneryGeneratorTests.cs` — doc-comment fix only.
- `tests/AcDream.Core.Tests/Rendering/Wb/MeshExtractionConformanceTests.cs` — doc-comment fix only.
- `tests/AcDream.Core.Tests/Terrain/ClientReference.cs`, `ClientConformanceTests.cs` — doc-comment fix only.
- `CLAUDE.md` — rewrite the "WB integration cribs" section.
- `docs/architecture/worldbuilder-inventory.md` — update ownership notes.

### Deleted during this phase

- `tests/AcDream.Core.Tests/Terrain/SplitFormulaDivergenceTest.cs` — one-time data-collection test, job done at N.5b.

---

## Task 1: Setup (was O-T2)

**Files:**
- Create: `src/AcDream.Core/Rendering/Wb/` (directory only — no files yet)
- Create/Modify: `NOTICE.md` (repo root)

The `src/AcDream.App/Rendering/Wb/` directory already exists.

- [ ] **Step 1: Create the Core/Rendering/Wb directory**

Run:
```powershell
New-Item -ItemType Directory -Force -Path "src/AcDream.Core/Rendering/Wb"
```
Expected: directory created, no error.

- [ ] **Step 2: Check whether NOTICE.md exists**

Run:
```powershell
Test-Path "NOTICE.md"
```
Expected: `True` or `False`. If `True`, read it before editing. If `False`, the next step creates it.

- [ ] **Step 3: Create or append the WorldBuilder attribution section in NOTICE.md**

Write to `NOTICE.md` (append if exists, create otherwise):

```markdown
## WorldBuilder

Portions of acdream's rendering and dat-handling code are copied from
WorldBuilder (https://github.com/Chorizite/WorldBuilder), MIT-licensed.
The extracted code lives under:

- `src/AcDream.Core/Rendering/Wb/` — pure helpers (texture decode,
  scenery transforms, terrain math).
- `src/AcDream.App/Rendering/Wb/` — GL infrastructure and mesh pipeline.

Original copyright holders: Chorizite contributors (see WorldBuilder's
LICENSE file). Adapted by acdream maintainers to consume our
`DatCollection` directly (replacing WB's `DefaultDatReaderWriter`) and
to remove editor-only code paths.

Original MIT license text:

[Insert full MIT license body here from references/WorldBuilder/LICENSE — verify the file exists at that path and use its exact content]
```

If the user has a different `NOTICE.md` style, follow theirs; this is the minimum content.

- [ ] **Step 4: Verify the MIT license body**

Run:
```powershell
Test-Path "references/WorldBuilder/LICENSE"
Get-Content "references/WorldBuilder/LICENSE" -TotalCount 5
```
Expected: file exists; first few lines should be MIT license text. Copy the full LICENSE body into NOTICE.md, replacing the `[Insert ...]` placeholder.

- [ ] **Step 5: Commit**

```powershell
git add NOTICE.md
git add "src/AcDream.Core/Rendering/Wb/.gitkeep"  # if needed to track empty dir
git commit -m @'
chore(O-T2): create Core/Rendering/Wb directory + NOTICE.md attribution

Phase O setup: extracted-WB code home + MIT attribution per O-D5.
Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
'@
```

Note: `New-Item -ItemType Directory` doesn't track empty dirs in git. If T1 needs the directory to exist before later commits, write a placeholder file (e.g., `.gitkeep` or a tiny `README.md` describing the directory's purpose) and commit it.

---

## Task 2: Extract pure stateless helpers to Core (was O-T5)

Moved earlier in the plan from spec's T5 position because: (a) lowest-risk extraction (pure functions, no GL, no dat behavior change); (b) unblocks the namespace migrations in our own Core files; (c) catches any namespace-update gotchas before the load-bearing T4.

**Files:**
- Create: `src/AcDream.Core/Rendering/Wb/TextureHelpers.cs`
- Create: `src/AcDream.Core/Rendering/Wb/SceneryHelpers.cs`
- Create: `src/AcDream.Core/Rendering/Wb/TerrainUtils.cs`
- Create: `src/AcDream.Core/Rendering/Wb/TerrainEntry.cs`
- Create: `src/AcDream.Core/Rendering/Wb/CellSplitDirection.cs`
- Modify: `src/AcDream.Core/World/SceneryGenerator.cs` (2 `using` lines)
- Modify: `src/AcDream.Core/World/WbSceneryAdapter.cs` (1 `using` line)
- Modify: `src/AcDream.Core/Textures/SurfaceDecoder.cs` (1 `using` line)
- Modify: `tests/AcDream.Core.Tests/Textures/TextureDecodeConformanceTests.cs` (1 `using` line)
- Modify: `tests/AcDream.Core.Tests/Terrain/SplitFormulaDivergenceTest.cs` — actually this gets DELETED at T7, but its imports still need to work between now and then OR the test is moved to deleted-list earlier. Decision: leave it broken-but-doesn't-affect-other-tests for now, delete cleanly at T7.

- [ ] **Step 1: Read each of the 5 WB source files**

Read:
- `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/TextureHelpers.cs`
- `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SceneryHelpers.cs`
- `references/WorldBuilder/WorldBuilder.Shared/Modules/Landscape/Lib/TerrainUtils.cs`
- `references/WorldBuilder/WorldBuilder.Shared/Modules/Landscape/Models/TerrainEntry.cs`
- `references/WorldBuilder/WorldBuilder.Shared/Modules/Landscape/Models/CellSplitDirection.cs`

(Note: the worktree's `references/WorldBuilder` submodule is empty. Use the main checkout at `/c/Users/erikn/source/repos/acdream/references/WorldBuilder/` if needed.)

Confirm each file is what the audit expected. Note any unexpected dependencies (other WB types referenced beyond NuGet packages like Silk.NET, BCnEncoder.Net, MemoryPack, etc.).

- [ ] **Step 2: Copy `TextureHelpers.cs` to Core**

Copy `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/TextureHelpers.cs` to `src/AcDream.Core/Rendering/Wb/TextureHelpers.cs`. Then update the file:

Change the namespace declaration line from:
```csharp
namespace Chorizite.OpenGLSDLBackend.Lib;
```
to:
```csharp
namespace AcDream.Core.Rendering.Wb;
```

If TextureHelpers has any `using` statements pointing to other WB types, note them but DO NOT change yet — copy is verbatim except for the namespace.

- [ ] **Step 3: Copy `SceneryHelpers.cs` to Core (same process)**

Same as Step 2 for `SceneryHelpers.cs`. Update its namespace to `AcDream.Core.Rendering.Wb`.

If `SceneryHelpers.cs` has a `using WorldBuilder.Shared.Modules.Landscape.Lib;` or similar for `TerrainUtils`, leave it — we'll update it at Step 6.

- [ ] **Step 4: Copy `TerrainUtils.cs` to Core**

Same. Update namespace from `WorldBuilder.Shared.Modules.Landscape.Lib` → `AcDream.Core.Rendering.Wb`.

- [ ] **Step 5: Copy `TerrainEntry.cs` to Core + strip `[MemoryPackable]`**

Copy. Update namespace. Then **strip the `[MemoryPackable]` attribute** + any `[MemoryPack...]` attributes on fields:

```csharp
// BEFORE
[MemoryPackable]
public partial struct TerrainEntry { ... }

// AFTER
public struct TerrainEntry { ... }
```

Drop the `partial` modifier as well if the only reason it's `partial` is the MemoryPack source generator (verify by checking whether any other file extends the type via a separate `partial`).

Drop any `using MemoryPack;` line.

- [ ] **Step 6: Copy `CellSplitDirection.cs` to Core**

Enum. Update namespace. Nothing else.

- [ ] **Step 7: Update internal cross-references in the new files**

After all 5 files exist in `src/AcDream.Core/Rendering/Wb/`:

If `SceneryHelpers.cs` had `using WorldBuilder.Shared.Modules.Landscape.Lib;` (for `TerrainUtils`), change to nothing — they're in the same namespace now.

If `TerrainUtils.cs` had `using WorldBuilder.Shared.Modules.Landscape.Models;` (for `TerrainEntry` / `CellSplitDirection`), change to nothing — same namespace.

Run:
```
Grep pattern="using (WorldBuilder|Chorizite\.OpenGLSDLBackend)" path="src/AcDream.Core/Rendering/Wb"
```
Expected: no matches. If any, that file's still pointing at the old namespace; fix.

- [ ] **Step 8: Update our own Core source files' `using` lines**

Three Core files import WB types directly:

In `src/AcDream.Core/World/WbSceneryAdapter.cs:2`:
```csharp
// BEFORE
using WorldBuilder.Shared.Models;
// AFTER
using AcDream.Core.Rendering.Wb;
```

Wait — `WbSceneryAdapter.cs:2` actually says `using WorldBuilder.Shared.Models;` not `...Modules.Landscape.Models;`. Verify the actual `using` and update accordingly. The import is for `TerrainEntry`. After T2 step 6, `TerrainEntry` is in `AcDream.Core.Rendering.Wb`.

In `src/AcDream.Core/World/SceneryGenerator.cs:2` and `:6`:
```csharp
// BEFORE
using Chorizite.OpenGLSDLBackend.Lib;
using WorldBuilder.Shared.Modules.Landscape.Lib;
// AFTER
using AcDream.Core.Rendering.Wb;
// (single using replaces both, since both target types now live in the new namespace)
```

In `src/AcDream.Core/Textures/SurfaceDecoder.cs:3`:
```csharp
// BEFORE
using Chorizite.OpenGLSDLBackend.Lib;
// AFTER
using AcDream.Core.Rendering.Wb;
```

- [ ] **Step 9: Update conformance test's `using`**

In `tests/AcDream.Core.Tests/Textures/TextureDecodeConformanceTests.cs:1`:
```csharp
// BEFORE
using Chorizite.OpenGLSDLBackend.Lib;
// AFTER
using AcDream.Core.Rendering.Wb;
```

Leave `tests/AcDream.Core.Tests/Terrain/SplitFormulaDivergenceTest.cs` alone for now — it's deleted at T7.

- [ ] **Step 10: Build solution; expect green (or only the SplitFormulaDivergenceTest broken)**

Run:
```
dotnet build
```
Expected: green. The `SplitFormulaDivergenceTest.cs` still has its `using WbTerrainUtils = WorldBuilder.Shared.Modules.Landscape.Lib.TerrainUtils;` alias — but `WorldBuilder.Shared` is still project-referenced at this point, so it still compiles. If anything else fails, the namespace migration missed a file. Fix and rerun.

- [ ] **Step 11: Run tests; expect green**

Run:
```
dotnet test --no-build
```
Expected: green. Pay particular attention to `TextureDecodeConformanceTests` — those should still pass byte-for-byte after the namespace move.

- [ ] **Step 12: Commit T5**

```powershell
git add src/AcDream.Core/Rendering/Wb/ src/AcDream.Core/World/WbSceneryAdapter.cs src/AcDream.Core/World/SceneryGenerator.cs src/AcDream.Core/Textures/SurfaceDecoder.cs tests/AcDream.Core.Tests/Textures/TextureDecodeConformanceTests.cs
git commit -m @'
feat(O-T5): extract pure stateless helpers to AcDream.Core.Rendering.Wb

Verbatim copy of 5 WorldBuilder files into src/AcDream.Core/Rendering/Wb/:
- TextureHelpers.cs (pixel-format decoders, Chorizite Lib)
- SceneryHelpers.cs (scenery transforms, Chorizite Lib)
- TerrainUtils.cs, TerrainEntry.cs, CellSplitDirection.cs (WB.Shared Landscape)

Namespace migrated from WorldBuilder.* / Chorizite.OpenGLSDLBackend.Lib
to AcDream.Core.Rendering.Wb per O-D11. [MemoryPackable] stripped from
TerrainEntry per O-D10 (we don't serialize the struct).

Updated 3 source files + 1 test file to import from the new namespace.

Verbatim discipline (O-D1): only namespace + MemoryPack attribute changed.
All algorithm bodies byte-identical to upstream.

Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
'@
```

---

## Task 3: Extract GL infrastructure to App (was O-T3)

**Files:**
- Create: 11+ files under `src/AcDream.App/Rendering/Wb/` (exact count after closure walk)
- Modify: none yet (the existing App `Wb/` adapter files don't import GL infra directly — they go through Silk.NET — but `WbMeshAdapter.cs` constructs `OpenGLGraphicsDevice` which it currently imports from `Chorizite.OpenGLSDLBackend`. That single import will be updated at T4 when `ObjectMeshManager` arrives.)

- [ ] **Step 1: Walk the GL-infra closure**

Read `references/WorldBuilder/Chorizite.OpenGLSDLBackend/OpenGLGraphicsDevice.cs` first. List every WB type it references (look at field types, method parameters, constructor calls, `using` statements).

Then read each referenced WB type. Continue until closure is closed.

Expected closure (per audit):
- `OpenGLGraphicsDevice.cs`
- `GLSLShader.cs`, `GLHelpers.cs`, `GLStateScope.cs` (likely in `Lib/`)
- `ManagedGLTexture.cs`, `ManagedGLTextureArray.cs`, `ManagedGLVertexBuffer.cs`, `ManagedGLIndexBuffer.cs`, `ManagedGLVertexArray.cs`, `ManagedGLFrameBuffer.cs`, `ManagedGLUniformBuffer.cs` (root or `Lib/`)
- `EmbeddedResourceReader.cs`, `TextureFormatExtensions.cs`, `BufferUsageExtensions.cs` (3 internals to promote)
- `DebugRenderSettings.cs` (sometimes needed by `OpenGLGraphicsDevice` ctor)

Write down the actual file list with paths.

- [ ] **Step 2: Check for `SixLabors.ImageSharp` reachability**

Run:
```
Grep pattern="SixLabors\.ImageSharp" path="references/WorldBuilder/Chorizite.OpenGLSDLBackend"
```
List every file in the closure that imports ImageSharp.

If empty: document "ImageSharp not reachable" in the T3 commit message. Skip step 3.

If non-empty: proceed to step 3.

- [ ] **Step 3: Strip ImageSharp usage from the closure files (only if needed)**

For each closure file that imports ImageSharp:
- Identify the call sites (typically PNG/JPG load helpers).
- Replace with our existing byte-handling or BCnEncoder calls.
- Remove the `using SixLabors.ImageSharp;` line.

If a file's ImageSharp usage cannot be cleanly removed (e.g., it's the file's main purpose), then either: (a) drop the file from the extraction (verify it's not called by our closure), or (b) add `SixLabors.ImageSharp` as a `<PackageReference>` to `AcDream.App.csproj` and keep the file.

Document the decision in the T3 commit.

- [ ] **Step 4: Copy each GL-infra file to `src/AcDream.App/Rendering/Wb/`**

For each file in the closure list:
- Copy verbatim from WB source to `src/AcDream.App/Rendering/Wb/<filename>`.
- Update the namespace from `Chorizite.OpenGLSDLBackend` (or `.Lib`) to `AcDream.App.Rendering.Wb`.
- Update any `using Chorizite.OpenGLSDLBackend.Lib;` / `using Chorizite.OpenGLSDLBackend;` lines inside the copied files to point at the new namespace.
- For the 3 internal types (`EmbeddedResourceReader`, `TextureFormatExtensions`, `BufferUsageExtensions`): change `internal class` / `internal static class` / etc. to `public class` / `public static class`.

Suggested copy order (each follows the same pattern): managed wrappers first (lowest deps), then helpers, then `OpenGLGraphicsDevice` last (depends on all the rest).

- [ ] **Step 5: Build, iterate until green**

Run:
```
dotnet build
```

Common failures + fixes:
- "The type or namespace name 'X' could not be found" → `using` was missed; add `using AcDream.App.Rendering.Wb;`. OR a transitive WB type wasn't copied — copy it.
- "The type 'X' is defined in an assembly that is not referenced" → ImageSharp/MemoryPack/other NuGet — see step 3.
- "Inconsistent accessibility" → an internal type isn't yet promoted to public — promote it.

Iterate until green.

- [ ] **Step 6: Run tests; expect green (no test count change)**

Run:
```
dotnet test --no-build
```

Tests should pass; this task doesn't change behavior, only file location and namespace.

- [ ] **Step 7: Commit T3**

```powershell
git add src/AcDream.App/Rendering/Wb/
git commit -m @'
feat(O-T3): extract GL infrastructure to AcDream.App.Rendering.Wb

Verbatim copy of GL infra (~15 files) from
Chorizite.OpenGLSDLBackend into src/AcDream.App/Rendering/Wb/:
- OpenGLGraphicsDevice.cs, GLSLShader.cs, GLHelpers.cs, GLStateScope.cs
- ManagedGL{Texture,TextureArray,VertexBuffer,IndexBuffer,
  VertexArray,FrameBuffer,UniformBuffer}.cs (7 files)
- DebugRenderSettings.cs

Three internal types promoted to public per O-D9:
- EmbeddedResourceReader, TextureFormatExtensions, BufferUsageExtensions

SixLabors.ImageSharp reachability: <fill in: "not reachable" OR
"stripped from N paths" OR "added as NuGet PackageReference">.

Verbatim discipline (O-D1): namespace + access modifier changes only.
All algorithm bodies byte-identical to upstream.

Build green; tests green (no count change).

Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
'@
```

---

## Task 4: Extract mesh pipeline + dat refactor (was O-T4)

The load-bearing task. Includes the `ObjectMeshManager` refactor or fallback to adapter, per O-D7.

**Files:**
- Create: `src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs`
- Create: `src/AcDream.App/Rendering/Wb/ObjectRenderBatch.cs`
- Create: `src/AcDream.App/Rendering/Wb/ObjectRenderData.cs`
- Create: ~5+ supporting files (particle batcher / emitter, global mesh buffer, modern render data — count + exact filenames during closure walk)
- Modify: `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs` (constructor + `using` updates; cleanup at T7)

- [ ] **Step 1: Walk the mesh-pipeline closure**

Read `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ObjectMeshManager.cs` end-to-end. It's ~2K LOC; use Read with offsets if needed. List every WB type it references that ISN'T already in the GL-infra closure (T3) or stateless-helpers closure (T2).

Expected new types in closure (per audit):
- `ObjectRenderBatch`, `ObjectRenderData`
- `GlobalMeshBuffer` (or similar; the single-VBO/IBO holder for modern rendering)
- `ParticleBatcher`, `ParticleEmitterRenderer` (referenced from mesh path)
- Modern render data structs (likely a couple of small files)

Write the actual file list with paths.

- [ ] **Step 2: Count the `_dats.X` call sites in `ObjectMeshManager.cs`**

Run:
```
Grep pattern="_dats\." path="references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ObjectMeshManager.cs"
```
Count the matches.

**Branch on count:**
- **≤ 20 sites**: take the refactor path (O-D7). Plan steps 3-7 cover this.
- **> 20 sites**: take the adapter fallback. Plan steps 3-7 are skipped; instead, plan steps A-D below (after step 7).

Document the actual count in the T4 commit message regardless.

- [ ] **Step 3: (refactor path) Copy mesh-pipeline files to App, namespace-update**

Copy each closure file to `src/AcDream.App/Rendering/Wb/<filename>`. Update each file's namespace from `Chorizite.OpenGLSDLBackend.Lib` → `AcDream.App.Rendering.Wb`. Update any internal `using` lines.

Do NOT yet modify `ObjectMeshManager`'s dat type — that's the next step. First, the verbatim copy + namespace.

- [ ] **Step 4: (refactor path) Refactor `ObjectMeshManager` dat field type**

In the newly-copied `src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs`:

a) Drop the import:
```csharp
// BEFORE
using WorldBuilder.Shared.Services;
```
(plus any related; remove if present.)

b) Add the import:
```csharp
using AcDream.Core;
// or wherever DatCollection lives — verify with:
//   Grep pattern="^namespace.*DatCollection" path="src/AcDream.Core"
// or:
//   Grep pattern="public.*class DatCollection\b" path="src/AcDream.Core"
```

c) Change the field declaration from:
```csharp
private readonly IDatReaderWriter _dats;
```
to:
```csharp
private readonly DatCollection _dats;
```

d) Change the constructor parameter from `IDatReaderWriter dats` to `DatCollection dats`.

e) Update each `_dats.X` call site. The most common shapes:
- `_dats.Portal.TryGet<T>(id, out var x)` — verify DatCollection exposes `Portal.TryGet<T>`. If yes, no change. If no, see step 5.
- `_dats.Cell.TryGet<T>(id, out var x)` — verify the same.
- `_dats.HighRes.TryGet<T>(...)` — same.
- `_dats.Language.TryGet<T>(...)` — same.
- `_dats.ResolveId(id)` — drop the call site if it's diagnostic-only (per O-D12). If it's load-bearing, see step 5.

For each call site, either: (a) leave it alone (DatCollection has the equivalent), (b) replace with DatCollection's API shape, or (c) escalate to step 5 (DatCollection needs a new method).

- [ ] **Step 5: (refactor path) Add missing methods to DatCollection if needed**

If step 4 surfaced a call shape DatCollection doesn't support, add the method to `src/AcDream.Core/Dats/DatCollection.cs` (or wherever DatCollection lives — find via grep).

DO NOT add methods speculatively — only add what `ObjectMeshManager` actually calls. Document each added method in the T4 commit.

- [ ] **Step 6: (refactor path) Update `WbMeshAdapter` to construct `ObjectMeshManager` with `DatCollection`**

In `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs`:

Update line 7-15 `using` lines: drop `using Chorizite.OpenGLSDLBackend;`, `using Chorizite.OpenGLSDLBackend.Lib;`, `using WorldBuilder.Shared.Models;`, `using WorldBuilder.Shared.Services;`. Add `using AcDream.App.Rendering.Wb;` (and `AcDream.Core.Rendering.Wb;` if needed for any of the now-Core types referenced).

Update line 85-88 ObjectMeshManager construction. Today:
```csharp
_meshManager = new ObjectMeshManager(
    _graphicsDevice,
    _wbDats,
    new ConsoleErrorLogger<ObjectMeshManager>());
```

Change to:
```csharp
_meshManager = new ObjectMeshManager(
    _graphicsDevice,
    dats,  // the DatCollection passed to WbMeshAdapter's ctor
    new ConsoleErrorLogger<ObjectMeshManager>());
```

Leave `_wbDats` field + its construction at line 79 + its disposal at line 346 in place for now — they're cleaned up at T7.

Do NOT touch the `[indoor-upload] NULL_RESULT` block (lines 192-263) yet — also cleaned up at T7.

- [ ] **Step 7: (refactor path) Build, iterate until green**

Run:
```
dotnet build
```
Common failures:
- `_dats.X` call shape doesn't match DatCollection — fix per step 4/5.
- Missing `using` for a moved type — add.

Iterate until green.

- [ ] **Step A: (adapter fallback path, only if count > 20)**

Create `src/AcDream.App/Rendering/Wb/DatCollectionAdapter.cs`:

```csharp
using AcDream.Core;  // or wherever DatCollection lives
using DatReaderWriter;
using DatReaderWriter.DBObjs;
// ... add interfaces / types needed to satisfy IDatReaderWriter

namespace AcDream.App.Rendering.Wb;

/// <summary>
/// Adapts our DatCollection to WB's IDatReaderWriter interface so
/// the extracted ObjectMeshManager can consume it without internal
/// refactoring. O-D7 fallback path — taken because ObjectMeshManager
/// has >20 _dats.X call sites and the refactor was deemed too invasive
/// for one ship-window.
/// </summary>
internal sealed class DatCollectionAdapter : IDatReaderWriter
{
    private readonly DatCollection _dats;

    public DatCollectionAdapter(DatCollection dats)
    {
        ArgumentNullException.ThrowIfNull(dats);
        _dats = dats;
    }

    // Implement each member of IDatReaderWriter by delegating to _dats.
    // Members are determined by reading IDatReaderWriter.cs (which is
    // also copied into this directory at this step, since we no longer
    // have it via the WorldBuilder.Shared project reference).
    // ...
}
```

You also need to copy `IDatReaderWriter.cs` from WB.Shared into `src/AcDream.App/Rendering/Wb/` (or define a slimmed-down version with only the members ObjectMeshManager uses). Document the choice.

- [ ] **Step B: (adapter fallback) Wire the adapter in WbMeshAdapter**

```csharp
// BEFORE (in WbMeshAdapter ctor)
_meshManager = new ObjectMeshManager(_graphicsDevice, _wbDats, ...);

// AFTER
_meshManager = new ObjectMeshManager(
    _graphicsDevice,
    new DatCollectionAdapter(dats),
    ...);
```

- [ ] **Step C: (adapter fallback) Build, iterate until green**

Same as step 7 but with the adapter in place.

- [ ] **Step D: (adapter fallback) Document the choice**

Note in the T4 commit message that the adapter path was taken, with the actual call-site count and the rationale (e.g., "37 call sites; 22 use methods DatCollection doesn't currently expose, escalating to a separate refactor phase").

- [ ] **Step 8: (both paths) Run tests; expect green**

Run:
```
dotnet test --no-build
```
Expected: green. No test count regression.

- [ ] **Step 9: (both paths) Commit T4**

```powershell
git add src/AcDream.App/Rendering/Wb/
git add src/AcDream.Core/Dats/  # only if DatCollection grew methods at step 5
git commit -m @'
feat(O-T4): extract mesh pipeline; <refactor OR adapter> dat-shim

Verbatim copy of ObjectMeshManager + supports (~8 files, ~3.3K LOC) from
Chorizite.OpenGLSDLBackend.Lib into src/AcDream.App/Rendering/Wb/.

ObjectMeshManager._dats call-site count: <N>.
Path taken: <refactor in place to DatCollection> OR <thin DatCollectionAdapter>.
Rationale: <one or two sentences>.

DatCollection additions (if any): <list new methods + why>.

WbMeshAdapter updated to construct ObjectMeshManager via the new path.
The _wbDats field + [indoor-upload] NULL_RESULT block stay in place for
now; cleaned up at T7.

SixLabors.ImageSharp: <if any was reachable in this closure, document>.

Verbatim discipline (O-D1): namespace + dat-surface only. Mesh extraction,
texture decode, particle pipeline byte-identical to upstream.

Build green; tests green (no count change).

Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
'@
```

---

## Task 5: Drop project references + cleanup (was O-T7)

**Files:**
- Modify: `src/AcDream.App/AcDream.App.csproj`
- Modify: `src/AcDream.Core/AcDream.Core.csproj`
- Modify: `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs`
- Modify: `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (`using` line)
- Modify: doc-comment-only files in tests/ (5-6 files, comment edits)
- Delete: `tests/AcDream.Core.Tests/Terrain/SplitFormulaDivergenceTest.cs`

- [ ] **Step 1: Delete `_wbDats` field + construction from `WbMeshAdapter.cs`**

Open `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs`. Delete:

a) Line 34 (the field declaration):
```csharp
private readonly DefaultDatReaderWriter? _wbDats;
```

b) Line 79 (the construction):
```csharp
_wbDats = new DefaultDatReaderWriter(datDir);
```

c) Line 346 (the disposal in `Dispose()`):
```csharp
_wbDats?.Dispose();
```

d) The `datDir` parameter — verify it's no longer used after `_wbDats` removal. If `OpenGLGraphicsDevice` ctor doesn't need it, remove `datDir` from `WbMeshAdapter`'s constructor signature + all call sites. If it's still needed for something else, keep it.

- [ ] **Step 2: Delete the `[indoor-upload] NULL_RESULT` block**

In `WbMeshAdapter.cs`, delete the block from line ~192 to line ~263 (the diagnostic block that uses `_wbDats.ResolveId(...)`). Also delete the `_pendingEnvCellRequests` field and its associated logic in `Tick()` (the `isPendingEnvCell` block at ~305-321).

Result: `WbMeshAdapter.cs` shrinks substantially — should be down from 349 to ~250 LOC.

- [ ] **Step 3: Update `WbMeshAdapter.cs` and `WbDrawDispatcher.cs` `using` lines**

In `WbMeshAdapter.cs` (lines 7-15):
- Drop: `using Chorizite.OpenGLSDLBackend;`, `using Chorizite.OpenGLSDLBackend.Lib;`, `using WorldBuilder.Shared.Models;`, `using WorldBuilder.Shared.Services;`, `using DatReaderWriter;`, `using DatReaderWriter.DBObjs;` (verify whether the last two are still needed — they probably are for `GfxObj` references in `PopulateMetadata`).
- Add: `using AcDream.App.Rendering.Wb;` if not present.

In `WbDrawDispatcher.cs` (line 9):
- Drop: `using Chorizite.OpenGLSDLBackend.Lib;`
- Add: `using AcDream.App.Rendering.Wb;`

- [ ] **Step 4: Delete the `<ProjectReference>` lines in both csprojs**

In `src/AcDream.App/AcDream.App.csproj`, delete lines 38-39 (the two `<ProjectReference>` entries to `WorldBuilder.Shared` and `Chorizite.OpenGLSDLBackend`). Also drop the comment immediately above them ("Phase N.4 Task 9: ...") since the reference rationale is now historical.

In `src/AcDream.Core/AcDream.Core.csproj`, delete lines 27-28 (same two references). Also drop the multi-line comment immediately above ("Phase N: WorldBuilder is acdream's rendering + dat-handling base...") since the references are gone.

- [ ] **Step 5: Delete `SplitFormulaDivergenceTest.cs`**

```powershell
Remove-Item "tests/AcDream.Core.Tests/Terrain/SplitFormulaDivergenceTest.cs"
```

This was a one-time data-collection test; its job finished at N.5b ship.

- [ ] **Step 6: Fix doc-only WB mentions in remaining test files**

For each of these files, update comment-only references to WB to point at the extracted location instead. Files (per audit grep):
- `tests/AcDream.Core.Tests/World/WbSceneryAdapterTests.cs:10-17` — mentions `WorldBuilder.Shared.Models.TerrainEntry`. Update to `AcDream.Core.Rendering.Wb.TerrainEntry`.
- `tests/AcDream.Core.Tests/World/SceneryGeneratorTests.cs:9` — mentions WB helpers in comment. Update to refer to `AcDream.Core.Rendering.Wb`.
- `tests/AcDream.Core.Tests/Rendering/Wb/MeshExtractionConformanceTests.cs:12` — mentions `WB's ObjectMeshManager`. Either update to extracted name OR keep with a clarifying comment "(formerly WB; now ours, see Phase O)".
- `tests/AcDream.Core.Tests/Terrain/ClientReference.cs:11` and `ClientConformanceTests.cs:10` — both "Ported from WorldBuilder-ACME-Edition". These are historical attributions; leave them OR update phrasing.

This is comment-edit work; no logic changes.

- [ ] **Step 7: Build solution; expect green**

Run:
```
dotnet build
```

Expected: green. If a `using WorldBuilder.*` or `using Chorizite.OpenGLSDLBackend*` was missed, the build will fail with "namespace not found." Find it and update.

Also expect: if any file still references `IDatReaderWriter` / `DefaultDatReaderWriter` directly, the build fails. Grep:
```
Grep pattern="(IDatReaderWriter|DefaultDatReaderWriter)" path="src"
```
Should return zero matches.

- [ ] **Step 8: Run tests; expect green minus deleted test count**

Run:
```
dotnet test --no-build
```

Expected: green. The total test count drops by however many tests were in the deleted `SplitFormulaDivergenceTest.cs` (likely a single test or two — record the actual delta).

- [ ] **Step 9: Verify the acceptance criteria for reference deletion**

Run all three:
```
Grep pattern="(WorldBuilder|Chorizite\.OpenGLSDLBackend)" path="src/AcDream.App/AcDream.App.csproj"
Grep pattern="(WorldBuilder|Chorizite\.OpenGLSDLBackend)" path="src/AcDream.Core/AcDream.Core.csproj"
Grep pattern="^using (WorldBuilder|Chorizite\.OpenGLSDLBackend)" path="src"
```
Expected: all three return zero matches.

```
Grep pattern="DefaultDatReaderWriter" path="src"
```
Expected: zero matches.

If any return matches: something was missed in this task. Fix before commit.

- [ ] **Step 10: Commit T7**

```powershell
git add src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs src/AcDream.App/AcDream.App.csproj src/AcDream.Core/AcDream.Core.csproj tests/
git rm tests/AcDream.Core.Tests/Terrain/SplitFormulaDivergenceTest.cs
git commit -m @'
feat(O-T7): drop WB project references; cleanup _wbDats + diagnostic block

End of Phase O extraction:
- Dropped <ProjectReference> entries to WorldBuilder.Shared and
  Chorizite.OpenGLSDLBackend from AcDream.App.csproj and
  AcDream.Core.csproj.
- Deleted _wbDats field + ctor + dispose from WbMeshAdapter.cs.
- Deleted [indoor-upload] NULL_RESULT diagnostic block (lines 192-263);
  the Phase 2 cell-resolution diagnostic served its purpose, and its
  dependency on _wbDats.ResolveId goes away with this commit (per O-D12).
- Deleted SplitFormulaDivergenceTest.cs (one-time N.5b data-collection
  test; job done).
- Updated comment-only WB mentions in 5 test files to point at the
  extracted namespace.

Verified acceptance criteria:
- Zero refs to WorldBuilder.* / Chorizite.OpenGLSDLBackend.* in any csproj.
- Zero "using WorldBuilder.*" / "using Chorizite.OpenGLSDLBackend*" in src/.
- DefaultDatReaderWriter referenced in zero places.

Build green; tests green (count: -<N> from deleted test, no regressions).

Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
'@
```

---

## Task 6: Visual verification gate (was O-T8)

**Goal:** Confirm that the extraction didn't regress rendering by side-by-side with main against retail in three scenes.

**Files:** none modified; this is a runtime test.

- [ ] **Step 1: Capture pre-extraction screenshots (do this BEFORE Task 2 if not already done)**

If not already captured: pause this plan, switch back to `main` (or a commit at HEAD before Task 1), launch acdream at three scenes:
- Holtburg town (outdoor + scenery)
- An inn interior (EnvCell)
- A dungeon (portals)

Save screenshots to `docs/research/phase-o-pre/holtburg-town.png`, `inn-interior.png`, `dungeon.png` (not committed; reference only).

If you forgot to capture pre-screenshots before starting: you can either roll back to a pre-O commit (`git checkout <sha>` on a scratch branch), or compare visually against the retail client side-by-side instead.

- [ ] **Step 2: Launch acdream after Task 5 with the test character**

Use the launch script from CLAUDE.md (PowerShell):

```powershell
$env:ACDREAM_DAT_DIR   = "$env:USERPROFILE\Documents\Asheron's Call"
$env:ACDREAM_LIVE      = "1"
$env:ACDREAM_TEST_HOST = "127.0.0.1"
$env:ACDREAM_TEST_PORT = "9000"
$env:ACDREAM_TEST_USER = "testaccount"
$env:ACDREAM_TEST_PASS = "testpassword"
dotnet build  # must be green first
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 |
    Tee-Object -FilePath "launch-O-verification.log"
```

Run in the background per CLAUDE.md guidance.

- [ ] **Step 3: Visit the three scenes**

In the running client:
1. **Holtburg town** — walk around, check terrain blending, scenery placement (trees/bushes), building exteriors. Compare with pre-O screenshot.
2. **Inn interior** — enter the inn at Holtburg, look at EnvCell geometry (walls, floor, fireplace), check that statics render correctly.
3. **Dungeon** — find any portal (Holtburg Town network portal works for spawn-based visuals; for a real dungeon, walk to a dungeon entrance). Check portal visibility, room geometry.

User confirms each scene reads identical to pre-O.

- [ ] **Step 4: Measure resident memory at radius=4 + 50 entities visible**

In the running client:
- Set quality preset to Low (radius=4 — see CLAUDE.md "Quality Preset system").
- Wait for streaming to settle.
- Open Task Manager (Windows) → Details tab → find `AcDream.App.exe` → Working Set (Memory).
- Record the value.

Compare to a pre-O baseline (same scene, same settings, on `main` HEAD before Phase O). Expected delta: ≥50 MB reduction (validates that the second `DefaultDatReaderWriter` cache is actually gone).

If <50 MB: investigate why. The dat-reader duplication should be ~50-100 MB per `WbMeshAdapter.cs:27` comment.

- [ ] **Step 5: Close client gracefully and record findings**

Use the graceful close from CLAUDE.md (WM_CLOSE) so ACE's session clears quickly.

Write a brief verification note (in chat or in the T8 commit message):
- Three scenes: identical / different (describe difference).
- Memory delta: X MB reduction.
- User confirmation: yes / no.

Do NOT proceed to Task 7 if any scene regressed or memory delta is <50 MB.

- [ ] **Step 6: Tag the verification commit (or just push to next task)**

No code change at T8 — but optionally record the verification artifacts:

```powershell
git add docs/research/  # if you committed any post-O screenshots
git commit --allow-empty -m @'
chore(O-T8): visual verification passed

Three scenes side-by-side against pre-O baseline: identical.
- Holtburg town (outdoor + scenery): OK
- Holtburg inn interior (EnvCell): OK
- <Dungeon name> (portals): OK

Memory measurement at radius=4 + 50 entities visible:
- Pre-O: <X> MB working set
- Post-O: <Y> MB working set
- Delta: <Z> MB reduction (>= 50 MB target)

User confirmed identical rendering.

Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
'@
```

---

## Task 7: Ship (was O-T9)

**Files:**
- Modify: `CLAUDE.md` — rewrite "WB integration cribs" section.
- Modify: `docs/architecture/worldbuilder-inventory.md` — update ownership notes.
- Modify: `docs/plans/2026-04-11-roadmap.md` — move Phase O from "in flight" to "shipped".
- Modify: `docs/plans/2026-05-12-milestones.md` — note Phase O ship if appropriate.

- [ ] **Step 1: Rewrite the "WB integration cribs" section in CLAUDE.md**

Find the block in `CLAUDE.md` that starts with "**WorldBuilder is acdream's rendering + dat-handling base, integrated as of Phase N.4 ship (2026-05-08).**" and continues through the WB integration cribs (the bullet list of files and patterns).

Replace it with text that reflects post-O reality:

```markdown
**WorldBuilder code is extracted into acdream's source tree as of Phase
O ship (2026-XX-XX).** The pure helpers (texture decode, scenery
transforms, terrain math) live in `src/AcDream.Core/Rendering/Wb/`.
The GL-touching infrastructure and mesh pipeline live in
`src/AcDream.App/Rendering/Wb/`. There is no `<ProjectReference>` to
`WorldBuilder.Shared` or `Chorizite.OpenGLSDLBackend` in any csproj —
those projects remain in `references/WorldBuilder/` as read-references
only (grep against them when porting NEW pieces).

ALL dat reads go through `DatCollection` (Phase O ship: ONE thing
touches the DATs). The previous `DefaultDatReaderWriter` second-reader
allocation in `WbMeshAdapter` is deleted.

[update the file-path cribs below the header to point at the new
locations under `src/AcDream.{Core,App}/Rendering/Wb/`]
```

Preserve all of the substantive cribs about GL pipeline behavior, the
indirect-draw command struct, the surface-id rules, etc. — just retarget
the file paths and remove the "WB is a project reference" language.

- [ ] **Step 2: Update `docs/architecture/worldbuilder-inventory.md`**

Add a top-of-file banner indicating the inventory now describes "what we
extracted vs what we left in references/." Update the §1 status line.
The body inventory tables stay valuable (they describe the WB source we
might still grep). Add a column or section noting "Extracted into acdream
at Phase O" for each component that moved.

- [ ] **Step 3: Update the roadmap**

In `docs/plans/2026-04-11-roadmap.md`, find Phase O in the "active" /
"in flight" section. Move it to "shipped" with a one-line summary:

```
Phase O — DatPath Unification (2026-XX-XX, <commit-sha>): extracted
~33 WB files / ~7.7K LOC into src/AcDream.{Core,App}/Rendering/Wb/.
ObjectMeshManager refactored to take DatCollection (<refactor or
adapter>). Memory reduction <X> MB. Reference: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md.
```

- [ ] **Step 4: Update milestones doc**

In `docs/plans/2026-05-12-milestones.md`, locate the M1.5 block. Phase O
pre-empted M1.5 — add a brief writeup that O has shipped and M1.5 work
can resume from the 2026-05-20 baseline.

- [ ] **Step 5: Build + test final**

Run:
```
dotnet build
dotnet test
```
Expected: both green.

- [ ] **Step 6: Final commit + Phase O ship marker**

```powershell
git add CLAUDE.md docs/architecture/worldbuilder-inventory.md docs/plans/2026-04-11-roadmap.md docs/plans/2026-05-12-milestones.md
git commit -m @'
ship(O): Phase O — DatPath Unification — SHIPPED

ONE thing touches the DATs. WB code lives in our repo:
- src/AcDream.Core/Rendering/Wb/ — pure helpers (5 files, ~782 LOC)
- src/AcDream.App/Rendering/Wb/ — GL infra + mesh pipeline (~25 files, ~7K LOC)

Project references to WorldBuilder.Shared + Chorizite.OpenGLSDLBackend
dropped from AcDream.App.csproj and AcDream.Core.csproj. references/
WorldBuilder remains in-tree as read-reference.

DefaultDatReaderWriter removed; DatCollection is the only dat reader.
Working-set memory delta: <Z> MB reduction at radius=4 + 50 entities.

Visual side-by-side passed: Holtburg town, inn interior, dungeon.

Updates:
- CLAUDE.md: rewrote WB integration cribs to point at extracted locations.
- docs/architecture/worldbuilder-inventory.md: ownership now ours.
- Roadmap: moved Phase O to "shipped".
- Milestones: M1.5 resumes from 2026-05-20 baseline.

Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
'@
```

- [ ] **Step 7: Merge to main**

Per spec O-D6 (one ship-window). Open a PR (or fast-forward merge if the
worktree branch is clean) targeting main. Use `gh pr create` per
CLAUDE.md if a PR is desired; or:

```powershell
git checkout main
git merge claude/serene-leakey-e6b8bc --ff-only
git push
```

Verify with:
```
git status
git log -10
```

---

## Self-review notes

Plan reviewed against spec sections 4, 5, 6:

**Spec coverage:**
- §4.1 (mesh pipeline) → Task 4. ✓
- §4.2 (GL infra) → Task 3. ✓
- §4.3 (stateless helpers) → Task 2. ✓
- §4.4 (NOT extracted) → no task needed (these are explicitly skipped).
- §4.5 (drops) → Task 5. ✓
- §5 task breakdown T2/T3/T4/T5/T7/T8/T9 → plan Tasks 1/3/4/2/5/6/7. ✓
- §6 acceptance criteria → checked at Task 5 step 9 + Task 6 step 4 + Task 7. ✓
- §7 risks → mitigations baked into task steps (T4 safety check at step 2; visual screenshots before Task 1 / Task 6 step 1). ✓

**Placeholder scan:**
- "[Insert full MIT license body here...]" in Task 1 step 3 — intentional, requires reading the actual LICENSE file at step 4. Acceptable since step 4 explicitly tells the executor to fetch the content.
- "exact filenames during closure walk" in Task 3 + Task 4 file-list — intentional honesty since the audit didn't persist the full per-file table. The closure-walk steps cover producing the exact list.
- "<fill in: ...>" in commit message templates — intentional; commit messages parameterize on findings (e.g., ImageSharp reachability) discovered during the task.

**Type consistency:**
- `_dats` field name preserved across tasks.
- `DatCollection` class name verified — see Task 4 step 4b (executor confirms via grep).
- Namespace conventions: `AcDream.Core.Rendering.Wb` for Core helpers, `AcDream.App.Rendering.Wb` for App GL/mesh code. Consistent.

**Scope check:** Single ship-window per spec O-D6. Plan is one cohesive unit; no decomposition needed.
