diff --git a/docs/research/2026-06-03-p0-conformance-apparatus-notes.md b/docs/research/2026-06-03-p0-conformance-apparatus-notes.md
index 9dadc38..e09b55f 100644
--- a/docs/research/2026-06-03-p0-conformance-apparatus-notes.md
+++ b/docs/research/2026-06-03-p0-conformance-apparatus-notes.md
@@ -167,6 +167,29 @@ evidence. **Do NOT design or code a P1 membership "fix" before the production-pa
RED/GREEN is read.** The P0 `..._DivergesFromRetail_PendingP1` test is a UNIT-level pin only, NOT
evidence of a production divergence.
+## ✅ RESOLVED — the production path DIVERGES (the "probe artifact" hypothesis is FALSIFIED)
+
+Built `ThresholdPortalCrossingReplayTests.ProductionPath_IndoorCrossings_DivergeFromRetail_PendingP1`
+(replays the golden indoor `0170↔0171` segments through the REAL `ResolveWithTransition` — engine
+builds the global sphere + sweeps; cells loaded from dats with real BSP). Result: **0/11 match
+retail.** Every segment: `restPos == target` (the sweep COMPLETES the move cleanly) but `CellId`
+stays on the SOURCE cell — acdream moves the body across the doorway yet **never advances
+`curr_cell`**. So production membership genuinely lags; the P0 finding is REAL, not a probe artifact.
+
+**Refined mechanism (supersedes the "portal-crossing vs point-in-cell criterion" framing).** Both
+retail and acdream PICK with center-only `point_in_cell`. The divergence is that retail's `curr_cell`
+ADVANCES to the neighbour during the sweep (the swept sphere crossing the doorway polygon, and/or a
+sphere point that leads the foot into the room), so by the time the foot rests at the captured
+position the membership has already advanced. acdream's swept advance does NOT promote the neighbour —
+at the end-position its tested sphere center is still inside the source cell's BSP, so the pick keeps
+the source cell. **P1's job: port how retail advances `curr_cell` across the portal mid-sweep.** The
+open decomp questions for P1: (1) how `global_sphere[0]` local origin relates to `m_position` (does
+retail's sphere point lead the foot?); (2) whether `curr_cell` advances via `find_transit_cells`'
+swept crossing in `transitional_insert`/`validate_transition` BEFORE the `find_cell_list` pick, vs the
+pick alone. Anchors: `CTransition::transitional_insert @ 0x50aa70 pc:272547`,
+`CTransition::validate_transition`, `CPhysicsObj::SetPositionInternal @ 0x515330 pc:283399`. The RED
+production-path test is the gate the P1 fix must turn GREEN.
+
## P0 status / P1-entry checklist — COMPLETE
**Apparatus: COMPLETE + GREEN.** (Conformance suite 59 pass / 1 skip / 0 fail.)
diff --git a/tests/AcDream.Core.Tests/Conformance/ThresholdPortalCrossingReplayTests.cs b/tests/AcDream.Core.Tests/Conformance/ThresholdPortalCrossingReplayTests.cs
new file mode 100644
index 0000000..e6651b0
--- /dev/null
+++ b/tests/AcDream.Core.Tests/Conformance/ThresholdPortalCrossingReplayTests.cs
@@ -0,0 +1,141 @@
+using System;
+using System.IO;
+using System.Numerics;
+using AcDream.Core.Physics;
+using DatReaderWriter;
+using DatReaderWriter.Options;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace AcDream.Core.Tests.Conformance;
+
+///
+/// P1 decisive evidence — the PRODUCTION-PATH membership conformance. Replays the
+/// captured retail doorway golden through the REAL
+/// (which builds the global sphere and SWEEPS it), then checks whether the swept
+/// CellId matches retail's committed cell. This is the integration test the
+/// P0 bare-FindCellList probe could NOT be: the probe fed the resting foot
+/// origin with no sweep, so it always reported the cell the foot stood in. Here the
+/// engine sweeps prev→curr, so the sphere crosses the doorway exactly as retail's did.
+///
+/// Diagnostic-first: prints MATCH/DIVERGE per segment so we read the real production
+/// behaviour before pinning an assertion. Outcomes (notes
+/// docs/research/2026-06-03-p0-conformance-apparatus-notes.md §CORRECTION): production
+/// MATCHES → the P0 divergence was a probe artifact; DIVERGES → a real bug to design from.
+///
+/// Scope here = the INDOOR segments (vestibule 0170 ↔ room 0171), which need only the
+/// building-cell cache. The outdoor-involving segments (0031↔0170) need the landcell +
+/// building portal and are a follow-up.
+///
+public class ThresholdPortalCrossingReplayTests
+{
+ private readonly ITestOutputHelper _out;
+ public ThresholdPortalCrossingReplayTests(ITestOutputHelper output) => _out = output;
+
+ // The doorway floor is flat at the captured pz across all transitions.
+ private const float FloorZ = 94.005f;
+ private const float SphereRadius = 0.4f;
+ private const float SphereHeight = 1.2f;
+ private const float StepUpHeight = 0.4f;
+ private const float StepDownHeight = 0.1f;
+
+ private static (PhysicsEngine, PhysicsDataCache) BuildBuildingEngine(DatCollection dats)
+ {
+ var cache = new PhysicsDataCache();
+ var engine = new PhysicsEngine { DataCache = cache };
+ for (uint low = 0x016Fu; low <= 0x0175u; low++)
+ ConformanceDats.LoadEnvCell(dats, cache, ConformanceDats.HoltburgLandblock | low);
+
+ // Stub landblock for FindObjCollisions context (flat far-below terrain; the
+ // indoor BSP path fires first, terrain is never consulted). Matches the
+ // CellarUpTrajectoryReplay harness pattern.
+ var heights = new byte[81];
+ var heightTable = new float[256];
+ for (int i = 0; i < 256; i++) heightTable[i] = -1000f;
+ engine.AddLandblock(0xA9B40000u, new TerrainSurface(heights, heightTable),
+ Array.Empty(), Array.Empty(), 0f, 0f);
+ return (engine, cache);
+ }
+
+ private static PhysicsBody GroundedBodyAt(Vector3 pos, uint cellId) => new()
+ {
+ Position = pos,
+ Orientation = Quaternion.Identity,
+ ContactPlaneValid = true,
+ ContactPlane = new Plane(0f, 0f, 1f, -FloorZ),
+ ContactPlaneCellId = cellId,
+ WalkablePolygonValid = true,
+ WalkablePlane = new Plane(0f, 0f, 1f, -FloorZ),
+ WalkableVertices = new[]
+ {
+ new Vector3(150f, 5f, FloorZ), new Vector3(150f, 20f, FloorZ),
+ new Vector3(165f, 20f, FloorZ), new Vector3(165f, 5f, FloorZ),
+ },
+ WalkableUp = Vector3.UnitZ,
+ TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable,
+ };
+
+ private static uint Low(uint id) => id & 0xFFFFu;
+
+ ///
+ /// Documents-the-bug (GREEN while acdream diverges; FAILS when P1 lands → rewrite to
+ /// per-segment Assert.Equal). The swept production path DIVERGES from retail on the
+ /// indoor doorway crossings: ResolveWithTransition completes the move
+ /// (restPos == target) but leaves CellId on the SOURCE cell — it never
+ /// advances curr_cell across the portal the way retail's change_cell golden
+ /// does. So the P0 finding is NOT a probe artifact: production membership genuinely lags.
+ /// P1 must port retail's swept-crossing curr_cell advance (how the sphere crossing
+ /// the doorway polygon / the leading sphere point promotes the neighbour to the membership
+ /// answer mid-sweep), then this flips to all-match.
+ ///
+ [Fact]
+ public void ProductionPath_IndoorCrossings_DivergeFromRetail_PendingP1()
+ {
+ var datDir = ConformanceDats.ResolveDatDir();
+ if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
+ var fixturePath = Path.Combine(ConformanceDats.FixturesDir, "find-cell-list-threshold.log");
+ if (!File.Exists(fixturePath)) { _out.WriteLine("SKIP: capture pending"); return; }
+
+ using var dats = new DatCollection(datDir, DatAccessType.Read);
+ var (engine, _) = BuildBuildingEngine(dats);
+ var picks = RetailTrace.ParseAll(File.ReadAllLines(fixturePath));
+
+ int match = 0, total = 0;
+ for (int i = 0; i + 1 < picks.Count; i++)
+ {
+ uint fromCell = picks[i].PickedCellId;
+ uint toCell = picks[i + 1].PickedCellId;
+ if (Low(fromCell) < 0x100 || Low(toCell) < 0x100) continue; // indoor segments only
+
+ var body = GroundedBodyAt(picks[i].Position, fromCell);
+ var result = engine.ResolveWithTransition(
+ currentPos: picks[i].Position,
+ targetPos: picks[i + 1].Position,
+ cellId: fromCell,
+ sphereRadius: SphereRadius,
+ sphereHeight: SphereHeight,
+ stepUpHeight: StepUpHeight,
+ stepDownHeight: StepDownHeight,
+ isOnGround: true,
+ body: body,
+ moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
+ movingEntityId: 0);
+
+ bool ok = result.CellId == toCell;
+ if (ok) match++;
+ total++;
+ _out.WriteLine(
+ $"seg 0x{Low(fromCell):X4}->0x{Low(toCell):X4} " +
+ $"pos=({picks[i].Position.X:F2},{picks[i].Position.Y:F2})->({picks[i + 1].Position.X:F2},{picks[i + 1].Position.Y:F2}) " +
+ $"acdream=0x{Low(result.CellId):X4} restPos=({result.Position.X:F2},{result.Position.Y:F2},{result.Position.Z:F2}) " +
+ $"{(ok ? "MATCH" : "DIVERGE")}");
+ }
+ _out.WriteLine($"=== production-path indoor crossings: {match}/{total} match retail ===");
+
+ Assert.True(total > 0, "no indoor doorway segments found in the golden");
+ Assert.True(match < total,
+ $"acdream's swept ResolveWithTransition now reproduces {match}/{total} retail indoor " +
+ "doorway crossings. If match == total, P1's curr_cell swept-advance port landed -> " +
+ "rewrite this to Assert.Equal(toCell, result.CellId) per segment. (Retail truth = the golden.)");
+ }
+}