acdream/docs/superpowers/plans/2026-06-02-phase-w-membership-flicker-fix.md
Erik 50b168bc1e docs(render): Phase W chunk-1 plan — transition-owned membership flicker fix
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>
2026-06-02 14:04:45 +02:00

15 KiB
Raw Blame History

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 §13. 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 35).

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 35).


File Structure

  • Modify src/AcDream.Core/Physics/PhysicsDiagnostics.cs — add the ProbeSweptEnabled flag (mirror ProbeCellEnabled).
  • Modify src/AcDream.Core/Physics/PhysicsEngine.cs — add the [cell-swept] probe in ResolveWithTransition (Task 1); switch the two return sites to the swept cell (Task 2).
  • Revert commit 2acd8f9 (W2b) — PhysicsEngine.cs prune/helper/const + CellGraphMembershipTests.cs hysteresis tests (Task 3).
  • Tests: tests/AcDream.Core.Tests/Physics/PhysicsDiagnosticsTests.cs (Task 1 flag); a doorway replay regression in tests/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.cs and find ProbeCellEnabled (env var ACDREAM_PROBE_CELL, a static bool read at startup + runtime-toggleable). Read PhysicsEngine.cs:867-899 (the existing ProbeResolveEnabled [resolve] block) — you will mirror its shape. Confirm SpherePath exposes CurCellId, 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 PhysicsDiagnosticsTests asserts 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 (ProbeSweptEnabled undefined).

  • Step 4: Add the flag. In PhysicsDiagnostics.cs, mirror ProbeCellEnabled exactly, for env var ACDREAM_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, AFTER ok is known and sp/ci are in scope (just after the existing ProbeResolveEnabled block ~:899), add — note this logs both swept fields and the position; it does NOT call ResolveCellId (which has a CellGraph.CurrCell side-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" src shows 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 builds new ResolveResult(sp.CheckPos, ResolveCellId(sp.GlobalSphere[0].Origin, sphereRadius, sp.CheckCellId), …) (~:907-912) and the partial path uses partialCellId + ResolveCellId(...) (~:925-931). Confirm SetCurrAndReturn(uint) (:265) writes DataCache.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 W2a CurrCell write 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 ResolveCellId still has its seed callers. rg "ResolveCellId\(" src — there must remain at least the spawn/teleport/server-set seed path outside ResolveWithTransition (e.g. in PlayerMovementController/GameWindow). If ResolveWithTransition was the ONLY caller, leave ResolveCellId in 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 DoorwayHoldMargin band + SphereOverlapsEnvCell + the prune block inside ResolveCellId, 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" src returns nothing.

  • Step 3: Commit (if git revert already 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 via Tee-Object to launch-w-stage1.log. Run in the background.
  • Step 3: STOP for the user. The user walks +Acdream to the cottage front-door threshold, jitters/straddles it, then walks room↔cellar↔outside. Acceptance: [cell-transit] stays stable (no 0170↔0031/0170↔0171 flip at a static spot); [cell-swept] shows curCell matching (it was the stable source all along). Read launch-w-stage1.log with PowerShell Get-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 35).

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.cs to see how a captured ResolveCall* JSONL record is loaded and replayed through engine.ResolveWithTransition(...), and how the returned ResolveResult.CellId is 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 under tests/AcDream.Core.Tests/Fixtures/ (mirror the issue98 fixture layout). Keep it small (the jitter run, ~2040 records).
  • Step 3: Write the regression test that replays the fixture through ResolveWithTransition and asserts no immediate ping-pong: across the threshold-jitter run, the returned CellId must not flip A→B→A within 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 static ResolveCellId path 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.md Phase W table: W2 reframed (transition-owned membership shipped; W2b reverted as superseded). Note chunk 2 (render: Stages 35) 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 35 (render) are explicitly deferred to chunk 2 — this plan is scoped to the flicker fix. ✓
  • Placeholders: Tasks 13 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, and ResolveResult.Position/onGround are untouched. ✓