acdream/docs/plans/2026-08-03-recent-regression-cleanup.md
Erik 2ef02f8cbb docs: close the recent-regression cleanup plan
S1 (#284), S2 (#282), and S3 (#283) are all landed and user-accepted. S3 is
recorded as measured-unreachable rather than restructured, so the plan's
"prove or disprove before moving ownership" step is what actually decided the
outcome.

Final complete Release solution: 10,844 passed / 4 skipped / 0 failed.
Next: the original campaign order, starting at C4 route 2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:34:07 +02:00

163 lines
8.2 KiB
Markdown
Raw Permalink 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.

# Recent-regression cleanup — plan (2026-08-03)
Three defects introduced by the 2026-08-02/03 stabilization batch, found while
reconciling the #281 test failures. All three are **ours, days old, and inside
the least-verified code in the tree**. They are cleared before C4 resumes so
six more placement routes are not stacked on top of them.
Issues: #282 (two cell fields), #283 (two world origins), #284 (silent park).
## Status — CLOSED 2026-08-03
All three landed and are user-accepted.
| Slice | Issue | Commit | Outcome |
|---|---|---|---|
| S1 | #284 | `97d11e6c` | Park reasons named; terminal on the contradictory state |
| S2 | #282 | `3c36b4cc` | One cell owner (`VisibilityCellId`); register row AP-133 |
| S3 | #283 | `898ff18b``89cf1e66` | Measured UNREACHABLE; permanent invariant instead of a restructure |
Connected Release gate (retail UI) on S1+S2: user verdict "works fine"; log
showed 9 completed reveals, 58 reveal events all `failures=0`, zero unhandled
exceptions, zero parked placements, graceful exit (`0c14c402`).
S3's probe run recorded zero disagreements across 11 reveals and six landblocks
spanning ~45 km, so ownership was deliberately left alone — the evidence
disproved the hypothesis, and the guard exists to keep it disproven.
Final complete Release solution: **10,844 passed / 4 skipped / 0 failed.**
Next: the original campaign order below, starting at C4 route 2.
## Why these first
Every one is an instance of the exact weakness the placement campaign exists to
remove: **two owners of one fact, with no single writer keeping them agreed.**
#282 duplicates "which cell is this in". #283 duplicates "where is zero". #284
is why both stayed invisible. Fixing them inside C4 would mean diagnosing them
through C4's much larger diff.
## Standing discipline for this plan
- Retail is the oracle. Grep `docs/research/named-retail/acclient_2013_pseudo_c.txt`
by `class::method` before writing.
- Root causes only. No timeouts, grace periods, suppression flags, or
catch-and-ignore. #284 in particular is observability + fail-fast, never a
retry cap.
- **The complete Release solution suite must be green before every commit.**
Focused-run-only gating is exactly what let #281#284 ship. The full suite
takes about 30 seconds; there is no excuse.
- Each fix is its own bisectable commit with root-cause evidence, and updates
the issue + divergence ledgers in that same commit.
---
## S1 — #284: make a parked placement visible (do this first)
Smallest, and it turns the other two from archaeology into observation.
1. Classify the park reason at the single site that produces it
(`RuntimeSetPositionState.PrepareMover`): awaiting collision generation,
awaiting Setup, awaiting world frame.
2. Fold per-reason parked counts into the existing physics ownership snapshot
(`RuntimePhysicsState.CaptureOwnership`) so they appear wherever ledgers are
already asserted, and in the connected gates' `report.json`.
3. Fail fast on unresolvable parks. A park awaiting the world frame *while a
local player is already registered* is not a wait — it is a contradiction.
Surface it as a committed invariant exception, the pattern `01f4791e`
established for receipt-ledger violations.
4. Convergence contract: parked entries must be zero at every stable
checkpoint. Wire that into the lifecycle/nine-stop gate assertions.
**Tests:** each park reason is reported exactly once and clears on resolution;
the contradictory park throws rather than retrying; ledgers converge to zero.
**Gate:** focused Runtime + complete solution suite.
---
## S2 — #282: one owner for an entity's visibility cell
1. **Establish the retail model.** `CPhysicsObj::set_cell_id` @0x0050f4f0,
`change_cell` @0x00513390, `set_cell_id_recursive` @0x00510da0,
`ShouldDrawParticles` @0x0050fe60. Retail carries ONE cell per physics
object, and particle gating reads that same cell. Write the pseudocode note
before touching C#.
2. **Audit the writers.** 12+ sites write `ParentCellId`
(`LiveEntityNetworkUpdateController` ×4, `RemotePhysicsUpdater` ×2,
`ProjectileController` ×3, `LiveEntityOrdinaryPhysicsUpdater`,
`LocalPlayerProjectionController`, `RemoteTeleportController`, …); 3 write
`EffectCellId`, all in `LiveEntityRuntime`. For each `ParentCellId` writer
record whether it also rebuckets — a rebucket with an exact cell currently
repairs the pair by accident. Produce the table before choosing the fix.
3. **Decide the shape.** `EffectCellId`'s documented purpose is narrow: outdoor
dat stabs that keep a null render parent while retail still gives them an
outdoor landcell. Live/interior entities were explicitly meant to use
`ParentCellId`. Preferred fix, in retail's direction: live entities stop
populating `EffectCellId`, the stab case keeps it as the documented
exception, and one owner writes the visibility cell that the effects path
reads. If the audit shows live entities genuinely need it, the alternative
is a single writer that maintains both — but never 12 independent writers
against a field that wins.
4. **Divergence register.** The two-field split is an adaptation from retail's
single cell. Add the row if none exists; delete it if step 3 collapses the
split.
**Tests:** an entity crossing a cell boundary keeps its particles and lights
attached; an equipped/attached child keeps its parent-relative behaviour; the
outdoor dat stab case is unchanged.
**Gate:** focused App + complete suite, then a **user visual check** — a
monster with an active spell effect crossing a cell boundary, and a lit static
object, indoors and outdoors.
---
## S3 — #283: one owner for the world origin
Sequenced last of the three and immediately before C4 route 3, which touches
the same portal code.
1. **Prove or disprove reachability first.** With S1 landed, assert at the
placement site that Runtime's frame center and App's `LiveWorldOriginState`
center agree; run the portal/recall routes. If they never diverge in
practice, the fix is a permanent invariant rather than a behaviour change —
record that and stop. Do not restructure on a hypothesis.
2. **Retail evidence.** How retail rebases its landblock offsets across a
teleport, and the ordering around `TAS_TUNNEL_CONTINUE` — the same
sequence #280 already needs read. Read once, use twice.
3. **Fix shape.** Runtime owns the world frame; App projects it. Today
`LiveWorldOriginState` is an independent owner with its own rebase edge.
Make it a projection of Runtime's frame, so there is exactly one origin and
the retirement-driven edge becomes a *publication* of that origin rather
than a second decision. This is the same ownership move the campaign has
already applied to entities, physics, and placement.
4. **Ordering invariant.** No placement may commit against an origin the
render side has not adopted. Whether that is expressed as a gate or made
structurally impossible falls out of step 3.
**Tests:** a teleport whose old-window retirement lags by many frames cannot
commit a placement against a mismatched origin; frame and origin rebase
together; ordinary movement rebases neither (already pinned by
`RuntimeWorldFrameTests`).
**Gate:** focused + complete suite, lifecycle/reconnect, and a **user visual
check** on repeated portal/recall arrivals with objects present.
---
## After S1S3
Resume the original campaign order, unchanged:
1. **C4 routes 27** — ForcePosition, portal (with S3 landed), remote
Create/Position, projectile correction, drops, pickup/parent/delete.
Fold in #276 and #277 where their route becomes authoritative.
2. **#280** — retail destination prefetch, landed adjacent to route 3.
3. **C5** — delete superseded writers, complete suite, lifecycle/reconnect,
nine-stop soak **on the final binary**, two-client observation, the #269/#278
slope-glide check. Only then retire AP-1, AD-1, AP-131, and AD-60's legacy
half.
4. **AP-22**`ShadowShapeBuilder` as sole authority for authored Setup
collision shapes.
5. **AD-10** — remote contact-plane projection through the real transition
sweep.
6. Final movement/collision matrix; ledger, architecture, roadmap, milestones,
memory, `CLAUDE.md`, `AGENTS.md`. Vendor Slice 5 resumes only after that.