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>
This commit is contained in:
Erik 2026-07-08 08:42:36 +02:00
parent 06c3ecd89d
commit 75e3b76445

View file

@ -0,0 +1,223 @@
# #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.