acdream/docs/superpowers/plans/2026-07-08-185-outdoor-stairs-fix.md
Erik 75e3b76445 docs(#185): implementation plan — red replay -> pin -> retention fix -> regress
Inline-execution plan (frozen collision internals): dat-backed seam replay as the
red pin, empirical pinning of the forward-move contact-plane loss, the localized
retail-faithful grounding-retention fix (candidate tree keyed to the pin), #137
regression net, register/digest/ISSUES bookkeeping, live gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 08:42:36 +02:00

223 lines
14 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.

# #185 Outdoor-Stairs Phantom Fix — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: this plan is intended for **inline execution by the
> lead engineer with full context** (superpowers:executing-plans), NOT subagent dispatch — it edits
> frozen collision internals and pins the fix empirically (CLAUDE.md: "Do not integrate via subagent
> unless the subagent has the full context"). Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** the local player walks up the full house-on-stilts outdoor staircase with no jam and no jump.
**Architecture:** restore retail's invariant that a grounded mover on a continuous walkable surface
keeps `contact_plane_valid` on the forward move (`transitional_insert` short-circuit, pc 273244), so
it never falls into the step-down recovery that cannot reach the *coplanar (at-level)* stair
continuation and fabricates a horizontal `(0,1,0)` sliding normal (`PrecipiceSlide`, the #137/TS-4
family). The exact retention-loss line is pinned by a dat-backed replay test, then fixed faithfully.
**Tech stack:** C# .NET 10, xUnit, `DatCollection` dat reader, `AcDream.Core.Physics`
(`PhysicsEngine.ResolveWithTransition`, `Transition`, `SpherePath`, `ShadowObjectRegistry`).
**Design spec:** `docs/superpowers/specs/2026-07-08-185-outdoor-stairs-fix-design.md` (read first).
**Fixtures already captured (this session, in the scratchpad):**
`185-recapture.jsonl` (jam pinned at `(131.71, 77.914, 61.485)`, `slidingNormal (0,1,0)`,
`collisionNormal (0,0.78,0.62)`); `gfxdump/0x01000AC5.gfxobj.json` (step-box tread = poly id 4, local
plane `(-0.625,0,0.781,-0.312)`); `[resolve-bldg]` per-object world origins, e.g. `0xF6822103 →
entOrigin_lb (132.0,77.2,60.4)`, tread world verts `(132.75,77.495,61.015)…`. **Copy the two gfxobj
dumps into `tests/AcDream.Core.Tests/Physics/fixtures/` in Task 1** so the test is self-contained.
**Reference tests to mirror (read before writing Task 1):**
`tests/AcDream.Core.Tests/Physics/Issue137CorridorSeamReplayTests.cs` (replay + probe-capture shape),
`DoorBugTrajectoryReplayTests.cs` / `CellarUpTrajectoryReplayTests.cs` (registering a **building/shadow
object** and replaying a trajectory — the outdoor-static harness #185 needs), `GfxObjDumpRoundTripTests.cs`
(loading a gfxobj + its physics polys), `ShadowObjectRegistryMultiPartTests.cs` (`RegisterMultiPart` API).
---
## Task 1: Red replay test — reproduce the wedge over the real stair objects
**Files:**
- Create: `tests/AcDream.Core.Tests/Physics/Issue185OutdoorStairsSeamReplayTests.cs`
- Create (fixtures): copy `…/scratchpad/gfxdump/0x01000AC5.gfxobj.json` (+ `0x01000ACA`) →
`tests/AcDream.Core.Tests/Physics/fixtures/`
- Read (harness API): `src/AcDream.Core/Physics/ShadowObjectRegistry.cs:92,152`
(`Register` / `RegisterMultiPart`), `PhysicsEngine.ResolveWithTransition`, and the two building-replay
reference tests above.
- [ ] **Step 1: Read the reference harness.** Read `DoorBugTrajectoryReplayTests.cs` and
`CellarUpTrajectoryReplayTests.cs` end-to-end to learn exactly how they (a) build a `PhysicsEngine`,
(b) register a building object's collision shape (`ShadowShapeBuilder`/`RegisterMultiPart` from a
Setup/GfxObj + world transform), and (c) drive `ResolveWithTransition`. Read
`GfxObjDumpRoundTripTests.cs` for how a gfxobj's `ResolvedPolygons` (the physics polys, incl. tread
poly id 4) are obtained from the dat. Confirm whether registration takes a Setup id or a GfxObj id;
the stairs are landblock statics whose collision is the GfxObj's own BSP/polys (no Setup cylsphere).
- [ ] **Step 2: Write the harness + the FAILING wedge test.** Build a `PhysicsEngine`, register the
stair step objects at their captured world transforms (each = `entOrigin_lb` translation × the +90°-about-Z
rotation that maps local tread normal `(-0.625,0,0.781)` → world `(0,-0.625,0.781)`), covering at least
the objects spanning the jam seam (`0xF68221` parts 13 at entOrigin Y 76.2/76.7/77.2 **and** the next
object up whose tread covers Y∈[77.5,78.0]+ — derive its origin as the +0.5 world-Y / +0.4 world-Z step
from `0xF68221` part 3). Seed a grounded body on tread N (contact plane + walkable poly = the tread quad,
world verts `(132.75,77.495,61.015),(131.25,77.495,61.015),(131.25,76.995,60.615),(132.75,76.995,60.615)`).
Replay the forward run from `(131.72,77.49,61.15)` toward `(131.71,78.18,61.15)` for ~6 held-forward
frames (flat target, +Y ≈ 0.2/frame — the movement controller sends flat, physics climbs). Use the
`ResolveWithTransition` args from the capture: `sphereRadius 0.48`, `sphereHeight 1.835`,
`stepUpHeight 0.6`, `stepDownHeight 1.5`, `isOnGround true`, `moverFlags IsPlayer|EdgeSlide`.
```csharp
// The wedge assertion (RED before the fix): the run must climb PAST the seam, not pin at Y≈77.9.
Assert.True(pos.Y > 78.10f,
$"Player must climb past the tread seam (reached Y={pos.Y:F3}); pinned at ~77.9 with " +
$"slidingNormal=(0,1,0) = the #185 fabricated-precipice wedge.");
Assert.False(body.TransientState.HasFlag(TransientStateFlags.Sliding),
"A continuous walkable ramp seam must not persist a horizontal sliding normal (#137 family).");
```
- [ ] **Step 3: Run it — verify it FAILS (reproduces the wedge).**
Run: `dotnet test tests/AcDream.Core.Tests --filter "FullyQualifiedName~Issue185OutdoorStairsSeamReplay" -v n`
Expected: **FAIL**`pos.Y` pins at ~77.9 and/or `Sliding` flag set (the wedge reproduced offline).
**If it does NOT reproduce** (climbs clean offline), the live trigger involves state beyond the replay's
reach (as the #137 corridor replay initially did) — STOP and add the missing seed (the exact
`bodyBefore` from `185-recapture.jsonl` tick 4277: contact plane, walkable verts, and the prior-frame
`slidingNormal`) before proceeding; do not weaken the assertion.
- [ ] **Step 4: Commit the red test.**
```bash
git add tests/AcDream.Core.Tests/Physics/Issue185OutdoorStairsSeamReplayTests.cs \
tests/AcDream.Core.Tests/Physics/fixtures/0x01000AC5.gfxobj.json \
tests/AcDream.Core.Tests/Physics/fixtures/0x01000ACA.gfxobj.json
git commit -m "test(#185): red — dat-backed outdoor-stairs seam replay reproduces the wedge
Registers the real stair step-boxes and replays the captured forward run; pins the
jam (no advance past Y~77.9, persisted (0,1,0) sliding normal). RED before the fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```
---
## Task 2: Pin the exact retention-loss site
**Files:**
- Modify (temporary probe only, reverted at end): the replay test — wrap the seam-crossing frame with
`PhysicsDiagnostics.ProbeContactPlaneEnabled = true` (env `ACDREAM_PROBE_CONTACT_PLANE`) +
`ProbeStepWalkEnabled` + `ProbeIndoorBspEnabled`, capturing `Console.Out` (exactly as
`Issue137CorridorSeamReplayTests.SeamShake_WestBoundary_SnapshotExact_Advances` does, lines 207236).
- Read: `src/AcDream.Core/Physics/TransitionTypes.cs` (TransitionalInsert loop `~10301110`
Slid-clears at 1047/1073/1102; the neg-poly/step-down dispatch `~1339`; `ValidateTransition` LKCP
retention `~46454735`), `src/AcDream.Core/Physics/PhysicsEngine.cs` (seed `~1004`, writeback `~1131`).
- [ ] **Step 1: Capture the seam-crossing frame's contact-plane transitions.** Run the red test with the
probes on; in the emitted trace, find the frame that reaches the step-down dispatch with
`ci.ContactPlaneValid == false`, and identify **which** upstream event set it false:
(a) an env/building/object channel returned **Slid** (clears cp at 1047/1073/1102), or
(b) the seed at `PhysicsEngine.cs:1004` never re-validated and the forward main-sweep set no fresh
contact plane, or
(c) `ValidateTransition`'s LKCP proximity guard (`~4698`, `radius+ε > |angle|`) DROPPED the plane
because the floated sphere is >radius from tread N's plane.
Record the concrete site (file:line + the branch) in a scratch note; this selects the Task-3 candidate.
- [ ] **Step 2: Confirm the retail-faithful expectation for that site.** Grep the named-retail decomp for
the corresponding retail function (the Slid channel → `BSPTREE::find_collisions` Contact branch
pc 323838+, or `set_contact_plane`/`get_object_info` pc 272547 for the retain path) and confirm what
retail does that keeps `contact_plane_valid` here. Do NOT invent behavior — cite the pc line. This is
the gate: the fix must make acdream match that retail behavior, not add a guard.
---
## Task 3: Apply the retail-faithful grounding-retention fix
**Files:**
- Modify: the pinned site from Task 2 in `src/AcDream.Core/Physics/TransitionTypes.cs` (or
`PhysicsEngine.cs`). Exactly ONE of the candidates below, chosen by Task 2's pin + Task 2's retail cite.
- [ ] **Step 1: Apply the fix matching the pinned site.** Candidate edits (pick the one Task 2 selected):
- **Candidate A (pin = spurious Slid on the coplanar-graze):** the grounded foot grazing the coplanar
continuation tread is being routed to a Slid (clearing cp) where retail records a walkable
contact/step (keeps cp). Fix the specific collision-response branch so a grounded mover grazing a
**walkable** (normal.Z ≥ FloorZ) coplanar poly it is resting on retains/sets the contact plane
instead of clearing it — mirroring the retail Contact-branch path Task 2 cited. Do not blanket-skip
Slid; scope to the walkable-coplanar-continuation case.
- **Candidate B (pin = forward move sets no fresh contact plane):** ensure the grounded forward move
re-establishes `ci.ContactPlane` from the coplanar tread the sphere rests on (retail's forward
landing keeps `contact_plane_valid`, pc 273244) — e.g. the walkable-landing/`find_walkable` on the
forward insert must find the at-level continuation, not only a below-foot surface.
- **Candidate C (pin = LKCP proximity guard drops the plane):** the floated sphere separates from
tread N's plane by >radius so the guard (`~4698`) drops it — but the sphere is genuinely resting on
the coplanar continuation. Fix so the proximity check is evaluated against the surface actually under
the sphere (the coplanar continuation), matching retail's `get_object_info` retain logic (pc 272547),
rather than dropping grounding at the seam.
Each candidate is a **localized** change. If the pinned faithful fix is larger than a localized edit,
STOP and return to the user (scope guard, per the spec §3).
- [ ] **Step 2: Register bookkeeping in the same commit.** Amend `docs/architecture/retail-divergence-register.md`
row **TS-4** (or add a new row) to record the grounding-retention change and its retail oracle
(pc 273244 / the Task-2 cite); retire/narrow as the port dictates.
---
## Task 4: Green the replay + remove the temporary probes
- [ ] **Step 1: Remove the temporary probe wrapping** added in Task 2 (leave the test asserting behavior,
no probe side effects).
- [ ] **Step 2: Run the red test — verify it now PASSES.**
Run: `dotnet test tests/AcDream.Core.Tests --filter "FullyQualifiedName~Issue185OutdoorStairsSeamReplay" -v n`
Expected: **PASS** — the run climbs past Y=78.10 with no persisted sliding normal.
- [ ] **Step 3: Commit the fix + green test.**
```bash
git add -A
git commit -m "fix(#185): keep the mover grounded on a continuous walkable ramp seam
<one line naming the pinned site + the retail oracle from Task 2>. The grounded
forward move now retains contact_plane_valid across the coplanar stair seam
(retail transitional_insert pc 273244) instead of dropping into the step-down
recovery that can't reach the at-level continuation and fabricates a horizontal
(0,1,0) sliding normal (PrecipiceSlide, #137/TS-4 family). Register TS-4 amended.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```
---
## Task 5: Regression — the shared grounded-move path
- [ ] **Step 1: Run the #137 suites (must stay green / un-skipped).**
Run: `dotnet test tests/AcDream.Core.Tests --filter "FullyQualifiedName~Issue137" -v n`
Expected: PASS (`Issue137CorridorSeamReplayTests`, `Issue137SlidingNormalLifecycleTests`,
`Issue137CorridorSeamInspectionTests`). Also run `CellarUpTrajectoryReplayTests`, `BSPStepUpTests`,
`SphereCollisionFamilyTests` — the step-up/step-down/slide neighbors.
- [ ] **Step 2: Full build + test.**
Run: `dotnet build src/AcDream.App/AcDream.App.csproj -c Debug` then
`dotnet test` (Core / App / UI / Net). Expected: all green, 0 warnings.
- [ ] **Step 3: If any regressed,** the fix over-reached (not scoped to the walkable-coplanar-continuation
case) — narrow it and re-run; do NOT loosen the regression assertions.
---
## Task 6: Bookkeeping (docs + memory)
- [ ] **Step 1:** `docs/ISSUES.md` — move **#185** to Recently closed with the fix SHA.
- [ ] **Step 2:** `claude-memory/project_physics_collision_digest.md` — add the #185 banner (root cause +
the fix + DO-NOT-RETRY: don't un-zero Z, don't touch step-up budget, fix the grounding provenance).
- [ ] **Step 3:** roadmap/milestones — note the M1.5 #137-family stair fix if a phase line applies.
- [ ] **Step 4: Commit the bookkeeping.**
```bash
git add docs/ISSUES.md claude-memory/project_physics_collision_digest.md
git commit -m "docs(#185): close — grounding-retention fix for the outdoor-stairs phantom
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```
---
## Task 7: Live visual gate (USER — acceptance)
- [ ] **Step 1: Build green, then launch** (canonical live command; no pre-close of running clients).
- [ ] **Step 2: USER walks `+Acdream` up the FULL outdoor staircase** — must climb with no jam and no
jump. Then regression spot-checks: indoor cellar/corridor stairs, another outdoor building, a ramp,
flat ground — no new phantom blocks AND no sliding through walkable ramps/edges.
- [ ] **Step 3:** on pass, #185 is closed (bookkeeping already landed in Task 6). On fail, capture with
the apparatus (§6 of the spec) and re-pin — do not guess a follow-up.