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>
163 lines
8.2 KiB
Markdown
163 lines
8.2 KiB
Markdown
# 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 S1–S3
|
||
|
||
Resume the original campaign order, unchanged:
|
||
|
||
1. **C4 routes 2–7** — 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.
|