diff --git a/docs/research/2026-05-25-door-bug-inside-out-geometry-gap.md b/docs/research/2026-05-25-door-bug-inside-out-geometry-gap.md
index 4a851b5..8b31333 100644
--- a/docs/research/2026-05-25-door-bug-inside-out-geometry-gap.md
+++ b/docs/research/2026-05-25-door-bug-inside-out-geometry-gap.md
@@ -142,27 +142,50 @@ the corner at (133.5, 17.10) — should encounter the cottage wall
and be stopped. If our engine handles the corner transition
incorrectly, sphere slides past.
-## What's next (revised)
+## What's next (revised AGAIN — corner test PASSED, bug is state-related)
-**Investigate sphere-vs-corner collision behavior** at the alcove
-east wall → cottage north wall meeting point at world (133.5, 17.10).
+**Corner-slide hypothesis: FALSIFIED.** `CornerSlide_AlcoveEastToCottageNorth_ShouldBlock`
+test runs cottage GfxObj + cell 0x0150 BSP both registered. Places
+sphere at (132.95, 16.8, 94) inside alcove near east wall. Walks +Y
+50 times at 0.05 m/tick. **Sphere stays put at (132.95, 16.8) for all
+50 ticks with cn=(0.71, -0.71, 0)** — the corner normal between
+alcove east wall and cottage north wall. **The corner handling works
+correctly in the harness.**
-Apparatus to write:
-- Load cottage GfxObj + cell 0x0150 BSP into harness
-- Place sphere at (133.0, 16.8, 94) (inside alcove, near east wall)
-- Walk sphere +Y in small increments
-- Expected: sphere stops at Y=17.10-0.48 = 16.62 if east wall blocks
- OR: sphere slides along corner staying in alcove
-- Captured: sphere ends up at (133.655, 17.59) — sliding past corner
+So production's walkthrough is **a STATE difference**, not a geometric
+or collision-detection bug. The harness's sphere can't reach
+X=133.655 inside the cottage geometry. Production's sphere does
+reach it somehow.
-If harness reproduces "sphere slides past corner", the bug is in
-the engine's corner-handling. Read BSPQuery for the sphere-vs-edge
-case. Retail oracle: CTransition::find_obj_collisions corner
-handling at acclient_2013_pseudo_c.txt.
+Differences between harness and production:
+- Harness uses identity walkable polygon (big quad). Production uses
+ real cell walkable polys (small, with edges).
+- Harness has stub landblock terrain at Z=-1000. Production has real
+ terrain.
+- Harness uses fresh body each tick. Production has accumulated state
+ from many prior ticks (velocity, contact plane history, etc.).
+- Harness uses sphereRadius=0.48 + sphereHeight=1.20 exactly. Production
+ matches but might have different stepUp / stepDown.
-If harness BLOCKS at corner (no sliding past), the bug is something
-else — maybe cell 0x0150 BSP isn't being queried in production from
-some sphere position.
+**Next-session apparatus**: replay the EXACT captured tick 2586's body
+state through the corner-blocking test setup. Tick 2586 was where
+sphere went from indoor cell 0x0150 to outdoor cell 0x0029 at
+PrevPy=17.586, Py=17.586 (no Y motion) with X=134.022 (way past alcove
+east wall). That tick is the smoking-gun "how did sphere get to X=134
+inside alcove" event. Load its body state into the harness, replay
+the call, see what the engine reports about getting to that position.
+
+If the harness blocks (sphere can't reach X=134), then production has
+state we're not capturing — probably accumulated push/depenetration
+across many earlier ticks. If the harness reproduces sphere at X=134,
+the bug is in the specific body state at that moment.
+
+The cleanest path forward is **cdb attach to retail** as the original
+handoff recommended. Inspect what retail does FRAME-BY-FRAME at the
+same doorway approach. If retail walks the user inside cottage at
+off-center approach EXACTLY like we do — the bug isn't a bug, and
+we should accept the behavior. If retail blocks cleanly — diff
+retail's body state evolution vs ours to find the divergence.
## OLD (superseded) "what's next" candidates
diff --git a/tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs b/tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs
index 3cd469e..c6fcb91 100644
--- a/tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs
+++ b/tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs
@@ -441,6 +441,145 @@ public class DoorBugTrajectoryReplayTests
$"wall is now blocking as expected — the #98 gate fix landed.");
}
+ ///
+ /// A6.P4 corner-slide hypothesis (2026-05-25 late) — reproduces the
+ /// inside-out walkthrough at unit-test speed. Builds engine with
+ /// BOTH cottage GfxObj 0x01000A2B (which contains the north exterior
+ /// wall east of doorway at X=[133.5, 136.3], Y=17.10) AND cell
+ /// 0xA9B40150's BSP (alcove east wall at X=133.5, Y=[16.5, 17.1]).
+ /// Sphere starts inside alcove sliding against east wall, then
+ /// walks NORTH. If harness slides sphere past the corner at
+ /// (133.5, 17.10) to end up at X > 133.5 Y > 17.10, bug reproduced.
+ ///
+ [Fact]
+ public void CornerSlide_AlcoveEastToCottageNorth_ShouldBlock()
+ {
+ var datDir = ResolveDatDir();
+ if (datDir is null) return;
+
+ var (engine, cache) = BuildFaithfulDoorEngine(datDir);
+
+ // 1. Register cottage GfxObj (contains the north exterior wall).
+ const uint CottageGfxId = 0x01000A2Bu;
+ const uint CottageEntityId = 0x00A9B479u;
+ var cottageFixturePath = Path.Combine(SolutionRoot(),
+ "tests", "AcDream.Core.Tests", "Fixtures", "issue98",
+ "0x01000A2B.gfxobj.json");
+ var cottageDump = GfxObjDumpSerializer.Read(cottageFixturePath);
+ var cottagePhysics = GfxObjDumpSerializer.Hydrate(cottageDump);
+ cache.RegisterGfxObjForTest(CottageGfxId, cottagePhysics);
+
+ engine.ShadowObjects.Register(
+ entityId: CottageEntityId,
+ gfxObjId: CottageGfxId,
+ worldPos: new Vector3(130.5f, 11.5f, 94.0f),
+ rotation: Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI),
+ radius: cottagePhysics.BoundingSphere?.Radius ?? 14f,
+ worldOffsetX: 0f,
+ worldOffsetY: 0f,
+ landblockId: DoorLandblockId,
+ collisionType: ShadowCollisionType.BSP,
+ scale: 1.0f,
+ cellScope: 0u);
+
+ // 2. Load cell 0xA9B40150 BSP into cache (the alcove walls).
+ const uint AlcoveCellId = 0xA9B40150u;
+ using (var dats = new DatCollection(datDir, DatAccessType.Read))
+ {
+ var envCell = dats.Get(AlcoveCellId);
+ Assert.NotNull(envCell);
+ var environment = dats.Get(
+ 0x0D000000u | envCell!.EnvironmentId);
+ Assert.NotNull(environment);
+ Assert.True(environment!.Cells.TryGetValue(envCell.CellStructure, out var cellStruct));
+
+ var cellOriginWorld = envCell.Position.Origin;
+ var cellTransform =
+ Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) *
+ Matrix4x4.CreateTranslation(cellOriginWorld);
+ cache.CacheCellStruct(AlcoveCellId, envCell, cellStruct!, cellTransform);
+ }
+ Assert.NotNull(cache.GetCellStruct(AlcoveCellId));
+
+ // 3. Sphere setup: inside alcove, near east wall.
+ // Alcove east wall at world X=133.5, Y=[16.5, 17.1]. Sphere at
+ // X=132.95 (sphere east edge 133.43 just west of wall), Y=16.8
+ // (inside alcove Y range).
+ var currentPos = new Vector3(132.95f, 16.8f, 94f);
+
+ // Walk sphere in +Y direction (toward cottage exterior north wall).
+ // Repeat several ticks with small steps to mimic walk-speed motion.
+ Vector3 pos = currentPos;
+ uint cellId = AlcoveCellId;
+ bool isOnGround = true;
+
+ var body = new PhysicsBody
+ {
+ Position = pos,
+ Orientation = Quaternion.Identity,
+ ContactPlaneValid = true,
+ ContactPlane = new System.Numerics.Plane(0f, 0f, 1f, -94f),
+ ContactPlaneCellId = cellId,
+ WalkablePolygonValid = true,
+ WalkablePlane = new System.Numerics.Plane(0f, 0f, 1f, -94f),
+ WalkableVertices = new[]
+ {
+ new Vector3(120f, 10f, 94f),
+ new Vector3(145f, 10f, 94f),
+ new Vector3(145f, 30f, 94f),
+ new Vector3(120f, 30f, 94f),
+ },
+ WalkableUp = Vector3.UnitZ,
+ TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable,
+ };
+
+ Console.WriteLine($"Start: pos=({pos.X:F3},{pos.Y:F3},{pos.Z:F3}) cell=0x{cellId:X8}");
+ for (int t = 1; t <= 50; t++)
+ {
+ var target = pos + new Vector3(0f, 0.05f, 0f); // walk speed
+ var result = engine.ResolveWithTransition(
+ currentPos: pos,
+ targetPos: target,
+ cellId: cellId,
+ sphereRadius: 0.48f,
+ sphereHeight: 1.20f,
+ stepUpHeight: 0.60f,
+ stepDownHeight: 1.5f,
+ isOnGround: isOnGround,
+ body: body,
+ moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
+ movingEntityId: DoorEntityId + 1);
+
+ pos = result.Position;
+ cellId = result.CellId;
+ isOnGround = result.IsOnGround;
+ body.Position = pos;
+
+ if (t % 5 == 0 || result.CollisionNormalValid)
+ {
+ Console.WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture,
+ "t={0,2} pos=({1:F3},{2:F3},{3:F3}) cell=0x{4:X8} cnValid={5} cn=({6:F2},{7:F2},{8:F2})",
+ t, pos.X, pos.Y, pos.Z, cellId,
+ result.CollisionNormalValid,
+ result.CollisionNormal.X, result.CollisionNormal.Y, result.CollisionNormal.Z));
+ }
+
+ // Stop if sphere has clearly walked through the wall.
+ if (pos.Y > 18f) break;
+ }
+
+ Console.WriteLine($"Final pos: ({pos.X:F3},{pos.Y:F3},{pos.Z:F3}) cell=0x{cellId:X8}");
+
+ // Document expected: sphere should stop at sphere center Y =
+ // 17.10 - 0.48 = 16.62 (cottage north wall + sphere reach).
+ // Bug: sphere slides past corner and exits north.
+ Assert.True(pos.Y < 17.20f,
+ $"BUG REPRODUCTION: sphere walked from inside alcove to Y={pos.Y:F3} " +
+ $"(past cottage north wall at Y=17.10). Cottage wall should have blocked " +
+ $"sphere at Y ≈ 16.62 (wall - sphere reach). If this assertion FAILS, " +
+ $"the corner handling at (X=133.5, Y=17.10) is letting sphere slide past.");
+ }
+
///
/// Diagnostic: dump cottage GfxObj 0x01000A2B polygons near world
/// position (133.655, 17.59, 94.5) — the sphere position where the