Bite-sized TDD plan for design Stages 0-1 + W2b revert + visual gate: add the [cell-swept] diagnostic, return the swept sp.CurCellId from ResolveWithTransition (retail SetPositionInternal), revert the superseded W2b hysteresis, visual-gate the doorway/cellar strobe, then lock it with a doorway replay regression. Render chunk (Stages 3-5) gets its own spec+plan after this gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
15 KiB
Phase W (rev) — Chunk 1: Transition-owned membership (flicker fix) Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use
- [ ].
Goal: Stop the cell-membership ping-pong (0xA9B40170↔0xA9B40031 at the cottage door, plus vestibule↔room and cellar) by returning the transition's swept cell from ResolveWithTransition instead of statically re-deriving it — matching retail SetPositionInternal.
Architecture: Design docs/superpowers/specs/2026-06-02-phase-w-transition-membership-and-pview-render-design.md §1–3. Retail carries sphere_path.curr_cell through the collision sweep (validate_transition advances on an accepted move, reverts on a block) and commits it; it never re-derives from the resting position. acdream already latches the swept cell in SpherePath.CurCellId via Transition.ValidateTransition (TransitionTypes.cs:3404-3434) and CheckOtherCells (:2061-2075); the only defect is that PhysicsEngine.ResolveWithTransition discards it and re-derives via ResolveCellId(sp.GlobalSphere[0].Origin, …) (PhysicsEngine.cs:909/:928). This chunk fixes the consumer; it does not touch collision math, the CELLARRAY/do_not_load_cells prune (Stage 2, secondary), or render (Stages 3–5).
Tech Stack: C# .NET 10, xUnit. Branch claude/thirsty-goldberg-51bb9b (unpushed — do NOT push). Every commit ends with Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>.
Acceptance: the decisive gate is visual — [cell-transit] stays stable at the doorway/cellar (no ping-pong) while the player stands/jitters at the threshold, and walking room↔cellar↔outside produces clean single transitions. No regression to collision or outdoor walking. Chunk 1 fixes the strobe; it does NOT seal the interior (the bluish/unsealed look is Stages 3–5).
File Structure
- Modify
src/AcDream.Core/Physics/PhysicsDiagnostics.cs— add theProbeSweptEnabledflag (mirrorProbeCellEnabled). - Modify
src/AcDream.Core/Physics/PhysicsEngine.cs— add the[cell-swept]probe inResolveWithTransition(Task 1); switch the two return sites to the swept cell (Task 2). - Revert commit
2acd8f9(W2b) —PhysicsEngine.csprune/helper/const +CellGraphMembershipTests.cshysteresis tests (Task 3). - Tests:
tests/AcDream.Core.Tests/Physics/PhysicsDiagnosticsTests.cs(Task 1 flag); a doorway replay regression intests/AcDream.Core.Tests/Physics/from a capture taken at the gate (Task 5).
Task 1: Stage-0 diagnostic probe ([cell-swept]) — zero behavior change
Files: Modify src/AcDream.Core/Physics/PhysicsDiagnostics.cs, src/AcDream.Core/Physics/PhysicsEngine.cs; Test tests/AcDream.Core.Tests/Physics/PhysicsDiagnosticsTests.cs.
-
Step 1: Read the existing pattern. Read
PhysicsDiagnostics.csand findProbeCellEnabled(env varACDREAM_PROBE_CELL, a static bool read at startup + runtime-toggleable). ReadPhysicsEngine.cs:867-899(the existingProbeResolveEnabled[resolve]block) — you will mirror its shape. ConfirmSpherePathexposesCurCellId,CheckCellId,CurPos,CheckPos(TransitionTypes.cs:328-336). Report if any differ. -
Step 2: Write the failing test in
tests/AcDream.Core.Tests/Physics/PhysicsDiagnosticsTests.cs(add to the existing class; mirror how the existing tests assert a probe flag default). Example:
[Fact]
public void ProbeSweptEnabled_DefaultsToFalse()
{
// The swept-cell probe (ACDREAM_PROBE_SWEPT) must be OFF unless explicitly enabled,
// so production pays only one static-bool read per ResolveWithTransition call.
PhysicsDiagnostics.ProbeSweptEnabled = false;
Assert.False(PhysicsDiagnostics.ProbeSweptEnabled);
}
If
PhysicsDiagnosticsTestsasserts flags differently (e.g. via an env-parse helper), match that style. Keep the assertion: the new flag exists and defaults false.
-
Step 3: Run, verify FAIL:
dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj -c Debug --filter "FullyQualifiedName~PhysicsDiagnosticsTests"— fails to compile (ProbeSweptEnabledundefined). -
Step 4: Add the flag. In
PhysicsDiagnostics.cs, mirrorProbeCellEnabledexactly, for env varACDREAM_PROBE_SWEPT:
/// <summary>
/// Phase W Stage 0 (2026-06-02): one [cell-swept] line per ResolveWithTransition call —
/// the transition's swept cell (sp.CurCellId/CheckCellId) vs the position-derived cell the
/// (legacy) static ResolveCellId path used. Lets a single doorway walk prove the swept cell
/// is stable where the static one strobes (Codex "verify before delete"). Env ACDREAM_PROBE_SWEPT=1.
/// </summary>
public static bool ProbeSweptEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_SWEPT") == "1";
-
Step 5: Run, verify PASS (same filter).
-
Step 6: Add the probe line. In
PhysicsEngine.ResolveWithTransition, AFTERokis known andsp/ciare in scope (just after the existingProbeResolveEnabledblock ~:899), add — note this logs both swept fields and the position; it does NOT callResolveCellId(which has aCellGraph.CurrCellside-effect) and does NOT change the return:
if (PhysicsDiagnostics.ProbeSweptEnabled)
{
Console.WriteLine(System.FormattableString.Invariant(
$"[cell-swept] ent=0x{movingEntityId:X8} ok={ok} inCell=0x{cellId:X8} " +
$"curCell=0x{sp.CurCellId:X8} checkCell=0x{sp.CheckCellId:X8} " +
$"curPos=({sp.CurPos.X:F3},{sp.CurPos.Y:F3},{sp.CurPos.Z:F3}) " +
$"checkPos=({sp.CheckPos.X:F3},{sp.CheckPos.Y:F3},{sp.CheckPos.Z:F3})"));
}
- Step 7: Build green + confirm zero behavior change.
dotnet build src/AcDream.Core/AcDream.Core.csproj -c Debug(0 errors).rg "ProbeSweptEnabled" srcshows only the declaration + this read. Commit:
git add src/AcDream.Core/Physics/PhysicsDiagnostics.cs src/AcDream.Core/Physics/PhysicsEngine.cs tests/AcDream.Core.Tests/Physics/PhysicsDiagnosticsTests.cs
git commit -m "feat(core): Phase W Stage 0 — [cell-swept] diagnostic (swept vs static cell, no behavior change)"
Task 2: Stage-1 transition-owned membership return (BEHAVIOR-CHANGING)
Files: Modify src/AcDream.Core/Physics/PhysicsEngine.cs.
-
Step 1: Confirm the return sites + the W2a wiring. Read
PhysicsEngine.ResolveWithTransition:901-932. Confirm the OK path buildsnew ResolveResult(sp.CheckPos, ResolveCellId(sp.GlobalSphere[0].Origin, sphereRadius, sp.CheckCellId), …)(~:907-912) and the partial path usespartialCellId+ResolveCellId(...)(~:925-931). ConfirmSetCurrAndReturn(uint)(:265) writesDataCache.CellGraph.CurrCell = GetVisible(id)then returns the id (this is the W2a render-read wiring we must KEEP). Report the exact lines. -
Step 2: Switch both return sites to the swept cell via
SetCurrAndReturn(preserves the W2aCurrCellwrite that render reads). OK path:
resolveResult = new ResolveResult(
sp.CheckPos,
// Phase W Stage 1: return the transition's SWEPT cell (retail SetPositionInternal
// reads sphere_path.curr_cell), not a static re-derive from the resting origin.
// ValidateTransition advances sp.CurCellId only on accepted moves and reverts on
// blocks, so a push-back/standing-still cannot flip it. SetCurrAndReturn keeps the
// W2a CellGraph.CurrCell write the render root consumes.
SetCurrAndReturn(sp.CurCellId),
onGround,
collisionNormalValid,
collisionNormal);
Partial path (replace the ResolveCellId(...) call; keep the partialCellId fallback for the degenerate CurCellId == 0 case):
resolveResult = new ResolveResult(
sp.CheckPos,
SetCurrAndReturn(sp.CurCellId != 0 ? sp.CurCellId : partialCellId),
partialOnGround,
collisionNormalValid,
collisionNormal);
Leave everything else (partialCellId computation, onGround, the probes) unchanged. Do NOT delete ResolveCellId — it stays for the seed/teleport callers (verify in Step 3).
-
Step 3: Confirm
ResolveCellIdstill has its seed callers.rg "ResolveCellId\(" src— there must remain at least the spawn/teleport/server-set seed path outsideResolveWithTransition(e.g. inPlayerMovementController/GameWindow). IfResolveWithTransitionwas the ONLY caller, leaveResolveCellIdin place anyway (seed path may call it later) and note it as DONE_WITH_CONCERNS. Do not delete it. -
Step 4: Build + full Core suite.
dotnet build src/AcDream.Core/AcDream.Core.csproj -c Debug(0 errors).dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj -c Debug— compare failures to the documented static-leak/stale-capture baseline (PhysicsResolveCapture/PhysicsDiagnostics statics; CellarUp/DoorBug document-the-bug captures). Report any NEW failure in a cell/physics/transition class (that would be a regression). The #98 cottage-floor-cap regression test must still pass. -
Step 5: Commit:
git add src/AcDream.Core/Physics/PhysicsEngine.cs
git commit -m "feat(core): Phase W Stage 1 — return swept sp.CurCellId from ResolveWithTransition (retail SetPositionInternal)"
Task 3: Revert W2b (superseded)
Files: Revert commit 2acd8f9.
- Step 1: Revert. W2b (the
DoorwayHoldMarginband +SphereOverlapsEnvCell+ the prune block insideResolveCellId, plus its 3 hysteresis tests) is superseded — the prune was the wrong mechanism in the wrong place (design §1 corollary). Revert it cleanly:
git revert --no-edit 2acd8f9
If the revert conflicts (because Task 2 edited nearby lines in PhysicsEngine.cs), resolve by keeping Task 2's swept-return changes and removing ONLY W2b's additions (DoorwayHoldMargin const, SphereOverlapsEnvCell helper, the if (DataCache?.CellGraph.CurrCell is …EnvCell prevCell …) return SetCurrAndReturn(prevCell.Id); block in the outdoor branch) and the W2b tests in CellGraphMembershipTests.cs. The W2 Task-1 test (ResolveCellId_Resolved_WritesCurrCellTrackingTheResolvedId) and SetCurrAndReturn STAY.
-
Step 2: Build + suite green (same commands as Task 2 Step 4). Confirm
rg "DoorwayHoldMargin|SphereOverlapsEnvCell" srcreturns nothing. -
Step 3: Commit (if
git revertalready committed, amend the message or leave it; if resolved manually, commit):
git commit -m "revert(core): Phase W — drop W2b doorway hysteresis (superseded by transition-owned membership)" --allow-empty
(Skip if git revert --no-edit already produced the commit.)
Task 4: Visual gate (STOP — the acceptance test) + capture
- Step 1: Build the App.
dotnet build src/AcDream.App/AcDream.App.csproj -c Debug— 0 errors before launching. - Step 2: Launch with the probes + a resolve capture (per CLAUDE.md "Running the client"), adding
ACDREAM_PROBE_CELL=1 ACDREAM_PROBE_SWEPT=1 ACDREAM_CAPTURE_RESOLVE=<repo>/doorway-capture.jsonl, piped viaTee-Objecttolaunch-w-stage1.log. Run in the background. - Step 3: STOP for the user. The user walks
+Acdreamto the cottage front-door threshold, jitters/straddles it, then walks room↔cellar↔outside. Acceptance:[cell-transit]stays stable (no0170↔0031/0170↔0171flip at a static spot);[cell-swept]showscurCellmatching (it was the stable source all along). Readlaunch-w-stage1.logwith PowerShellGet-Content/ the ripgrep Grep tool (UTF-16 — NOT GNU grep). Do not proceed until the user confirms the strobe is gone. Remind the user the interior will still look unsealed/bluish (that's Stages 3–5).
Task 5: Lock the fix with a doorway replay regression test
Files: Test in tests/AcDream.Core.Tests/Physics/ (new DoorwayMembershipReplayTests.cs) + a fixture from the Task-4 capture.
- Step 1: Confirm the replay harness API. Read
tests/AcDream.Core.Tests/Physics/CellarUpTrajectoryReplayTests.cs+src/AcDream.Core/Physics/PhysicsResolveCapture.csto see how a capturedResolveCall*JSONL record is loaded and replayed throughengine.ResolveWithTransition(...), and how the returnedResolveResult.CellIdis read. Report the exact load + replay calls. - Step 2: Distill a fixture. From
doorway-capture.jsonl(Task 4), extract the contiguous run of records at the threshold (the ~8 cm jitter band, world XY ≈ (155.x, 16.x)). Save a trimmed fixture undertests/AcDream.Core.Tests/Fixtures/(mirror the issue98 fixture layout). Keep it small (the jitter run, ~20–40 records). - Step 3: Write the regression test that replays the fixture through
ResolveWithTransitionand asserts no immediate ping-pong: across the threshold-jitter run, the returnedCellIdmust not flipA→B→Awithin a short window while the position stays within the jitter band (assert the distinct-cell-transition count over the run is ≤ 1, matching retail's accept-on-move). Use the exact harness calls from Step 1. (This is the automated lock; the visual gate is the primary acceptance.) - Step 4: Run, verify PASS.
dotnet test … --filter "FullyQualifiedName~DoorwayMembershipReplayTests". (Sanity: confirm the SAME fixture replayed through the old staticResolveCellIdpath would have flipped — note it in the test doc comment; do not keep the old path.) - Step 5: Commit:
git add tests/AcDream.Core.Tests/Physics/DoorwayMembershipReplayTests.cs tests/AcDream.Core.Tests/Fixtures/<doorway-fixture>
git commit -m "test(core): Phase W Stage 1 — doorway replay regression locks no-ping-pong membership"
Task 6: Roadmap + handoff
- Update
docs/plans/2026-04-11-roadmap.mdPhase W table: W2 reframed (transition-owned membership shipped; W2b reverted as superseded). Note chunk 2 (render: Stages 3–5) is next, pending its own spec. - Commit the roadmap update.
Self-Review
- Spec coverage: design Stage 0 (Task 1), Stage 1 (Task 2), W2b revert (Task 3, design §1 corollary), visual gate (Task 4, design §6), automated lock (Task 5). Stage 2 (CELLARRAY parity) + Stages 3–5 (render) are explicitly deferred to chunk 2 — this plan is scoped to the flicker fix. ✓
- Placeholders: Tasks 1–3 carry concrete code. Tasks 1/2/5 have "confirm the live API first" steps because the probe pattern, the exact return-site lines, and the replay-harness API are verify-then-use against real code (the W2-plan pattern); concrete code/assertions are supplied for each. Task 5's fixture depends on the Task-4 capture by design (evidence-first). ✓
- Type consistency:
SpherePath.CurCellId/CheckCellId/CurPos/CheckPos,SetCurrAndReturn(uint)→uint(W2a),ResolveResult(Vector3, uint, bool, bool, Vector3),PhysicsDiagnostics.ProbeSweptEnabled(new). ✓ - No collision-math change: Task 2 changes only the returned cell id; the sweep,
FindEnvCollisions,CheckOtherCells, andResolveResult.Position/onGroundare untouched. ✓