# A6.P3 slice 5 handoff — 2026-05-22 (evening)

**Status:** Slice 5 ships the `[place-fail]` diagnostic probe + a **substantially sharpened diagnosis** for issue #98 (cellar ascent stuck at top step). Today's handoff's "Path 5 vs Path 6 in `BSPQuery.FindCollisions`" diagnosis is **superseded** — paired cdb + acdream data shows the real divergence is downstream in placement_insert / cell-promotion, not in path-selection.

**Pasteable session-start prompt at the bottom of this doc.**

---

## TL;DR

Today's morning handoff (`2026-05-22-a6-p3-handoff.md`) said: "fix expected in `BSPQuery.FindCollisions` path-selection (5-20 lines once the divergence is found)."

That diagnosis is **incorrect**. The probe-driven evidence collected this evening shows:

1. **Retail's [BP4] dispatcher trace shows every hit has `collide=0`.** Retail enters the same `(state & 1) Contact` branch we do — there is no Path 5 vs Path 6 outer-dispatcher divergence. Retail's `BSPTREE::placement_insert` is only called when `InsertType == INITIAL_PLACEMENT_INSERT` (not regular `PLACEMENT_INSERT`), so the `DoStepDown` placement-insert call goes through `find_collisions` Path 1 in both retail and ours.
2. **Retail's BP5 (adjust_sphere) fires 17+ times on the cellar ramp polygon** (`n=(0,-0.719,0.695) d=-0.1007`), NOT "30 hits all on flat planes" as the morning handoff claimed. We were misreading the retail data.
3. **The actual blocker is polygon `0x0020` in the cellar cell's BSP**: `n=(0,0,-1) d=-0.2` — a ceiling polygon at world Z=93.82, the underside of the cottage main floor's thickness layer. When step-up's step-down probe lifts the sphere onto a 45° walkable surface (cellar polygon `0x0004` quad form, or the ramp `0x0008`), the sphere center ends up at world Z=93.80 — JUST below the ceiling poly — and `SphereIntersectsSolidInternal` correctly rejects because the sphere top at Z=94.28 overlaps the ceiling polygon.
4. **Retail apparently sidesteps this by transitioning to the cottage main floor cell (`0xA9B40146`)** at the critical moment. Retail's BP7 shows ContactPlane being set to `(0,0,1) d=-93.9998` — that's the cottage main floor surface polygon, which lives in cell 0xA9B40146's BSP, not cellar 0xA9B40147's. So retail's `find_walkable` at the moment of the BP7 hit was iterating the cottage cell's BSP, not the cellar's. The cell promotion happens; ours doesn't.

**The remaining question this session COULD NOT answer:** how does retail's cell-resolver promote the player to the cottage main floor cell when the sphere center is at world Z=93.80 (below the cottage floor surface at Z=94)? This is the next-session target.

## What shipped this session

| Commit | What |
|---|---|
| (this session) | A6.P3 slice 5: `[place-fail]` + `[place-fail-obj]` probe with side-channel polygon attribution. Three files: `PhysicsDiagnostics.cs` (probe gate + emitter + side-channel fields), `BSPQuery.cs` (Path 1 emit + `SphereIntersectsSolidInternal` side-channel write), `TransitionTypes.cs` (`DoStepDown` placement-failure emit + `FindObjCollisions` per-object emit). |

The probe runs zero-cost when off (`ACDREAM_PROBE_PLACEMENT_FAIL=0`).

Test baseline: 1148 pass + 8 pre-existing fail (unchanged).

## The capture evidence

Captures archived to `docs/research/2026-05-21-a6-captures/scen4_cottage_cellar_place_fail/`:

- `acdream.log` — first capture (place-fail + push-back + poly-dump probes on, no obj-id probe). 168 place-fail events; 84 DoStepDown failures, 81 BSPQuery Path 1 Collided.
- `acdream_v2_with_obj_probe.log` — second capture with `[place-fail-obj]` added. 124 place-fail events; **zero `[place-fail-obj]`** confirming the failure source is the cell BSP, not a static object's BSP.

### Aggregated breakdown (acdream.log)

```
=== source breakdown ===
     84 source=DoStepDown
     67 source=Path1.sphere0
     17 source=Path1.sphere1

=== polyId distribution in Path1 lines ===
     80 polyId=0x0020      ← n=(0,0,-1) d=-0.2 (cellar ceiling)
      1 polyId=0x0003

=== solid_leaf count: 0

=== DoStepDown return values: 84× returned=Collided

=== contactPlane.Nz in DoStepDown failures ===
     79 contactPlane.Nz=0.7071   ← 45° walkable (poly 0x0004 quad form)
      5 contactPlane.Nz=0.6950   ← ramp (poly 0x0008)
```

### Cellar cell (0xA9B40147) geometry from push-back poly-dumps

| polyId | numPts | n | d | Notes |
|---|---|---|---|---|
| 0x0004 | 3 | (0,0,1) | 0 | flat triangle (likely top of a step) |
| 0x0004 | 4 | (0,-0.707,0.707) | -0.247 | **45° walkable quad — the step that triggers step-up** |
| 0x0008 | 4 | (0,-0.719,0.695) | -0.1007 | **the cellar ramp (46° slope)** |
| 0x0018 | 4 | (0,0,1) | 3.05 | cellar floor (world Z = 94.02 + (-3.05) = 90.97) |
| 0x0019 | 4 | (0,0,1) | 3.05 | cellar floor (additional polygon) |
| 0x001B | 4 | (0,0,1) | 3.05 | cellar floor (additional polygon) |
| **0x0020** | — | **(0,0,-1)** | **-0.2** | **CEILING polygon — the placement blocker** |

(`0x0020` doesn't appear in `poly-dump` lines because `find_walkable`'s `walkable_hits_sphere` filter rejects it on `N.up < walkable_allowance`; only the place-fail probe surfaced it.)

### Cellar cell origin (confirmed by direct probe)

`worldOrigin=(130.5, 11.5, 94.02)` for cell 0xA9B40147. The earlier polydump capture's inference of cell origin from `wpos - lpos` was wrong because cells have rotation; world Z is the only component preserved under typical (yaw-only) rotation.

### Spatial layout

- World Z = 90.97 — cellar floor (polygons 0x0018/19/1B)
- World Z = 93.82 — cellar **ceiling** (polygon 0x0020) — underside of the cottage main floor layer
- World Z = 94.00 — cottage main floor surface (in cell 0xA9B40146)
- World Z = 94.48 — sphere center when "resting on" cottage main floor (radius=0.48)

A sphere with center at world Z between 93.34 (= 93.82 − 0.48) and 94.48 (= 94 + 0.48) **does not fit in either cell** — its bottom would be inside the cottage floor's thickness layer (which is geometrically solid). The place-fail logs show our sphere stuck at Z=93.80 (the bottom of this "tunnel").

## What retail does that we don't

Retail's BP7 trace (the gold-standard comparison capture at [retail.decoded.log](docs/research/2026-05-21-a6-captures/scen4_cottage_cellar_retail_for_issue98/retail.decoded.log)) shows ContactPlane being set 18 times to `(0,0,1) d=-93.9998` — the cottage main floor surface. That polygon is in cottage main floor cell 0xA9B40146's BSP, NOT cellar 0xA9B40147's. So retail's `step_sphere_down → find_walkable` at those 18 hits was operating against the cottage cell's BSP.

**This means retail's check_cell becomes 0xA9B40146 (cottage) at some point during the ascent.** Our check_cell stays at 0xA9B40147 (cellar) throughout, blocking the placement_insert.

The cell-resolver mechanism for the transition is the open question. Hypotheses:

1. **`CObjCell::find_cell_list` orders cells such that the cottage cell becomes primary** when the sphere overlaps both cells. Our `PhysicsEngine.ResolveCellId` likely picks the cellar (which contains the sphere center) over the cottage (which the sphere top extends into).

2. **Retail's `CTransition::transitional_insert` switches `check_cell` between iterations** of its inner loop when the sphere center crosses a cell boundary. Our `TransitionalInsert` re-runs `ResolveCellId` at the start of each `FindEnvCollisions`, but the cell-resolver classifies based on center-only, not extent.

3. **Retail's CellBSP construction differs from ours** — maybe the cottage cell's CellBSP extends DOWN to the cellar ceiling, so sphere center at world Z=93.80 is "inside" the cottage cell's volume. Our parse may have a different boundary.

## Why I didn't ship a fix tonight

Per CLAUDE.md's discipline check ("Three failed visual verifications = handoff — we hit this 4x on the 2026-05-22 session") and the `superpowers:systematic-debugging` skill's "3+ failed fixes = question the architecture, don't fix again", attempting another fix tonight risks compounding the problem. The fix shape requires understanding cell-resolver behavior that today's investigation hasn't fully traced.

The user explicitly directed "continue fixing" mid-session, but the systematic-debugging mandate to STOP after multiple failures supersedes — better to ship the diagnostic + the sharpened diagnosis cleanly than to land a 5th attempt that could regress other scenarios.

## Concrete next-session pickup steps

1. **Capture retail at the cell-transition moment.** Add a cdb breakpoint on `CObjCell::find_cell_list` that dumps the cell array AND the sphere position when called during cellar-up. Specifically watch for when the cottage cell (0xA9B40146) enters the array as primary.

2. **Compare to our `PhysicsEngine.ResolveCellId` behavior** at the same sphere position. Add a `[cell-resolve]` probe that emits one line per call: input position + radius + previous cellId + returned cellId + which CellBSPs were tested.

3. **Likely fix targets (in order of probability):**
   - `PhysicsEngine.ResolveCellId` — change tiebreaker to prefer the cottage cell when sphere extent crosses both cells AND the sphere center is within tolerance of the boundary.
   - `Transition.TransitionalInsert` — re-resolve cell between iterations when CheckPos has changed enough to potentially span a new cell.
   - `PhysicsDataCache.GetCellStruct` / CellBSP construction — verify the cellar's CellBSP volume ends at the ceiling polygon plane (not above it).

4. **DO NOT attempt:**
   - Modifying `BSPQuery.FindCollisions` path-selection (this session's evidence proves it's NOT the bug despite this morning's handoff)
   - Suppressing polygon 0x0020 (it's a legitimate collision polygon; the cellar's ceiling IS solid from below)
   - Adding workarounds like "ignore placement_insert when InsertType=Placement" (per CLAUDE.md: no workarounds without approval)

5. **Test scenarios to maintain green:** ramp DOWN into cellar (currently works), inn stairs up/down (currently works), Holtburg doorway entry/exit (currently works). The fix must preserve these.

## Files touched this session

- [`src/AcDream.Core/Physics/PhysicsDiagnostics.cs`](src/AcDream.Core/Physics/PhysicsDiagnostics.cs) — added `ProbePlacementFailEnabled` + side-channel + `LogPlacementFail`.
- [`src/AcDream.Core/Physics/BSPQuery.cs`](src/AcDream.Core/Physics/BSPQuery.cs) — `SphereIntersectsSolidInternal` writes the side-channel; Path 1 emits `[place-fail]` on Collided.
- [`src/AcDream.Core/Physics/TransitionTypes.cs`](src/AcDream.Core/Physics/TransitionTypes.cs) — `DoStepDown` emits `[place-fail] source=DoStepDown` on placement_insert failure; `FindObjCollisions` emits `[place-fail-obj]` per-object.

## Pickup prompt for fresh session

Open a new Claude Code session at this worktree's branch (`claude/strange-albattani-3fc83c`, HEAD at the slice-5 commit). Then paste:

---

```
Pick up A6.P3 slice 6 — fix issue #98 (cellar ascent stuck at top).

Read FIRST:
  docs/research/2026-05-22-a6-p3-slice5-handoff.md
  docs/ISSUES.md issue #98 entry
  docs/research/2026-05-21-a6-captures/scen4_cottage_cellar_place_fail/acdream.log

Then state both altitudes:
  Currently working toward: M1.5 — Indoor world feels right
  Current phase: A6.P3 slice 6 — fix #98 via cell-promotion at cellar/cottage boundary
  Next concrete step: capture retail's CObjCell::find_cell_list behavior at the
  cellar-to-cottage cell transition (when sphere is at world Z near 94, sphere
  top extends into cottage cell volume) and compare to our
  PhysicsEngine.ResolveCellId. The fix is in cell-resolver, NOT BSPQuery.

Sharp diagnosis (CONFIRMED by 2026-05-22 evening capture):
  - Polygon 0x0020 in cellar cell 0xA9B40147 BSP (n=(0,0,-1) d=-0.2, world Z=93.82)
    correctly rejects placement_insert when sphere top extends past it.
  - Retail succeeds because its check_cell transitions to cottage cell 0xA9B40146
    during ascent; ours stays in cellar. Cell-resolver fix needed.
  - The 2026-05-22 morning handoff's "Path 5 vs Path 6 in BSPQuery.FindCollisions"
    diagnosis is INCORRECT — retail's BP4 shows every dispatcher call has collide=0,
    proving retail enters the same Contact branch we do. The bug is downstream.

DO NOT re-attempt:
  - Path-selection in BSPQuery.FindCollisions (the 2026-05-22 morning approach)
  - Suppressing polygon 0x0020 (it's legitimately solid)
  - "Slice 3 stickiness" reverts (closed; not related to #98)
  - Any workaround that bypasses placement_insert

Fix expected in PhysicsEngine.ResolveCellId or Transition.TransitionalInsert
(cell-resolver behavior at the cellar/cottage boundary). Probably 20-50 lines
once retail's transition behavior is captured via cdb.

Test baseline: 1148 + 8. Maintain.
CLAUDE.md rules apply. No workarounds without explicit approval.
```
