# A6.P3 Slice 1 — Indoor ContactPlane retention (Finding 2 fix) 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:** Stop acdream's indoor-physics ContactPlane resynthesis blowup (A6.P2 Finding 2 — ~1,470× more CP writes than retail). Strip the per-frame synthesis path inside `Transition.FindEnvCollisions` indoor branch and rely on the existing Mechanism A (Path-6 land write) + Mechanism B (LKCP restore) for ContactPlane state.

**Architecture:** Make our indoor branch of `FindEnvCollisions` match retail's tiny `CEnvCell::find_env_collisions` (10 lines: just call `BSPTREE::find_collisions` and return state). The current branch calls `TryFindIndoorWalkablePlane` (a synthesis workaround) + `ValidateWalkable` (which writes CP) EVERY frame — that's the blowup. The body-level LKCP-restore already exists in `PhysicsEngine.RunTransitionResolve` (lines 668-674) and handles the cross-frame retention. We add per-transition Mechanism B (LKCP restore into `ci.ContactPlane` inside the transition resolver) so the indoor branch can return OK without writing CP and downstream consumers still see a valid CP.

**Tech Stack:** C# .NET 10, existing physics code in `src/AcDream.Core/Physics/`. Unit tests use xUnit (already wired). Integration verification uses the A6.P1 cdb probe infrastructure (already shipped).

**Spec:** [`docs/superpowers/specs/2026-05-21-phase-a6-indoor-physics-fidelity-design.md`](../specs/2026-05-21-phase-a6-indoor-physics-fidelity-design.md) §1.2 (hypothesis), §5 (A6.P3 fix surface).

**Findings:** [`docs/research/2026-05-21-a6-cdb-capture-findings.md`](../../research/2026-05-21-a6-cdb-capture-findings.md) Finding 2.

**Retail oracle:** `docs/research/named-retail/acclient_2013_pseudo_c.txt`:
- `CEnvCell::find_env_collisions` line 309573 — the 10-line indoor branch shape.
- `COLLISIONINFO::set_contact_plane` line 271925 — the CP setter.
- LKCP restore inside `validate_transition` family — line 272565-272582 (restore CP from `last_known_contact_plane` when sphere is geometrically close).
- `frames_stationary_fall` flat-CP synthesis — line 272622+ (Mechanism C; deferred to A6.P3 slice 2).

**Out of scope (deferred to A6.P3 slice 2):**
- Mechanism C (`frames_stationary_fall` counter + flat CP synthesis after 2+ stationary-falling frames). Add only if slice 1 visual verification shows first-frame fall-through after teleport / cell entry.
- Finding 3 (cell-resolver sling-out). Independent fix surface. Separate plan.
- Issue #95 (visibility blowup). Outside A6 scope.

**Acceptance for slice 1:**
- scen3 re-capture: acdream cp-write count drops from 86,748 to ≤ 200 (≤ retail BP7 + small idle buffer).
- scen1, scen5 re-captures: CP-write ratio drops from 1,000+× to ≤ 10×.
- Visual verification at Holtburg inn 2nd floor: walking feels solid (no falling, no jitter, no fall-through to outdoor terrain).
- `dotnet build` + `dotnet test` green (1147+8 baseline maintained).

---

## File Structure

| Path | Purpose | Change |
|---|---|---|
| `src/AcDream.Core/Physics/TransitionTypes.cs` | `Transition` class — indoor BSP branch of `FindEnvCollisions` is the blowup site (lines 1514-1777). `TryFindIndoorWalkablePlane` (lines 1294-1380) is the synthesis workaround. | Modify — strip synthesis from `FindEnvCollisions` indoor branch; leave `TryFindIndoorWalkablePlane` definition in place (deleted in A6.P4) |
| `src/AcDream.Core/Physics/PhysicsEngine.cs` | `RunTransitionResolve` already has cross-frame LKCP restore (lines 668-674). Verify per-tick Mechanism B (LKCP restore into `ci.ContactPlane`) is wired or add it. | Read; modify if needed |
| `tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs` | New regression test asserting CP-write count stays low across multiple `FindTransitionalPosition` calls on the same flat plane. | Create |
| `docs/research/2026-05-21-a6-p3-slice1-retail-mech-b-research.md` | Short research note grounding the fix in retail's exact LKCP-restore pattern. Mandatory before code changes. | Create |
| `docs/research/2026-05-21-a6-captures/scen1-recap/` etc | Re-capture directories for verification. | Create dirs (already part of capture protocol) |

---

## Task Decomposition

### Task 1: Research note — retail Mechanism B + how `FindEnvCollisions` returns OK without writing CP

This is non-negotiable. The current synthesis path was added to fix a real bug (fall-through to outdoor terrain at the inn doorway). Removing it without understanding retail's equivalent retention pattern will re-introduce that bug. Read retail's flow and document the exact path before changing code.

**Files:**
- Create: `docs/research/2026-05-21-a6-p3-slice1-retail-mech-b-research.md`

- [ ] **Step 1: Read `CEnvCell::find_env_collisions` in retail decomp**

Run:
```bash
sed -n '309570,309600p' docs/research/named-retail/acclient_2013_pseudo_c.txt
```

Expected: see the 10-line function (already inspected in plan-write phase). Confirm: returns OK after `BSPTREE::find_collisions` returns OK; no `set_contact_plane` call inside `find_env_collisions`.

- [ ] **Step 2: Read retail's `validate_transition` LKCP-restore block**

Run:
```bash
sed -n '272540,272620p' docs/research/named-retail/acclient_2013_pseudo_c.txt
```

Expected: see the block where `last_known_contact_plane_valid != 0` triggers `COLLISIONINFO::set_contact_plane(&this->collision_info, &last_known_contact_plane, ...)`. This is the per-transition restore that closes the gap.

- [ ] **Step 3: Find which retail function contains the LKCP-restore**

Run:
```bash
awk 'NR<=272540 && /void __thiscall.*::/ {f=$0; ln=NR} NR>272540 {print ln, f; exit}' docs/research/named-retail/acclient_2013_pseudo_c.txt
```

Expected: print the most recent function header before line 272540. This identifies whether the restore is inside `validate_transition`, `find_obj_collisions`, `transitional_insert`, or another function.

- [ ] **Step 4: Find the equivalent in our code**

Search for our equivalent of that retail function name. Try `grep -n` for the C++ method name without the class prefix:

Run:
```bash
grep -rn "TransitionalInsert\|TransitionalInsertGround\|FindTransitionalPosition\|ValidateTransition" src/AcDream.Core/Physics/
```

Expected: identifies the C# method that should contain Mechanism B. Likely `Transition.FindTransitionalPosition` or `Transition.CheckTransition`.

- [ ] **Step 5: Write the research note**

```bash
# Open and write the file with the answers
```

Write `docs/research/2026-05-21-a6-p3-slice1-retail-mech-b-research.md` with these sections:

1. **`CEnvCell::find_env_collisions` shape** (paste the 10-line function with our line annotations).
2. **Retail Mechanism B location** (the function name + line number from Step 3).
3. **Retail Mechanism B trigger condition** (the geometric proximity check at line 272569 — `|dot(global_curr_center, LKCP.N) + LKCP.d| <= radius + 0.0002f`).
4. **Our equivalent function** (from Step 4).
5. **Decision: where to add Mechanism B in our code** — either inside `Transition.FindEnvCollisions` itself (if our equivalent isn't called per-transition) OR inside the per-transition resolver (if it is). Note the choice.
6. **Risk: first-frame fall-through** — what happens when LKCP is invalid AND BSP returns OK on the first frame in a cell (post-teleport or post-cell-cross). Either accept the risk (slice 2 adds Mechanism C) or document a slice-1 mitigation.

- [ ] **Step 6: Commit the research note**

```bash
git add docs/research/2026-05-21-a6-p3-slice1-retail-mech-b-research.md
git commit -m "docs(research): A6.P3 slice 1 — retail Mechanism B oracle for CP retention

Pre-fix research note grounding the indoor CP-retention refactor in
retail's exact LKCP-restore pattern (acclient_2013_pseudo_c.txt:272565-272582)
and CEnvCell::find_env_collisions tiny shape (line 309573).

Output of this note drives the per-transition Mechanism B insertion
point selection in Task 4 + the slice-1 acceptance shape.

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

---

### Task 2: Add CP-write count probe assertion infrastructure

We need a deterministic way to count CP writes from a unit test. Look for an existing counter; if none, add one inside `CollisionInfo.SetContactPlane`.

**Files:**
- Read: `src/AcDream.Core/Physics/TransitionTypes.cs:251-270` (the `SetContactPlane` setter)
- Modify (if needed): `src/AcDream.Core/Physics/TransitionTypes.cs` (add a static counter for tests)
- Test: `tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs` (created in Task 3)

- [ ] **Step 1: Read the current `SetContactPlane` setter**

```bash
sed -n '245,275p' src/AcDream.Core/Physics/TransitionTypes.cs
```

Expected: see the method signature `public void SetContactPlane(Plane plane, uint cellId, bool isWater = false)` setting `ContactPlane`, `ContactPlaneCellId`, `ContactPlaneIsWater`, plus updating LKCP fields.

- [ ] **Step 2: Check if a CP-write counter already exists**

```bash
grep -rn "ContactPlaneWriteCount\|CpWriteCount\|TotalContactPlaneWrites" src/AcDream.Core/Physics/
```

Expected: probably no result (counter doesn't exist). If a result appears, use that counter and skip step 3.

- [ ] **Step 3: Add a test-only counter to `CollisionInfo`**

In `src/AcDream.Core/Physics/TransitionTypes.cs`, inside the `CollisionInfo` class (around line 245), add:

```csharp
/// <summary>
/// Test-only counter for ContactPlane writes. Incremented by every
/// call to <see cref="SetContactPlane"/>. Used by
/// IndoorContactPlaneRetentionTests to assert that CP retention is
/// working (A6.P3 slice 1, 2026-05-21).
/// </summary>
internal int ContactPlaneWriteCount { get; private set; }
```

Then modify `SetContactPlane` (the existing method around line 251) to increment the counter. Find the existing line:

```csharp
public void SetContactPlane(Plane plane, uint cellId, bool isWater = false)
{
    ContactPlane = plane;
```

Insert before `ContactPlane = plane;`:

```csharp
    ContactPlaneWriteCount++;
    ContactPlane = plane;
```

- [ ] **Step 4: Build and verify**

```bash
dotnet build src/AcDream.Core/AcDream.Core.csproj -c Debug
```

Expected: `Build succeeded. 0 Warning(s). 0 Error(s).`

- [ ] **Step 5: Commit**

```bash
git add src/AcDream.Core/Physics/TransitionTypes.cs
git commit -m "test(phys): A6.P3 slice 1 — add CollisionInfo.ContactPlaneWriteCount

Internal test-only counter incremented by SetContactPlane. Required
by IndoorContactPlaneRetentionTests to assert CP retention works
post-Finding-2 fix (A6.P2).

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

---

### Task 3: Write the failing regression test

TDD step. Encode the expected post-fix behavior as a test that FAILS today and will PASS after the fix.

**Files:**
- Create: `tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs`

- [ ] **Step 1: Identify the test fixture pattern in the existing test project**

```bash
ls tests/AcDream.Core.Tests/Physics/ | head -20
```

Look for an existing test file using `Transition` directly — likely a `TransitionTests.cs` or `BSPQueryTests.cs`. Read it to learn the setup pattern (how to construct `Transition`, `SpherePath`, `CollisionInfo`, mock `PhysicsEngine`).

```bash
grep -l "new Transition\|Transition.*FindTransitionalPosition\|Transition.*FindEnvCollisions" tests/AcDream.Core.Tests/Physics/
```

Expected: one or more files; pick the most-similar test file as the template.

- [ ] **Step 2: Write the test file**

Create `tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs`:

```csharp
using System.Numerics;
using AcDream.Core.Physics;
using Xunit;
// Plus any using statements identified from the template file in Step 1.

namespace AcDream.Core.Tests.Physics;

/// <summary>
/// A6.P3 slice 1 (2026-05-21). Regression tests for Finding 2:
/// ContactPlane resynthesis blowup. Asserts that running multiple
/// FindEnvCollisions calls on the same indoor flat-floor configuration
/// does NOT cause an unbounded CP-write count.
/// </summary>
public class IndoorContactPlaneRetentionTests
{
    [Fact]
    public void IndoorFlatFloorWalk_DoesNotResynthesizeContactPlanePerFrame()
    {
        // Arrange: build a minimal indoor scenario.
        // - Create a Transition with a SpherePath positioned inside an
        //   indoor cell (CellId low 16 bits >= 0x0100).
        // - Mock a cell with a flat floor poly at Z=0.
        // - Seed the CollisionInfo's ContactPlane + LastKnownContactPlane
        //   with the floor plane (simulating "we've already touched the floor").
        // - Reset ContactPlaneWriteCount to 0 just before the test loop.
        //
        // Setup the Transition + SpherePath + CollisionInfo per the
        // template-test pattern identified in Step 1.

        var transition = /* ... build per template ... */;
        var ci = transition.CollisionInfo;

        // Seed: simulate "already on the floor"
        var floorPlane = new System.Numerics.Plane(new Vector3(0, 0, 1), 0f);
        ci.SetContactPlane(floorPlane, cellId: 0xA9B40166, isWater: false);

        // Reset write counter after seeding.
        int seededWrites = ci.ContactPlaneWriteCount;
        Assert.Equal(1, seededWrites);

        // Act: simulate 60 frames of flat-floor walking by calling
        // FindEnvCollisions 60 times with positions that stay on the same
        // flat plane (small horizontal deltas, identical Z).
        var engine = /* ... mock PhysicsEngine ... */;
        for (int frame = 0; frame < 60; frame++)
        {
            transition.SpherePath.SetCheckPos(
                new Position(/* same Z, small XY delta */),
                cellId: 0xA9B40166);

            var state = transition.FindEnvCollisions(engine);
            Assert.Equal(TransitionState.OK, state);
        }

        // Assert: after 60 frames of identical flat-floor walking, the
        // ContactPlane should have been written AT MOST a small constant
        // number of additional times (ideally 0; allow a small budget for
        // legitimate Mechanism A re-lands by BSP path-6).
        int totalWrites = ci.ContactPlaneWriteCount;
        int additionalWrites = totalWrites - seededWrites;

        // Threshold: 60 frames should produce at most ~5 additional
        // CP writes (ratio ≤ 0.1 writes/frame). Today's broken code
        // produces ~60-180 (1-3 writes/frame from per-frame synthesis).
        Assert.True(additionalWrites <= 5,
            $"Expected ≤5 additional CP writes across 60 flat-floor frames, "
            + $"got {additionalWrites}. Finding 2 fix not complete.");
    }
}
```

**Note:** The exact `Transition` construction depends on the template test from Step 1. If the template requires concrete `CellPhysics` + `DataCache`, replicate that setup. If the construction is too painful (requires mocking many fields), simplify the test by directly calling `Transition.FindEnvCollisions` and verifying ContactPlaneWriteCount stays low — even if the test setup is artificial, the assertion's job is to prove "we don't write CP on every frame."

- [ ] **Step 3: Build the test project**

```bash
dotnet build tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj -c Debug
```

Expected: build succeeds.

- [ ] **Step 4: Run the test — expect FAIL**

```bash
dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~IndoorContactPlaneRetentionTests" --no-build
```

Expected: test FAILS with message like "Expected ≤5 additional CP writes ... got 60+." This proves the test correctly captures the bug.

If the test PASSES today (no extra writes), the test setup doesn't exercise the indoor synthesis path. Revisit: either the SpherePath cell isn't in the indoor range (low 16 bits must be ≥ 0x0100), or the `engine.DataCache` mock isn't returning a cell with a BSP — meaning the indoor branch isn't entered.

- [ ] **Step 5: Commit the failing test**

```bash
git add tests/AcDream.Core.Tests/Physics/IndoorContactPlaneRetentionTests.cs
git commit -m "test(phys): A6.P3 slice 1 — failing regression for Finding 2 CP blowup

Test asserts 60 frames of indoor flat-floor walking should produce
≤5 ContactPlane writes. Fails today (broken code: ~60-180 writes).
Will pass after Task 4 + Task 5 strip the synthesis path.

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

---

### Task 4: Add Mechanism B (LKCP restore) per-transition

Based on Task 1's research note, add the LKCP-restore step in the correct location (per Step 5 of Task 1's research note). The shape should match retail's `validate_transition` lines 272565-272582 — when LKCP is valid AND the sphere is close to the LKCP plane, restore CP from LKCP.

**Files:**
- Modify: `src/AcDream.Core/Physics/TransitionTypes.cs` OR `src/AcDream.Core/Physics/PhysicsEngine.cs` (location determined by Task 1 Step 5)

- [ ] **Step 1: Re-read Task 1's research note**

```bash
cat docs/research/2026-05-21-a6-p3-slice1-retail-mech-b-research.md
```

Locate Section 5: "Decision: where to add Mechanism B in our code."

- [ ] **Step 2: Read the target function (the file + line range identified in Task 1 Step 5)**

Read the function in full to identify the correct insertion point. Mechanism B should fire AFTER the transition's sub-step loop completes with OK_TS but BEFORE the body persist.

- [ ] **Step 3: Write the Mechanism B insertion**

Add at the determined insertion point (replace `<INSERTION_POINT>` with the exact location from Step 2):

```csharp
// ── Mechanism B — restore CP from LKCP when geometrically close ──
// A6.P3 slice 1 (2026-05-21). Retail oracle:
// acclient_2013_pseudo_c.txt:272565-272582 (validate_transition's
// LKCP-restore block). When the player is moving across a flat
// indoor floor and FindEnvCollisions returns OK without writing
// a fresh CP (per the now-stripped synthesis path), restore CP
// from LastKnownContactPlane if the sphere is close to the LKCP
// plane geometrically.
if (ci.LastKnownContactPlaneValid && !ci.ContactPlaneValid)
{
    var sphereCenter = SpherePath.GlobalCurrCenter[0].Origin;
    var lkcp = ci.LastKnownContactPlane;
    float distToLKCP = MathF.Abs(
        Vector3.Dot(sphereCenter, lkcp.Normal) + lkcp.D);
    float threshold = SpherePath.GlobalSphere[0].Radius + 0.0002f;
    if (distToLKCP <= threshold)
    {
        ci.SetContactPlane(
            lkcp,
            ci.LastKnownContactPlaneCellId,
            ci.LastKnownContactPlaneIsWater);
    }
}
```

- [ ] **Step 4: Build**

```bash
dotnet build src/AcDream.Core/AcDream.Core.csproj -c Debug
```

Expected: build succeeds.

- [ ] **Step 5: Run the full Core test suite — should be 1147+ green**

```bash
dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build
```

Expected: still 1147+ pass. Mechanism B by itself should not break anything (it only fires when ContactPlane is invalid but LKCP is valid — a state that's rare in the current code, but harmless if it does happen).

If a previously-green test now fails: the Mechanism B logic is firing where it shouldn't. Inspect the failing test's setup to understand what state assumption it makes, then either narrow the Mechanism B condition or fix the test.

- [ ] **Step 6: Commit Mechanism B alone**

```bash
git add src/AcDream.Core/Physics/<file_modified>
git commit -m "feat(phys): A6.P3 slice 1 step 1 — add Mechanism B (LKCP restore)

Restores CollisionInfo.ContactPlane from LastKnownContactPlane when:
  - LKCP is valid
  - ContactPlane is currently invalid
  - sphere is geometrically close to the LKCP plane
    (|dot(center, N) + d| <= radius + 0.0002)

Matches retail's validate_transition LKCP-restore at
acclient_2013_pseudo_c.txt:272565-272582. Slice 1 step 1 of the
A6.P3 indoor CP retention fix. Step 2 (Task 5) strips the
TryFindIndoorWalkablePlane synthesis from FindEnvCollisions.

Tests pass: 1147+ green.

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

---

### Task 5: Strip the synthesis path from `FindEnvCollisions` indoor branch

The main fix. Match retail's `CEnvCell::find_env_collisions` tiny shape.

**Files:**
- Modify: `src/AcDream.Core/Physics/TransitionTypes.cs:1623-1737` (the synthesis block after BSP returns OK)

- [ ] **Step 1: Re-read the current indoor branch**

```bash
sed -n '1623,1745p' src/AcDream.Core/Physics/TransitionTypes.cs
```

Expected: see the block that calls `CheckOtherCells`, then `TryFindIndoorWalkablePlane`, then `ValidateWalkable` on the synthesized plane.

- [ ] **Step 2: Identify what to KEEP vs DELETE**

KEEP:
- The `cellState != TransitionState.OK` early return (lines 1623-1628). Retail's tiny version has this.
- The Phase A4 `CheckOtherCells` call (lines 1638-1642). This is multi-cell collision iteration — separate concern from CP retention; keep.
- All probe diagnostics (`[indoor-bsp]`, `[indoor-walkable]` lines). These print only when the env var is set; keep for A6.P3 verification re-captures.

DELETE:
- The `TryFindIndoorWalkablePlane` call (lines 1658-1662).
- The `if (walkableHit)` block calling `ValidateWalkable` (lines 1727-1737).
- The defensive comment about "fall through to outdoor terrain" — replaced by Mechanism B.
- The `[walk-miss]` diagnostic block (lines 1682-1725). This diagnostic was tied to the synthesis MISS case; with synthesis stripped, the diagnostic is meaningless. Move out of scope OR delete.

REPLACE with: `return TransitionState.OK;` immediately after `CheckOtherCells` returns OK.

- [ ] **Step 3: Apply the edit**

Replace the block (lines 1645-1743 approximately) with:

```csharp
                // ── Indoor walkable handling — A6.P3 slice 1 (2026-05-21) ─
                // Retail's CEnvCell::find_env_collisions (decomp
                // acclient_2013_pseudo_c.txt:309573) returns OK after
                // BSPTREE::find_collisions returns OK — NO call to
                // set_contact_plane or any synthesis. ContactPlane is
                // either:
                //   - Already valid from a previous frame's Path-6 land
                //     write inside BSPQuery.FindCollisions (Mechanism A).
                //   - Restored from LKCP by the per-transition Mechanism B
                //     in <function> (see Task 4).
                //
                // The old TryFindIndoorWalkablePlane synthesis path is
                // removed here; the function definition is retained for
                // now and is deleted in A6.P4 along with the #90
                // workaround.
                //
                // If subsequent visual verification shows first-frame
                // fall-through (LKCP invalid AND no Path-6 land happens
                // for a flat-walk-only scenario), A6.P3 slice 2 adds
                // Mechanism C (retail's frames_stationary_fall flat-CP
                // synthesis at acclient_2013_pseudo_c.txt:272622+).
                return TransitionState.OK;
            }
        }

        // ── Outdoor terrain collision ────────────────────────────────────
```

(Keep everything BEFORE `// ── Synthesize indoor walkable contact plane ──` and everything AFTER `// ── Outdoor terrain collision ──`.)

- [ ] **Step 4: Build**

```bash
dotnet build src/AcDream.Core/AcDream.Core.csproj -c Debug
```

Expected: build succeeds. If `walkableHit`, `indoorPlane`, `indoorVertices`, `hitPolyId`, `INDOOR_WALKABLE_PROBE_DISTANCE`, `WalkMissDiagnostic` are referenced by other code OUTSIDE this block, build will fail with "unused variable" warnings — those are now unused and the code outside this block should also not reference them; reach for `git grep`.

- [ ] **Step 5: Run the IndoorContactPlaneRetentionTests test — expect PASS now**

```bash
dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~IndoorContactPlaneRetentionTests" --no-build
```

Expected: test PASSES. The 60-frame flat-walk no longer writes CP per-frame because synthesis is gone, and Mechanism B (Task 4) keeps `ci.ContactPlane` valid via LKCP restore.

If test still fails: Mechanism B isn't firing in the test scenario. Debug by adding a `Console.WriteLine` inside the Mechanism B block to print whether it fires. Re-run test, inspect output.

- [ ] **Step 6: Run the FULL test suite — should be 1147+8 green**

```bash
dotnet test --no-build
```

Expected: full pass. Tests that touched the old indoor-walkable path (if any) may need updating; investigate failures individually. Common causes:
- A test that expected `[walk-miss]` diagnostic to fire — re-scope or remove.
- A test that called `TryFindIndoorWalkablePlane` directly — those tests are testing the deleted-in-A6.P4 workaround; remove them or skip.

- [ ] **Step 7: Commit the strip**

```bash
git add src/AcDream.Core/Physics/TransitionTypes.cs
# Any other modified test files from Step 6
git commit -m "fix(phys): A6.P3 slice 1 step 2 — strip indoor walkable synthesis

Closes A6.P2 Finding 2 (ContactPlane resynthesis blowup, 250x to ∞x
more CP writes than retail). Indoor branch of Transition.FindEnvCollisions
now matches retail's CEnvCell::find_env_collisions tiny shape (decomp
line 309573): call BSPTREE::find_collisions, return state. No
synthesis, no per-frame ValidateWalkable call, no per-frame
ContactPlane write.

Cross-frame CP retention now flows via:
  - Mechanism A: BSPQuery.FindCollisions Path-6 land write (already
    present, unchanged).
  - Mechanism B: per-transition LKCP restore (added in prior commit).
  - PhysicsEngine.RunTransitionResolve body persist (unchanged).

TryFindIndoorWalkablePlane definition retained for now; deleted in
A6.P4 alongside the #90 sphere-overlap workaround.

Verification:
- IndoorContactPlaneRetentionTests now passes.
- Full suite 1147+8 green maintained.
- Re-capture verification deferred to Task 6.

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

---

### Task 6: Re-capture scen3 and verify CP-write ratio drops

Integration verification. Re-run the A6.P1 capture protocol for scen3 (flat 2nd-floor walk, the cleanest CP-write-blowup signal) and confirm the ratio drops from ~86,748 to ≤200.

**Files:**
- Create: `docs/research/2026-05-21-a6-captures/scen3_inn_2nd_floor_postfix/` directory
- The acdream.log is the artifact; no code changes in this task.

- [ ] **Step 1: Confirm build is green**

```bash
dotnet build -c Debug 2>&1 | tail -5
```

Expected: `Build succeeded. 0 Warning(s). 0 Error(s).`

- [ ] **Step 2: User confirms retail is running, character on Holtburg inn 2nd floor (via stairs in retail), ready to walk**

Interactive coordination — wait for the user to confirm position.

- [ ] **Step 3: Run cdb capture for retail-postfix (optional — gives a fresh paired baseline)**

```powershell
.\tools\cdb\a6-probe-runner.ps1 -ScenarioTag "scen3_inn_2nd_floor_postfix"
```

Wait for "a6-probe v4 armed:" in `docs/research/2026-05-21-a6-captures/scen3_inn_2nd_floor_postfix/retail.log`.

User walks: forward 3 m, sidestep 1 m, walk back. Shuffle a bit at the end. Then close retail to release cdb.

- [ ] **Step 4: Decode retail.log**

```bash
py tools/cdb/decode_retail_hex.py docs/research/2026-05-21-a6-captures/scen3_inn_2nd_floor_postfix/retail.log
```

- [ ] **Step 5: Launch acdream with probe env vars + walk same scenario**

```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"
$env:ACDREAM_DEVTOOLS            = "1"
$env:ACDREAM_PROBE_PUSH_BACK     = "1"
$env:ACDREAM_PROBE_INDOOR_BSP    = "1"
$env:ACDREAM_PROBE_CELL          = "1"
$env:ACDREAM_PROBE_CELL_CACHE    = "1"
$env:ACDREAM_PROBE_CONTACT_PLANE = "1"
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 |
    Out-File -FilePath "docs\research\2026-05-21-a6-captures\scen3_inn_2nd_floor_postfix\acdream.log" -Encoding ASCII
```

User teleports +Acdream to the inn 2nd floor (via `@teleport` per the original scen3 capture protocol), walks the same scenario, closes gracefully.

- [ ] **Step 6: Compare CP-write counts**

```bash
D="docs/research/2026-05-21-a6-captures/scen3_inn_2nd_floor_postfix"
echo "--- retail BP7 (set_contact_plane) ---"
grep -c "^\[BP7\]" "$D/retail.decoded.log"
echo "--- acdream cp-write ---"
grep -c "^\[cp-write\]" "$D/acdream.log"
echo "--- prefix vs postfix acdream ---"
echo "scen3 prefix: 86,748 cp-writes (committed at 4b5aebc)"
echo "scen3 postfix: $(grep -c "^\[cp-write\]" "$D/acdream.log") cp-writes"
```

Expected: acdream cp-write count ≤ 200 (the success threshold). If still in the thousands, Finding 2 fix is INCOMPLETE — diagnose:
- Is Mechanism B firing? Add a probe inside the Mechanism B `if` block, re-launch, check.
- Is some OTHER write site still firing? Grep `git log -p src/AcDream.Core/Physics/` for `SetContactPlane` calls not yet audited.

- [ ] **Step 7: Visual verification with user**

User walks +Acdream on the inn 2nd floor for ~30 seconds in various patterns:
- Walk back and forth.
- Stand still for a few seconds.
- Walk into a wall.
- Walk into furniture.

User reports: does the player feel solid (no falling, no jitter, no fall-through to outdoor terrain)?

If user reports a regression: ROLLBACK the commit from Task 5 and either:
- Add Mechanism C (frames_stationary_fall synthesis) per slice 2, then retry; OR
- Re-scope: keep TryFindIndoorWalkablePlane gated on `!ci.LastKnownContactPlaneValid` (synthesis only on first frame in cell), keep the rest of the fix.

- [ ] **Step 8: Commit the verification capture**

```bash
git add docs/research/2026-05-21-a6-captures/scen3_inn_2nd_floor_postfix/
git commit -m "capture(research): A6.P3 slice 1 — scen3 post-fix verification

Re-capture of scen3 (Holtburg inn 2nd floor, flat-floor walk) after
the A6.P3 slice 1 fix. CP-write ratio:

  scen3 pre-fix (4b5aebc):  retail BP7 = 0, acdream cp-write = 86,748
  scen3 post-fix:           retail BP7 = N, acdream cp-write = M

[Fill in N + M from Step 6 output.]

Visual verification with user at the inn 2nd floor: [PASS/FAIL/notes].

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

---

### Task 7: Re-capture scen1 and scen5 for full slice 1 sign-off

Two more re-captures to ensure the fix generalizes (no regressions in scenarios where retail DID write CP). scen2 stair-fail + scen4 sling-out are intentionally OUT of slice 1 scope (those are Finding 3 territory).

**Files:**
- Create: `docs/research/2026-05-21-a6-captures/scen1_inn_doorway_postfix/`
- Create: `docs/research/2026-05-21-a6-captures/scen5_sewer_entry_postfix/`

- [ ] **Step 1: Re-capture scen1 (doorway walk-through)**

Apply the same protocol as Task 6 Steps 2-6, with `-ScenarioTag "scen1_inn_doorway_postfix"`. Walk script: walk forward through inn front door, stop just inside.

Expected: cp-write count drops from 73,304 to a small multiple of retail's BP7 (18 hits) — target ≤ ~200.

- [ ] **Step 2: Re-capture scen5 (Town Network portal entry)**

Apply the same protocol with `-ScenarioTag "scen5_sewer_entry_postfix"`. Walk script: walk to Town Network Portal, enter, walk 2m inside.

Expected: cp-write count drops from 20,956 to a small multiple of retail's BP7 (65 hits) — target ≤ ~500 (portal threshold + indoor hub walking is naturally more CP-active than flat 2nd-floor walking).

- [ ] **Step 3: Commit both verifications**

```bash
git add docs/research/2026-05-21-a6-captures/scen1_inn_doorway_postfix/
git add docs/research/2026-05-21-a6-captures/scen5_sewer_entry_postfix/
git commit -m "capture(research): A6.P3 slice 1 — scen1 + scen5 post-fix verification

scen1 pre-fix vs post-fix CP-write ratio:
  retail BP7: 18
  acdream cp-write: 73,304 -> N (ratio ~4072x -> ~Nx)

scen5 pre-fix vs post-fix CP-write ratio:
  retail BP7: 65
  acdream cp-write: 20,956 -> M (ratio ~322x -> ~Mx)

[Fill in N + M from re-capture decodes.]

A6.P3 slice 1 acceptance threshold (CP-write ratio ≤ ~10x) met
across all three flat-floor + portal-walk scenarios.

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

---

### Task 8: Update findings doc + roadmap; mark A6.P3 slice 1 SHIPPED

Bookkeeping. Updates the A6.P2 findings doc with the post-fix data and the roadmap with the slice-1 ship.

**Files:**
- Modify: `docs/research/2026-05-21-a6-cdb-capture-findings.md`
- Modify: `docs/plans/2026-04-11-roadmap.md`
- Modify: `CLAUDE.md` (Currently-working-toward block)

- [ ] **Step 1: Append a "post-fix" section to the findings doc**

In `docs/research/2026-05-21-a6-cdb-capture-findings.md`, add a new section after the existing "Findings" section:

```markdown
## A6.P3 slice 1 — Finding 2 closed (2026-MM-DD)

[Update date when this lands.]

Strip + Mechanism B fix shipped at commits [list]. Re-capture verification:

| Scenario | Pre-fix cp-write | Post-fix cp-write | Pre-fix ratio | Post-fix ratio |
|---|---:|---:|---:|---:|
| scen1 inn doorway | 73,304 | N | 4,072x | Nx |
| scen3 inn 2nd floor | 86,748 | M | ∞ | Mx |
| scen5 town network | 20,956 | K | 322x | Kx |

[Fill in N, M, K from Task 6 + Task 7 outputs.]

Finding 2 closed. Finding 1 dispatcher entry frequency mismatch
[status — closed as side effect / still wide / TBD per re-capture].

Next: Finding 3 (cell-resolver sling-out from scen4). Separate plan
when A6.P3 slice 2 is scoped.
```

- [ ] **Step 2: Update the roadmap A6.P3 entry**

In `docs/plans/2026-04-11-roadmap.md` find the `- **A6.P3 — Fix the BSP correction paths**` line and update to show slice 1 SHIPPED with the commits.

- [ ] **Step 3: Update CLAUDE.md Currently-working-toward block**

In `CLAUDE.md` find the M1.5 + A6.P3 block. Update the "Current phase" line to reflect slice 1 ship + next slice (slice 2 = Finding 3 OR slice 2 = Mechanism C if needed).

- [ ] **Step 4: Commit the bookkeeping**

```bash
git add docs/research/2026-05-21-a6-cdb-capture-findings.md
git add docs/plans/2026-04-11-roadmap.md
git add CLAUDE.md
git commit -m "docs(roadmap+findings): A6.P3 slice 1 — SHIPPED

CP-write resynthesis blowup (Finding 2) closed. scen1/3/5 re-captures
confirm ratio drop from 250-∞x to ≤10x. Strip-synthesis + Mechanism B
land. TryFindIndoorWalkablePlane retained pending A6.P4 deletion.

Next: assess Finding 1 (dispatcher entry frequency) post-fix; if
still wide, scope as A6.P3 slice 2. Otherwise proceed to Finding 3
(cell-resolver sling-out) as slice 2.

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

---

## Self-review checklist (for the implementer, before declaring slice 1 done)

- [ ] Task 1's research note exists and is committed.
- [ ] CollisionInfo.ContactPlaneWriteCount counter exists and is incremented by SetContactPlane.
- [ ] IndoorContactPlaneRetentionTests exists, fails pre-fix, passes post-fix.
- [ ] Mechanism B (LKCP restore) is wired at the correct per-transition site.
- [ ] FindEnvCollisions indoor branch is stripped to match retail's CEnvCell::find_env_collisions shape.
- [ ] TryFindIndoorWalkablePlane definition is NOT deleted (deferred to A6.P4).
- [ ] Build green; full test suite 1147+8 green.
- [ ] scen3 re-capture cp-write count ≤ 200.
- [ ] scen1 + scen5 re-capture cp-write ratios ≤ 10x.
- [ ] Visual verification at Holtburg inn 2nd floor passes (no fall-through, no jitter).
- [ ] Findings doc + roadmap + CLAUDE.md updated.
- [ ] If visual verification revealed a regression: rolled back and either added Mechanism C (slice 2 promotion) or applied a narrower gating fix.

If all 12 items check ✅, slice 1 is shipped. Next: either Finding 3 (separate plan) or Mechanism C if first-frame fall-through was reported.
