acdream/docs/superpowers/plans/2026-05-23-a6-p3-issue98-cellar-up-fix.md
Erik 8a232a3e6e diag(phys): A6.P3 #98 — [step-walk-adjust] probe inside AdjustOffset
Adds one log line per AdjustOffset call (gated by ACDREAM_PROBE_STEP_WALK)
naming the branch taken (no-cp / no-cp-slide / slide-degenerate /
slide-crease / into-plane / away-plane, optionally +safety-push) plus
zGain = output.Z - input.Z.

No math or control-flow changes — pure observability so the next capture
can disambiguate the three failure-mode hypotheses for the cellar-ramp
climb cap. Re-reading the existing capture (a6-issue98-negpoly-...log)
showed the sphere DOES climb 90.00 -> 92.79 (2.79 m gain), then caps,
contradicting the divergence comparison's "no altitude gain" framing.
The real question is what stops the climb at world Z ~= 92.79 with the
cottage floor still 1.21 m higher. Existing [step-walk] probes wrap
AdjustOffset; this new probe reveals which branch the projection takes.

Fix plan with the four-branch decision tree at
docs/superpowers/plans/2026-05-23-a6-p3-issue98-cellar-up-fix.md.

Test baseline maintained: 1167 + 8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:16:42 +02:00

481 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# A6.P3 issue #98 — cellar-up fix plan (diagnostic-first)
> **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:** Close issue #98 (sphere stuck on cottage cellar ramp) by identifying the EXACT failure point through a focused diagnostic probe, then writing a minimal evidence-driven fix. The user can walk up and out of a Holtburg cottage cellar in the live client.
**Architecture:** Three phases. Phase 0 re-reads the existing capture for everything the divergence comparison missed — no code. Phase 1 adds ONE focused probe at the only site where existing instrumentation is silent (inside `AdjustOffset`). Phase 2 runs the probe and decides which of three branches the fix must take. Phase 3 writes the fix against the replay harness as TDD oracle.
**Tech Stack:** C# .NET 10. The deterministic replay loop ([Issue98CellarUpReplayTests.cs](../../tests/AcDream.Core.Tests/Physics/Issue98CellarUpReplayTests.cs), <200ms) is the inner test loop. The cdb capture script ([tools/cdb/issue98-runner.ps1](../../tools/cdb/issue98-runner.ps1)) is the outer ground-truth loop.
**Risk:** Four prior sessions guessed-and-shipped (10+ variants). The pattern: convinced of diagnosis ship fix user reports "still can't pass." This plan refuses to ship code until the diagnostic data NAMES the failure site.
---
## What the existing log already proves (Phase 0 already partially done)
The slice 7 handoff and the divergence comparison both claimed *"our sphere oscillates at world Z ≈ 92.01 with no altitude gain."* That framing is **wrong**. Scanning the 2,589 `[step-walk]` lines in [a6-issue98-negpoly-20260523-135032.out.log](../../a6-issue98-negpoly-20260523-135032.out.log) shows:
| What the log shows | Implication |
|---|---|
| Sphere `cur` Z climbs **90.00 → 92.79** across the capture (2.79 m gain). | The climb works for most of the ramp. Z gain per `[step-walk] site=after-adjust` is +0.197 to +0.227 when offset points INTO the ramp normal. |
| Climb **caps at world Z ≈ 92.79**, then descends back. | Something at the top of the ramp prevents the last ~1.21 m of climb to cottage floor (world Z=94). |
| At the peak (line 1745817485 in the log): `before-insert` has `check=(141.58, 7.18, 92.79)`, `walkPoly=True`. `after-insert` has same position but **`walkPoly=False`** + `winterp=-0.0000`. | `DoStepDown` at the peak burned `WalkInterp` from 1.0 0 in one tick and the sphere ended up off the walkable polygon. |
| `cell=0xA9B40147->0xA9B40147` for every line. | Our sphere never transitions to a cottage cell (`0xA9B40143` / `0xA9B40146`) during the climb. Retail's BPA capture shows queries against cottage cells starting at BPA hit#430. |
| ContactPlane normal is `(0, +0.719, +0.695)` in our log. The divergence doc says retail's ramp normal is `(0, -0.719, +0.695)` (sign-flipped) but retail's BPE NEVER writes the ramp as ContactPlane at all (only flat cellar floor or flat cottage floor). | Retail does not use the ramp as a ContactPlane. Our code does. This is a SHAPE-of-the-fix question for later; do not act on it before the diagnostic confirms it matters. |
**Revised hypothesis (informed by the data, not the divergence doc):**
The climb works while the sphere is mid-ramp. It **fails at the top of the ramp** where the polygon ends. Three mutually-exclusive failure modes are plausible:
1. **Geometric cap.** The ramp polygon physically ends at world Z 92.79; there is no further walkable surface within the step-up reach. Retail's sphere reaches the cottage floor by a transition we never trigger.
2. **Cell-set divergence.** At world Z 92.79 the sphere overlaps cottage cell volumes, but our `CheckOtherCells` either doesn't query the cottage cell, or queries it with the wrong sphere reference (foot vs lifted). Retail's BPA hit#430 / hit#434 show queries against TWO different leaves at SIGNIFICANTLY different sphere positions (cell A: local 0.48, cell B: local 0.63) that's the retail two-step-up pattern at work, which we may not be doing.
3. **WalkInterp depletion before forward motion applies.** The peak `[step-walk]` line at 17485 shows `winterp=-0.0000` after `DoStepDown`. If `DoStepDown` consumed all WalkInterp on the lift (the slice 7 handoff's reading), the next tick starts WalkInterp at 1.0 again so this isn't the inter-tick blocker the handoff thought. But INTRA-tick, the next call's `AdjustOffset` runs with no remaining WalkInterp, which could legitimately gate further motion.
**The point of Phase 1 is to disambiguate between (1), (2), and (3).**
---
## Phase 1 — focused diagnostic (no code logic changes)
Add ONE probe site inside `AdjustOffset` and rerun. The existing `[step-walk]` probe instruments the call from the outside (req adj across the call) but never reveals which **branch** AdjustOffset took. The new probe reveals the branch; that's the missing signal.
### Task 1.1: Add `[step-walk-adjust]` probe call inside AdjustOffset
**Files:**
- Modify: `src/AcDream.Core/Physics/PhysicsDiagnostics.cs` (add one new helper)
- Modify: `src/AcDream.Core/Physics/TransitionTypes.cs:2635-2739` (add 4 probe calls inside AdjustOffset's branches)
- [ ] **Step 1: Add the diagnostic emitter to PhysicsDiagnostics.cs**
Add this method right after `LogStepWalk` (currently ending around line 700). It is intentionally separate from `LogStepWalk` so the line format stays short and greppable.
```csharp
/// <summary>
/// A6.P3 issue #98 (2026-05-23) — focused probe INSIDE AdjustOffset
/// revealing which branch was taken and the Z gain per call. Pair with
/// <c>[step-walk] site=after-adjust</c> at the call site to triangulate
/// where the projection ends up. Caller MUST guard with
/// <c>if (!ProbeStepWalkEnabled) return;</c> before calling.
/// </summary>
public static void LogStepWalkAdjust(
string branch,
Vector3 input,
Vector3 output,
Plane? contactPlane,
bool slidingValid,
Vector3 slidingNormal,
float collisionAngle,
float walkInterp)
{
var culture = System.Globalization.CultureInfo.InvariantCulture;
string cpDesc = contactPlane is { } cp
? string.Format(culture,
"n=({0:F4},{1:F4},{2:F4}) d={3:F4}",
cp.Normal.X, cp.Normal.Y, cp.Normal.Z, cp.D)
: "n/a";
string slideDesc = slidingValid
? string.Format(culture,
"({0:F4},{1:F4},{2:F4})",
slidingNormal.X, slidingNormal.Y, slidingNormal.Z)
: "n/a";
Console.WriteLine(string.Format(culture,
"[step-walk-adjust] branch={0} input=({1:F4},{2:F4},{3:F4}) " +
"output=({4:F4},{5:F4},{6:F4}) zGain={7:F4} " +
"cp={8} slide={9} colAngle={10:F4} winterp={11:F4}",
branch,
input.X, input.Y, input.Z,
output.X, output.Y, output.Z,
output.Z - input.Z,
cpDesc, slideDesc, collisionAngle, walkInterp));
}
```
- [ ] **Step 2: Wire the probe at the four AdjustOffset branches**
Open [TransitionTypes.cs](../../src/AcDream.Core/Physics/TransitionTypes.cs) at line 2635 (`private Vector3 AdjustOffset(Vector3 offset)`). The body currently has FOUR exit/branch points:
1. **no-cp** path (line 26542659): `if (!ci.ContactPlaneValid) return result;`
2. **slide** path (lines 26652676): `if (checkSlide) { ... result = ... }`
3. **into-plane** path (lines 26772681): `else if (collisionAngle <= 0f) { result -= ... }`
4. **away-plane** path (lines 26822687): `else { result -= ... }` same arithmetic as `into-plane`, kept distinct for the probe.
At the FINAL return (line 2738 `return result;`), emit ONE probe line that captures the branch taken and the final result.
Replace lines 26352739 with a body that tracks the branch token (initialize to `"unknown"`) and assigns it at each branch point. Concretely:
```csharp
private Vector3 AdjustOffset(Vector3 offset)
{
var sp = SpherePath;
var ci = CollisionInfo;
Vector3 result = offset;
bool checkSlide = false;
string branch = "init"; // ← new
// Check if we should apply sliding.
float slidingAngle = Vector3.Dot(result, ci.SlidingNormal);
if (ci.SlidingNormalValid)
{
if (slidingAngle < 0f)
checkSlide = true;
else
ci.SlidingNormalValid = false;
}
// No contact plane — simple slide projection.
if (!ci.ContactPlaneValid)
{
if (checkSlide)
{
result -= ci.SlidingNormal * slidingAngle;
branch = "no-cp-slide"; // ← new
}
else
{
branch = "no-cp"; // ← new
}
if (PhysicsDiagnostics.ProbeStepWalkEnabled) // ← new
PhysicsDiagnostics.LogStepWalkAdjust(
branch, offset, result,
contactPlane: null,
slidingValid: ci.SlidingNormalValid,
slidingNormal: ci.SlidingNormal,
collisionAngle: 0f,
walkInterp: sp.WalkInterp);
return result;
}
// Have a contact plane — project movement onto the contact surface.
float collisionAngle = Vector3.Dot(result, ci.ContactPlane.Normal);
Vector3 slideOffset = Vector3.Cross(ci.ContactPlane.Normal, ci.SlidingNormal);
if (checkSlide)
{
// Project movement along the crease between contact and slide planes.
float slideLen = slideOffset.Length();
if (slideLen < PhysicsGlobals.EPSILON)
{
result = Vector3.Zero;
branch = "slide-degenerate"; // ← new
}
else
{
slideOffset /= slideLen;
result = Vector3.Dot(slideOffset, result) * slideOffset;
branch = "slide-crease"; // ← new
}
}
else if (collisionAngle <= 0f)
{
// Moving into the contact plane: remove component into the plane.
result -= ci.ContactPlane.Normal * collisionAngle;
branch = "into-plane"; // ← new
}
else
{
// Moving away from contact plane: snap to plane surface.
result -= ci.ContactPlane.Normal * collisionAngle;
branch = "away-plane"; // ← new
}
// ── (Existing safety check unchanged — keep lines 26892736 verbatim) ──
if (ci.ContactPlaneCellId != 0 && !ci.ContactPlaneIsWater)
{
Vector3 globCenter = sp.GlobalSphere[0].Origin;
float radius = sp.GlobalSphere[0].Radius;
float dist = Vector3.Dot(globCenter, ci.ContactPlane.Normal)
+ ci.ContactPlane.D;
float naturalRestingDist = radius * ci.ContactPlane.Normal.Z;
if (dist < naturalRestingDist - PhysicsGlobals.EPSILON)
{
float zDist = (naturalRestingDist - dist) / ci.ContactPlane.Normal.Z;
if (radius > MathF.Abs(zDist))
{
sp.AddOffsetToCheckPos(new Vector3(0f, 0f, zDist));
branch += "+safety-push"; // ← new — branch annotation
}
}
}
if (PhysicsDiagnostics.ProbeStepWalkEnabled) // ← new
PhysicsDiagnostics.LogStepWalkAdjust(
branch, offset, result,
contactPlane: ci.ContactPlane,
slidingValid: ci.SlidingNormalValid,
slidingNormal: ci.SlidingNormal,
collisionAngle: collisionAngle,
walkInterp: sp.WalkInterp);
return result;
}
```
**What this adds:** ONE log line per AdjustOffset call (probe-gated) naming the branch and showing Z gain. Nothing else changes the math is identical.
- [ ] **Step 3: Run dotnet build to confirm the edit compiles**
```powershell
dotnet build src\AcDream.Core\AcDream.Core.csproj -c Debug
```
Expected: green build. The diff is additive (one new diagnostic helper + four single-line probe gates inside an existing method).
- [ ] **Step 4: Run dotnet test to confirm the apparatus tests stay green**
```powershell
dotnet test tests\AcDream.Core.Tests\AcDream.Core.Tests.csproj -c Debug --filter "FullyQualifiedName~Issue98CellarUpReplayTests"
```
Expected: all 7 Issue98CellarUpReplayTests pass (they document the bug; the failing-frame assertions still pin the current behavior). No new failures elsewhere.
- [ ] **Step 5: Commit the probe-only change**
```powershell
git add src\AcDream.Core\Physics\PhysicsDiagnostics.cs src\AcDream.Core\Physics\TransitionTypes.cs
git commit -m "diag(phys): A6.P3 #98 — [step-walk-adjust] probe inside AdjustOffset
Adds one log line per AdjustOffset call (gated by ACDREAM_PROBE_STEP_WALK)
naming the branch taken (no-cp / no-cp-slide / slide-degenerate /
slide-crease / into-plane / away-plane, optionally +safety-push) plus
zGain = output.Z - input.Z.
No math changes — pure observability so the next capture can disambiguate
the three failure-mode hypotheses for the cellar-ramp climb cap at
world Z ≈ 92.79."
```
---
### Task 1.2: Capture with the new probe
- [ ] **Step 1: Confirm ACE is up and the test character is in the cottage cellar**
The character is `+Acdream` (server guid `0x5000000A`). Stand on the cellar ramp, facing the top.
- [ ] **Step 2: Launch the client with the step-walk probe enabled**
```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_PROBE_STEP_WALK = "1"
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 |
Tee-Object -FilePath "a6-issue98-stepwalkadjust-$timestamp.out.log"
```
- [ ] **Step 3: User walks SLOWLY up the cellar ramp until stuck — no 180° turn-around**
The previous capture (negpoly log) polluted the trajectory because the user turned around and walked back. This capture must be a clean monotone climb.
- [ ] **Step 4: User closes the client (graceful — Alt-F4 / window close, not Ctrl-C)**
Per the logout-before-reconnect rules in CLAUDE.md hard kill costs 3 minutes of ACE session recovery.
- [ ] **Step 5: Snapshot the capture to docs/research**
```powershell
$captureDir = "docs\research\2026-05-23-a6-captures\stepwalkadjust"
New-Item -ItemType Directory -Force $captureDir | Out-Null
Copy-Item "a6-issue98-stepwalkadjust-$timestamp.out.log" "$captureDir\acdream.log"
```
---
### Task 1.3: Analyze the new probe data
- [ ] **Step 1: Extract the [step-walk-adjust] lines from the climb portion only**
```powershell
Select-String "step-walk-adjust" "docs\research\2026-05-23-a6-captures\stepwalkadjust\acdream.log" |
Select-Object -ExpandProperty Line |
Set-Content "docs\research\2026-05-23-a6-captures\stepwalkadjust\adjust-only.log"
```
- [ ] **Step 2: Cross-correlate [step-walk-adjust] with [step-walk] site=after-adjust by line position**
For each ramp-climb tick, you should see the pattern:
```
[step-walk] site=after-adjust cur=(...) req=(rX,rY,0) adj=(aX,aY,aZ) ...
[step-walk-adjust] branch=... input=(rX,rY,0) output=(aX,aY,aZ) zGain=aZ cp=(...) ...
```
- [ ] **Step 3: Classify the climb by branch token**
Walk forward through the lines from the start of the climb to the peak (world Z 92.79). Build a histogram:
```
branch=into-plane — how many calls? Avg zGain?
branch=away-plane — how many? Avg zGain?
branch=slide-crease — how many? Avg zGain?
branch=no-cp — how many? (means ContactPlaneValid cleared mid-climb)
+safety-push annotation — how often? At what zGain?
```
- [ ] **Step 4: Decide which Phase 2 branch the fix takes**
Decision tree (read the histogram + the climb-cap moment together):
| Observation | Implies fix target | Phase 2 branch |
|---|---|---|
| `into-plane` dominates the climb, +zGain ~0.2/call, then at peak the branch flips to `no-cp` or `away-plane` with zero zGain | **Target A: ContactPlane is being cleared / replaced at the ramp top.** Fix: investigate why the ramp's CP is dropping when the climb is incomplete. | **Branch A** |
| `into-plane` continues at peak but zGain becomes near-zero (offset.Y trends to zero) | **Target B: forward motion is being consumed elsewhere.** Either by step-up burning WalkInterp before AdjustOffset gets the offset, or by the offset being slid horizontally by a wall hit. Look at `[step-walk] site=before-insert` for a Y-collapsing pattern. | **Branch B** |
| `into-plane` at peak with correct zGain but CurPos doesn't advance | **Target C: the offset is computed correctly but never committed.** Look at `TransitionalInsert` / `Insert` for a path that returns before commit. | **Branch C** |
| Any branch with frequent `slide-crease` mid-climb | **Side-finding: a SlidingNormal is being set against the ramp.** Should not happen on a clean slope points at a wall poly being mis-classified. | **Branch D (side-investigation, not the fix)** |
- [ ] **Step 5: Write a 1-page findings note**
Save to `docs/research/2026-05-23-a6-stepwalkadjust-findings.md`. Format:
```markdown
# A6.P3 #98 — [step-walk-adjust] capture analysis (YYYY-MM-DD)
## Climb branch histogram (90.00 → peak)
- into-plane: X calls, avg zGain=Y
- away-plane: ...
- ...
## At the climb cap (world Z ≈ ZZZ):
- Branch flipped from X to Y at log line NNN
- ContactPlane normal at cap: (...)
- WalkInterp at cap: ...
## Conclusion: Fix target is Branch A / B / C / D.
## Reason (one paragraph)
...
```
- [ ] **Step 6: Commit the findings note**
```powershell
git add docs\research\2026-05-23-a6-stepwalkadjust-findings.md docs\research\2026-05-23-a6-captures\stepwalkadjust\
git commit -m "research(phys): A6.P3 #98 — [step-walk-adjust] capture + findings
Identifies fix target Branch [A/B/C/D] for the cellar-up climb cap."
```
---
## Phase 2 — branch to fix target (data-driven)
**Do NOT execute Phase 2 until Phase 1's findings note exists and the user has reviewed it.** This is the explicit lesson from the 4-session failure pattern: shipping speculative fixes wastes a session each time.
Per CLAUDE.md (no-workarounds rule) the fix must address the root cause. Each branch below names the LIKELY fix shape. The actual code lands as a separate plan once Phase 1 confirms the branch.
### Branch A — ContactPlane clearing / replacement at ramp top
If the climb works while CP=ramp and FAILS when CP changes mid-climb:
**Investigate first (no code):**
- Where in `Transition.FindEnvCollisions` / `Transition.SetContactPlane` is the ramp CP dropped? Likely candidates: `[TransitionTypes.cs:1814](../../src/AcDream.Core/Physics/TransitionTypes.cs:1814)` (FindEnvCollisions write), `[TransitionTypes.cs:1837](../../src/AcDream.Core/Physics/TransitionTypes.cs:1837)` (post-stepdown write).
- Compare against retail's BPE pattern: retail's CP toggles cellar-floor cottage-floor with **no intermediate** (per the divergence comparison table). It never sets CP to the ramp. Our code DOES. The fix MAY be making our ramp-CP behavior match retail (never set CP=ramp; set CP=cellar-floor while climbing, then transition CP=cottage-floor when the sphere is above cottage floor) but this is a SHAPE-of-the-fix question; verify it's actually needed before changing the contract.
**Probable fix file:** [src/AcDream.Core/Physics/TransitionTypes.cs](../../src/AcDream.Core/Physics/TransitionTypes.cs) around `FindEnvCollisions` (line 17001900).
**Acceptance:** Phase 1's findings note's branch histogram shows the failure point. The fix must produce a log where `cur` Z monotonically increases from 90.00 to 94.00 across the climb.
### Branch B — forward motion consumed before AdjustOffset
If `into-plane` continues at peak but zGain 0 because input offset Y 0:
**Investigate first:**
- Look at `[step-walk] site=before-insert` in the existing log around line 17458 at the peak. Compare the `req` value to the next tick's `req`. If `req.Y` decays to zero across consecutive ticks, the motion source itself is being cut. Trace back to where the per-tick offset is generated `[PlayerMovementController](../../src/AcDream.Core/Physics/PlayerMovementController.cs)` and the input velocity offset chain.
- Compare against retail's BPF (adjust_sphere_to_plane) cadence 431 hits over 35K BPs suggests retail re-projects relatively frequently but not in a way that zeros out motion. Our code might be over-projecting.
**Probable fix file:** [src/AcDream.Core/Physics/PlayerMovementController.cs](../../src/AcDream.Core/Physics/PlayerMovementController.cs) or the velocity-source upstream of the physics tick.
**Acceptance:** Same as Branch A.
### Branch C — offset computed but never committed (CurPos doesn't advance)
If `[step-walk-adjust] zGain` is correct per call but `[step-walk] cur` doesn't accumulate across calls:
**Investigate first:**
- `TransitionalInsert` in [TransitionTypes.cs](../../src/AcDream.Core/Physics/TransitionTypes.cs) find the path that returns without committing `sp.CurPos += sp.GlobalOffset`. Likely guard: a collision state that retreats.
- The `[step-walk] delta=(0,0,0)` pattern across consecutive lines is the smoking gun if it persists at the peak.
**Probable fix file:** `TransitionTypes.cs`, the per-step commit at the bottom of the step loop (around line 689760).
**Acceptance:** Same as Branch A.
### Branch D — sliding-normal mis-classification (side-investigation only)
If `slide-crease` appears mid-climb, the ramp polygon is being treated as a wall by some upstream call. This is unlikely to be the fix for #98 slide is the right answer for walls, the question is which polygon triggered it.
**Investigate:** which polygon's normal got installed as `SlidingNormal`? Add a one-line probe at `ci.SlidingNormal = ...` write sites and capture again.
---
## Phase 3 — write the fix (TDD against replay tests)
Once Phase 1 names the branch and the findings note is committed, write a fresh plan for the fix:
`docs/superpowers/plans/2026-MM-DD-a6-p3-issue98-cellar-up-fix-impl.md`
Use [superpowers:test-driven-development](../../.claude/superpowers/skills/test-driven-development/SKILL.md) discipline:
1. **Flip the replay test assertions FIRST** so they document the FIXED behavior. Two assertions to invert:
- `FailingFrame_CottageNeighborA_NearestWalkableIsOutsideSphereAndEdges` at minimum one of `OverlapsSphere` / `InsideEdges` must become `true`.
- `FailingFrame_NoCottageNeighbourYieldsAcceptedWalkable` at least one neighbour cell must accept.
2. Run `dotnet test`; the inverted assertions fail (red).
3. Implement the minimal fix in the file Phase 2 identified.
4. Run `dotnet test`; the inverted assertions pass (green).
5. Run the live client; the user verifies they can walk up out of the cottage cellar.
---
## Acceptance for Phase 1 (the only Phase landing in this plan)
- [ ] `dotnet build` green
- [ ] `dotnet test --filter "FullyQualifiedName~Issue98CellarUpReplayTests"` 7 passing (no regressions)
- [ ] `dotnet test` overall baseline 1167 + 8 maintained (no new failures)
- [ ] Diagnostic commit landed (one commit, additive only no math/control-flow changes)
- [ ] Capture commit landed (acdream.log + findings note)
- [ ] Findings note names ONE branch (A / B / C / D) as the Phase 2 fix target
**Acceptance for Phase 3 (the eventual fix, captured here for the next session):**
- [ ] `Issue98CellarUpReplayTests.FailingFrame_NoCottageNeighbourYieldsAcceptedWalkable` assertion inverted to require acceptance; test passes.
- [ ] `Issue98CellarUpReplayTests.FailingFrame_CottageNeighborA_NearestWalkableIsOutsideSphereAndEdges` assertion inverted to require sphere-overlap; test passes.
- [ ] `dotnet build` green.
- [ ] `dotnet test` baseline 1167 + 8 (or better) maintained.
- [ ] User walks up out of the Holtburg cottage cellar in the live client **visual verification required**.
---
## What this plan does NOT do (per CLAUDE.md no-workarounds rule)
- Does not reattempt placement-insert bypasses in `BSPQuery.FindCollisions`, `Transition.FindEnvCollisions`, or `Transition.DoStepDown`. Six variants already tried; none worked.
- Does not reattempt cell-resolver tiebreaker changes in `PhysicsEngine.ResolveCellId`. Slice 3 already shipped a stickiness fix; the bug persists.
- Does not reattempt negative-side polygon handling. Reverted in `35b37df`; the neg-poly branch fired zero times in the failing log.
- Does not reattempt the bldg-check / IsLandblockBuilding flag propagation. Reverted in `35b37df`.
- Does not add suppression flags, grace periods, retry loops, or `if (problematicState) return` symptom guards.
If at any point during Phase 1 implementation you feel certain about the fix without having seen the new probe's branch histogram, **STOP**. The 4-session pattern was: convinced of diagnosis ship fix user reports "still can't pass." Verifying via the probe is what breaks the pattern.
---
## Self-review (writing-plans skill discipline)
**Spec coverage.** The user's prompt asks for: state altitudes (done in handoff), diagnostic-first verification (Task 1.11.3), branch-to-fix decision (Phase 2), TDD against replay tests (Phase 3), forbidden shortcut list (final section).
**Placeholder scan.** No "TBD" / "add error handling" / "similar to Task N" left in the plan. Exact file paths and line numbers throughout.
**Type consistency.** `LogStepWalkAdjust` matches `LogStepWalk`'s signature pattern. Branch tokens are stringly-typed but enumerated explicitly. `[step-walk-adjust]` prefix is distinct from `[step-walk]`.
**Realism.** Phase 1 ships one diagnostic commit + one capture commit. ~30 minutes if everything goes smoothly. Phase 2 is research (no code). Phase 3 is the actual fix separate plan. The whole arc fits in 12 working days, broken at the user-review checkpoint after Phase 1's findings note.