# C5b — classify-before-merge on every steady-state Position (#275): pinned contract (2026-08-05) Written at HEAD `02578441` (branch `claude/acdream-physics-divergence-5aa784`; C5a landed at `6921a027`, #319 at `392c1e22`, AP-145/#318 at `f8e55ba5`). Every symbol below was re-verified **by symbol at this HEAD**, not inherited; §12 lists what the C5 scoping (`2026-08-05-c5-scoping.md`, written at `52175aa1`) got wrong. Inputs: the scoping's C5b section, register rows AP-131 / AD-60 / AP-130 / AP-135 / AP-146, the 4b-3 contract (`2026-08-04-c4-route-4b-3-contract.md` — D1 is C5b's direct ancestor), the C4 closeout handoff's seven process findings, the C5a contract (`2026-08-05-c5a-contract.md`), and issue #275. **One-paragraph verdict:** C5b is smaller than the scoping feared, because of a fact the scoping did not state: the merge's two flags (`installPlacementFrame` / `clearParent`) are a pure function of the **timestamp disposition** and the **hasAnimations proxy** — both available inside the merge itself, pre-merge, with no routing outcome and no `playerDistance`. Retail runs `unset_parent` and the `SetPlacementFrame` gate BEFORE `MoveOrTeleport` is consulted, so the flags are upstream of classification proper. C5b therefore needs **no route plumbing, no signature changes, no App→Runtime threading** — it replaces two `true` literals with a four-row truth table the classifier already encodes, and flips one `refreshPosition:` argument to `false`. The blast radius is correspondingly narrower than "45+ sites mis-reading a withheld wire cell", for a second reason the scoping missed entirely: **the merge stamp is one of THREE steady-state wire-cell writers, and the other two stay** (§5, W2/W3). What genuinely changes is the classification window, the refused-ForcePosition shape, and the (test-only) missile arm. The contract pins all of this. --- ## 1. Retail ground truth — Gate A and the pre-placement sequence, verified in `acclient_2013_pseudo_c.txt` `SmartBox::HandleReceivedPosition` @0x00453FD0 (pseudo-C lines 92896–93051). The exact order, with addresses: ``` 00453fe3 objcell_id = arg3->objcell_id // wire cell read into a LOCAL 00453ff4 Frame::operator=(&var_40, &arg3->frame) // wire frame copied into local Position var_48 0045400c GATE A: if (arg2 == player && newer_event(player, FORCE_POSITION_TS, arg9)): 00454044 wrapped-compare update_times[4] (TELEPORT_TS) vs arg8 — teleport must NOT be newer 00454056-68 get_heading / Frame::set_heading(&var_40) // preserve body heading 00454074 SmartBox::BlipPlayer(this, &var_48) 00454079 player->update_times[0] = arg7 // stamp POSITION_TS 00454091 cmdinterp->SendPositionEvent() 0045409d return // BEFORE unset_parent / SetPlacementFrame 004540b7 if (!newer_event(arg2, POSITION_TS, arg7)) → return (the 004540e6 teleport-regression quirk stamps update_times[0] and returns @004540f6) 004540f9-11e if (parented && parent->id != player_id): weenie->SetParentedState(0) notification 00454129 CPhysicsObj::unset_parent(arg2) // UNCONDITIONAL on this path 00454137 if (CPhysicsObj::HasAnims(arg2) == 0): 00454142 CPhysicsObj::SetPlacementFrame(arg2, arg4, 1) 0045414d if (arg2 != player): // REMOTE branch 00454254 if (MoveOrTeleport(arg2, &var_48, arg8, arg5, arg6) != 0): 00454272 ConstrainTo(arg2, &arg2->m_position, …) // post-placement anchor 0045415f else if TELEPORT_TS newer: TeleportPlayer @00454168, ConstrainTo @0045418a anchored at the WIRE &var_48, set_velocity(0) @004541b4 else: ConstrainTo @004541ec; if UsePositionFromServer && arg5: InterpolateTo @0045422c ``` Five facts decide C5b: 1. **The wire cell is never written to the object.** `arg3->objcell_id` is read into a local @0x00453FE3 and flows only into the local `var_48` Position handed to `BlipPlayer` / `TeleportPlayer` / `MoveOrTeleport` / `ConstrainTo`. Nowhere in this function is the object's cell assigned. The object's `cell` changes only inside the placement family (`SetPositionInternal` @0x00515BD0 → `set_cell`; `enter_world` @0x00516310/0x00516170) or per-frame movement transit. This is the retail rule AD-60's executor half already encodes ("a wire position never directly makes the record resident"). 2. **Gate A (@0x0045400C) is decided on data that exists before any merge:** the entity is the player, FORCE_POSITION_TS advanced, TELEPORT_TS equal. It returns @0x0045409D **before `unset_parent` @0x00454129 and before the `SetPlacementFrame` gate @0x00454137** — a ForcePosition never unparents and never installs a placement frame. acdream's exact analog exists upstream: `PhysicsTimestampGate.TryAcceptPositionEvent:190-203` produces the `ForcePosition` disposition only when `isLocalPlayer && IsNewer(FORCE_POSITION_TS) && teleport == _timestamps[Teleport]` — retail's Gate A pair, verified at HEAD. **A remote entity can never receive the `ForcePosition` disposition** (`isLocalPlayer` guard at `:190`), so the flag truth table below needs no entity-kind term. 3. **The two pre-placement writes are gated on exactly two facts.** `unset_parent` @0x00454129 runs for every accepted non-Gate-A Position (including packets `MoveOrTeleport` will return 0 for — retail unparents BEFORE learning the routing outcome). `SetPlacementFrame` @0x00454142 is gated on `HasAnims(arg2) == 0` alone. **Neither gate reads the near/far/teleport classification** — which is why acdream's merge flags are a function of (disposition, hasAnimations) and nothing else. 4. **`MoveOrTeleport` @0x00516330 reads the BODY's own state at entry:** TELEPORT_TS @0x00516375 and `this_1->cell == 0` @0x00516386 — the committed cell, read before any placement — then contact `arg4` @0x0051638E. Route 4b-3's D1 fed the classifier the PRE-merge committed cell for exactly this reason; C5b's withhold (D2) makes that pre-merge value structurally equal to the record's `FullCellId` in the classification window (§4, "strengthened invariant"). 5. **`store_position` @0x00515CE2 is `SetPositionInternal`'s no-transition branch** (placement ran, no walkable transition → `GotoLostCell` @0x00515CF2), not a `MoveOrTeleport` path. A retail placement that fails leaves the wire pose in `m_position` and the object HIDDEN in the lost cell — acdream's surviving wire-cell-on-non-commit behaviour (W2, §5) is the visible variant of this, already filed across AP-136/AP-138/#309. C5b does not re-litigate it. ## 2. The two scoping-identified sites, re-verified at HEAD `02578441` ### S1 — the steady-state merge's unconditional flags `InboundPhysicsStateController.TryApplyPosition` (`src/AcDream.Runtime/Entities/InboundPhysicsStateController.cs:610`), hardcoding `installPlacementFrame: true, clearParent: true` at **`:662-663`** (scoping exact). Production chain at HEAD, every hop verified: `LiveEntityNetworkUpdateController.OnPosition:1948` → `LiveEntityInboundAuthorityGate.TryAcceptPosition:161` (`src/AcDream.App/Physics/` — the scoping gave no directory) → `LiveEntityRuntime.TryApplyPosition:2550` → `RuntimeEntityObjectLifetime.TryApplyPosition:1789` (scoping said `:1781`) → `RuntimeEntityDirectory.TryApplyPosition:654` → the S1 overload. The route-flag-threaded sibling (`ApplyAcceptedPositionSnapshot:673`, flags at `:681-682`) already exists and is what the continuation executor uses (`RuntimeInitialCreateContinuationExecutor.cs:1983-1993`, classified flags at `:1991-1992`, withhold at `:1996`) — scoping §1b verified exact. **What changes semantically (not "what gets deleted"):** the merged snapshot's `Physics.AnimationFrame` / `PlacementId` and its `ParentGuid` / `ParentLocation` / `Physics.Parent` stop being unconditionally wire-installed / wire-cleared and start obeying retail's two gates: | disposition | installPlacementFrame | clearParent | classifier row (cite, don't restate) | |---|---|---|---| | `Rejected` | unread (timestamp-only path `:793-794`) | unread | — | | `ForcePosition` (local player only, teleport-equal) | **false** | **false** | `RuntimeAuthoritativePositionRouteClassifier.cs:311-333` (Gate A) | | `Apply` | **`!hasAnimations`** | **true** (unchanged) | classifier `:336-465` — every accepted non-force route carries `UnparentBeforeRouting: true, ApplyPlacementFrameBeforeRouting: !request.HasAnimations` | `hasAnimations` is the AP-130 static proxy, computed from the PRE-merge snapshot `old` with the identical expression `RuntimeAcceptedPositionRouteRequests.cs:99-101` uses (`old.MotionTableId ?? old.Physics?.MotionTableId) is { } id && id != 0u`). `WorldSession.EntitySpawn.MotionTableId` exists (`WorldSession.cs:103`); the inbound controller's `_snapshots[guid]` and `canonical.Snapshot` are the same data pre-merge, so the two computations cannot diverge. Two observability notes that decide the test plan (§8): - The `ForcePosition` placement-frame half is **inert**: for `disposition is not Apply`, `appliedPlacement` keeps `old.PlacementId` under either flag value (`:815-819`). Gate A's force half discriminates ONLY through parent retention (`:855-857`). - The `Apply` placement-frame half is live exactly when `hasAnimations == true`: today the wire frame (`update.PlacementId ?? 0u`) is stamped onto an animated entity's snapshot where retail's `HasAnims` gate skips `SetPlacementFrame`. That is AP-131's animation snap/reset half. ### S2 — the wire-acceptance-derived `FullCellId` stamp `RuntimeEntityObjectLifetime.cs:1923-1926` — `Entities.RefreshSnapshot(canonical, snapshot, refreshPosition: acceptedPosition)` (argument at `:1926`, matching the C5a contract's corrected citation; the register's `:1338` and the scoping's `:1918` are both stale — cite by symbol). `refreshPosition: true` routes through `RuntimeEntityRecord.RefreshDerivedState:230-237` → `SetFullCell(position.LandblockId, …)` — the wire cell, stamped on bare acceptance, plus `PropagateFullCellToChildren` (`RuntimeEntityDirectory.cs:240-246`) when it changes. **What changes semantically:** `refreshPosition: false`. The merge stops being a residency writer. In the window between the merge and the OnPosition prologue rebucket, `canonical.FullCellId` IS the pre-merge committed cell — which is exactly what 4b-3's D1 threads out-of-band today (`timestamps.PreMergeCommittedCellId`, `:1904-1907`). Post-D2, 4b-3's convention becomes a structural property of the record. **The executor's analogue is unchanged and untouched** (`refreshPosition: false` at executor `:1996` — its CANONICAL CELL SEMANTICS comment at `:1966-1976` is the model for S2's new comment). ## 3. Design decisions — pinned, not open for redesign ### D1 — the merge flags become the classifier's truth table, computed pre-merge inside the merge In S1's `TryApplyPosition`, replace the two literals at `:662-663` with the table in §2-S1, computed from `disposition` and `hasAnimations(old)`. No signature changes; no new parameters; no route construction in the merge; `playerDistance` is never needed (the full route's near/far decision is downstream of retail's unset_parent/SetPlacementFrame order and stays post-merge in `ClassifyRemoteAcceptedPosition`). The post-merge classification (`LiveEntityNetworkUpdateController.cs:2167-2175` → `RuntimeEntityObjectLifetime.ClassifyRemoteAcceptedPosition` → `RuntimeAcceptedPositionRouteRequests.Build`) is **untouched**: it computes the full route from the same retained inputs, so its `ApplyPlacementFrameBeforeRouting` / `UnparentBeforeRouting` fields equal the merge's flags by construction. The consistency is pinned by test (§8 test 4), not by a shared code path — the two computations are small, pure, and separately sabotage-verifiable. Doc comments that must be made true in the same commit (process rule (c)): the `:595-608` "Round 4 R4-15 … no HasContact or route-classification concept" remarks; the `:657-661` "Legacy immediate-apply reproduces EXACT prior behavior" comment; `ApplyAcceptedPosition`'s `:772-781` two-caller doc; the `:809-814` PositionPack comment's "retail skips it entirely while HasAnimations is true" (already true — now load-bearing for BOTH callers). ### D2 — the steady-state merge withholds the wire cell (`refreshPosition: false`) One argument flip at S2 plus the comment rewrite. The 4b-3 D1 comment at `:1894-1907` is extended, not deleted: it currently explains why the classifier needs the threaded pre-merge value; it must add that after this change the post-merge record CARRIES that same value through the classification window, making the threading a belt-and-suspenders duplicate that stays (the threaded value remains the classifier's input of record — process finding (b), read the observable once). **Two publish-side effects, both pinned (trap T3):** 1. The merge's publish-kind ternary (`:1948`, `beforeCell != canonical.FullCellId ? Rebucketed : Updated`) becomes always-`Updated` — the merge can no longer change the cell. The `Rebucketed` edge for a wire-cell-changing packet moves to the prologue rebucket's `CommitRebucket` (`RuntimeEntityObjectLifetime.cs:1956-1987`), which TODAY early-outs publish-less (`previous == fullCellId` at `:1973` — because the merge already stamped it) and after D2 publishes. **Net observable: exactly one `Rebucketed` delta per cell-changing accepted Position — same kind, same value, same publisher, later point in the same call.** A test pins this at the observable (one delta, not zero, not two); it does not pin the source site. 2. `PropagateFullCellToChildren` for a parent whose accepted Position changes its cell moves from the merge's directory wrapper (`RuntimeEntityDirectory.cs:240-246`) to `CommitRebucket`'s public `SetFullCell` (`:355`). Same packet, same value, same single propagation. This is the #319 child-equality channel — a directory-level test pins exactly one propagation per parent cell change on the steady-state path, for BOTH parent classes (player `0x5…` and creature `0x8…`; process rule from #319 — a test population that only ever sees sequence-0 parents is blind to the player class). ### D3 — the other two wire-cell writers STAY, and the register says so The scoping's premise — withholding S2 "moves residency changes onto placement/simulation commits only" — is **false at HEAD** (§12 item 1). Two more steady-state wire-cell writers exist downstream of the merge in `OnPosition`, and C5b keeps both: - **W2 — the prologue rebucket** (`LiveEntityNetworkUpdateController.cs:2329` `RebucketLiveEntity(update.Guid, p.LandblockId)` → `LiveEntityRuntime.cs:935-945` → `CommitRebucket` → `SetFullCell(wire cell)`). Runs for EVERY classification reaching the generic tail — near, far, teleport, cell-less, unrouted, and every ordinary local-player Apply echo. Its other jobs (draw-bucket move, bucket-promotion recovery, `prepare_to_enter_world` clock edges) are not cell writes and are not in scope. **Do NOT gate W2 by classification (trap T5):** for the local player W2's echo-driven commit is the mechanism that advances the canonical cell across landblocks during ordinary movement (AP-146/#320 machinery — the per-tick projection deliberately passes a landblock id and preserves the cell, `LiveEntityRuntime.cs:935-938`); gating it silently freezes the player's cell between teleports and #319's child equality inherits the freeze. - **W3 — the post-routing wire-cell adopt** (`TryAdoptWireCellAfterRouting`, `LiveEntityNetworkUpdateController.cs:1613-1624`): `RemoteMotion.CellId = wire` for every non-placing arm, writing through to `FullCellId` via `CommitCanonicalCell`. Already filed as retained bookkeeping under **AP-135**; the placing arms (far/teleport) are suppressed there because the placement receipt is the cell authority. W2/W3's population note: packets that RETURN before `:2329` never see W2 — the force-local path (Committed/Deferred `:2055`, Rejected/Contention `:2066`) and the missile arm (`:2256`). For those, post-D2 residency is placement-receipt-authoritative (or, on a refused/contended force, unchanged at the last commit — retail's shape, since retail's `BlipPlayer`-era body keeps its last placed cell; AD-62 already files the non-commit force outcomes). This is D2's genuinely new steady-state surface and it is **retail-correcting**, asserted by §8 test 6. ### D4 — register bookkeeping, in the implementation commit - **AP-131 RETIRES** (§7 evidence). Its "legacy caller is deleted at the production cutover" framing is overtaken: the caller is not deleted, it is **corrected** — the unconditional literals are replaced by the classified computation. The retirement commit rewrites the row's text to past tense with the evidence, per register rule 1 (a deviation found without a row is a bug twice over; a retired divergence needs its retirement recorded, which the C5a/AP-1 and 4b-3/AP-137 rewrites established as the house style). - **AD-60 — legacy half RETIRES, row REWRITTEN, not deleted** (4b-3's D8 precedent: silent whole-row deletion would hide surviving wire-cell channels). The rewrite keeps the executor half as the standing rule, records the legacy half's retirement with §7's evidence, and **names the surviving wire-cell channels**: W2 (prologue rebucket commit — cross-cited into AP-146 for the local player) and W3 (already AP-135). The row's surviving claim is therefore precise: "a wire Position never makes a record resident *inside the merge or ahead of classification*; the post-routing controller still adopts the wire cell for non-placement outcomes as the bounded-residency bridge, filed at AP-135/W2." - **AP-130 amendment (bookkeeping, not retirement):** its Site column names only the executor's `hasAnimations` local; the merge now consumes the same static proxy. One sentence added. The proxy itself MUST NOT be "improved" to a live animation-queue read in this slice (trap T7). - **AP-146 / #320 amendment (bookkeeping):** #320's edge list cites "an accepted inbound Position/ForcePosition (`RuntimeEntityDirectory.RefreshSnapshot` → `RuntimeEntityRecord.cs:234`)" as a local-player cell writer. After D2 that writer is the generic-tail rebucket's `CommitRebucket` (and, for ForcePosition, the placement receipt). Amend both in the same commit or the next reader files a phantom regression. - **AP-135 stays, its writes stay** (must-remain-true item 3). - **AP-1, AD-1, AP-141–AP-145, AD-61, AD-62: pinned untouchable.** ## 4. What must REMAIN true (process rule — the contract causes the defect) For every path this slice touches, including every refusal and rejection: 1. **Classification reads pre-merge inputs only.** 4b-3's D1 binds in full: `PreMergeCommittedCellId` is threaded exactly as today (`RuntimeEntityObjectLifetime.cs:1904-1907`), measured by the SAME `TryApplyPosition` call, never re-read after the merge. D2 adds the structural property (the record equals it in the window) but the threaded value stays the classifier's input of record. 2. **The D4 constraint-arm partition (4b-3) does not move.** One post-operation `ConstrainTo` site, its partition table unchanged. C5b touches no arming code. 3. **AP-135's bookkeeping writes stay** — the server-cell adopt (`RemoteMotion.CellId`) and `LastServerPos`/`LastServerPosTime` samples in `LiveEntityNetworkUpdateController`, on both airborne no-op neighbourhoods. 4. **Dual-parent-class test discipline.** Every new test touching parent/ child cell propagation runs a player (`0x5…`) parent AND a creature (`0x8…`) parent (#319's lesson; the handoff's corrected gate-2 criterion). 5. **The executor path is untouched** — its classified flags, its `refreshPosition: false`, its replay semantics (`ApplyAcceptedPositionExecutionRejectedSnapshot`). Do not unify the two merge callsites into one helper "while here" (trap T8). 6. **The timestamp gate is untouched**; Gate A's teleport-concurrent shape stays upstream (`PhysicsTimestampGate.cs:190-217`; a force with a teleport advance falls through to `Apply`, and `ValidAcceptedAuthority`'s `PreviousTeleport == AcceptedTeleport` rule handles the classified side). 7. **The OnPosition routing/arms are untouched** — generic render-pose gate, W2, the arm dispatch, W3, the unified tail, AP-87's catch-up, AP-139's landing clear, AP-140's `InContact` gates. 8. **The local player's cell-freshness path** (echo → generic tail → W2 → `CommitRebucket`) is unchanged. AP-146 stays accurate after its §D4 amendment. 9. **The initial-residence branch of `TryApplyPosition`** (`:1800-1860`) is untouched — it is the executor's enqueue path, already classified. 10. **`Physics.ObserveLocalWorldFrame`** keeps reading the wire landblock directly (`:1887-1889`); it is not a `FullCellId` consumer. 11. **No new skip** (process rule (d)) and no test weakened to make the withhold pass (trap T2 — the withhold assertion lives at the merge boundary, never at OnPosition level, where W2 legitimately re-stamps). ## 5. THE BLAST RADIUS — itemised by consumer class Premise: S2's withhold is the only residency write C5b removes; W2/W3 stay (D3). So the consumers that ever read a value that is intentionally NOT the wire cell are (a) everything in the classification window (merge → `:2329`), and (b) steady state after packets that return before W2 (force path, missile arm). Everyone else reads the same value as today. The #319 lesson is applied, not assumed: a stale cell is worse than a zero cell **for derived/presentation copies** — but `FullCellId`'s whole semantic post-D2 is "the last committed cell", which is retail's `this->cell`; that is not staleness, it is the definition. Zero would be the lie here. ### Class A — placement owners (read at/after placement). Verdict: CORRECT, no gate. `RuntimeSetPositionState` (pre-flight reads `:1172-1255`, commit compares `:2305/:2760/:5020`, parks `:3004/:3031/:3677-3697`, `:3795/:3954`, `:5225/:5242`), `RuntimeRemotePlacementDriveController` (`:1053-1068` — verified: reads `record.FullCellId` AFTER the placement commit for the shadow publish; the commit just wrote it), `RuntimeAcceptedPositionDriveController` (receipt `resolvedCell:` reads `:914-1535`). Retail's placement reads `this->cell` — the committed cell, never the wire cell. These get a MORE honest input post-D2. ### Class B — per-tick simulation. Verdict: CORRECT, no gate. `RuntimeOrdinaryPhysicsUpdater:127` (transits FROM `record.FullCellId` — retail's per-frame cell transit reads `this->cell`), `RuntimePhysicsState` (`:958`, commit `:2145-2159`), `RuntimeProjectilePhysicsUpdater` (`:53-293`), the free-fall sweep's `rm.CellId != 0` gate (AP-135). The simulation is the retail-sanctioned residency writer between packets. ### Class C — classification inputs. Verdict: CORRECT — strengthened. `RuntimeAcceptedPositionRouteRequests` (`:57` route-1 overload — executor scope, unchanged; `:641`-family drive-controller builds for the local player — reads the local player's committed cell, whose freshness path is W2, unchanged). The remote PositionEvent build is the threaded `PreMergeCommittedCellId` — post-D2 the record in the window equals it structurally (§3 D2). 4b-3's D1 moves from convention-defended to structure-defended. ### Class D — presentation/projection, event-time readers. Verdict: HARMLESS, with three named window sites. Event-time readers (landblock-loaded, visibility-changed, per-tick sync) run outside the per-packet window, and W2 keeps steady-state values identical to today for every generic-tail packet: `RuntimePlacementPresentationSink:286` (post-commit receipts), `LiveRenderProjectionJournal:270-271` (falls back when 0), `EntityEffectController:481`, `LiveStaticAnimationResidency:25`, `StaticLiveRootCommitter:76`, `ArchRenderScene` (`Residency.FullCellId` — statics), `HeadlessLocalPlayerFrameHost:87`, `HeadlessRuntimePlacementProjectionSink:103` (token compare), `RemotePhysicsUpdater:239/:294` / `LiveEntityOrdinaryPhysicsUpdater:107` (simulation-snapshot → `ParentCellId` sync). Named window sites (inside merge→W2, or pre-W2 returns) — verdict per site: | site | reads | verdict | |---|---|---| | `LiveEntityHydrationController:592` (`?? candidate.FullCellId` fallback) | landblock-loaded event | HARMLESS — event-time, post-W2 population; the fallback is third in a `??` chain behind projection/snapshot position | | `LiveEntityHydrationController:1077` (`FullCellId != 0` gate) | projection recovery inside the window | VERIFY at implementation: after D2 it reads the committed cell (correct — recovery should re-place from the last commit, not from an unplaced wire claim). Pre-D2 it could read the just-stamped wire cell; that was the AP-1-shaped read this slice exists to remove | | `LiveEntityRuntime:1219` / `:1564` (`token.ExactCellId` compares) | hydration/migration tokens | VERIFY: token cells are create/placement-derived, not wire-derived; the comparison against the committed cell is the intended predicate | | `LiveEntityPresentationController:220` (`RestoreShadow`'s `FullCellId == 0` bail) | visibility-edge restore | HARMLESS/CORRECT — a cell-less body correctly restores no shadow row; the force-path population keeps its committed cell | ### Class E — residency/liveness predicates (the "45+ sites"). Verdict: HARMLESS, two CORRECT-AND-LOAD-BEARING. - `GetRootObjectClockDisposition` (`LiveEntityRuntime:2602-2617`), `HasSpatialRuntimeProjection` (`:3340-3345`), the ordinary-root gates (`:953-956`, `:3452-3455`): tick/edge-time, read the committed cell — retail's `this->cell != 0` predicate shape. CORRECT; unchanged in value for generic-tail packets (W2), changed only for refused-force (retail's shape). - `IsAffectedCollisionResident` (`RuntimeSetPositionState:3949`) and its `!HasCommittedParent` companion gates, the collision-retirement sweeps (`:3771/:3796/:3990/:4052`): event-time (landblock retirement), read the committed cell. CORRECT — a retirement sweep must act on where the body IS, not where an unplaced wire packet claimed. - The initial-create residence `FullCellId != 0` refusal (`RuntimeInitialCreateResidenceState:583`) and executor baselines (`:877-1182`, executor `:1836/:2484`): pending-residence records never take the steady-state merge branch. UNAFFECTED. - 4b-3's cell-less classification input: threaded pre-merge value (Class C). - `RuntimeEntityDirectory:492` child propagation and `RuntimeEntityObjectLifetime:1546-1555` parent-attach propagation: write-side (D2's propagation-source move, pinned in §3 D2 item 2). - `ProjectileController:279/:568/:930` and the missile arm: the ONLY population whose residency becomes exclusively placement-receipt-driven post-D2 (the arm returns before W2, `:2256`). Test-only today (AP-141: ACE never sends a missile UpdatePosition). CORRECT — retail's projectile cell comes from `SetPosition`, full stop. ### The headline behavioural delta, stated once Post-C5b, a **refused or contended local ForcePosition** (AD-62's shapes (iv)-(vi)) leaves `FullCellId` at the last committed cell where today the merge stamps the refused packet's wire cell. Retail cannot refuse (AD-62) and its body keeps the last placed cell — the new shape is the retail-reachable one. Every other steady-state observable is either unchanged (W2/W3) or retail-correcting (the classification window, the placement-authoritative force/portal/missile arms). ## 6. Proof obligations (must prove, not assume) 1. **The truth table is the classifier's.** §8 test 4 drives both the merge and `RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition` (via `RuntimeAcceptedPositionRouteRequests.Build`) over the packet-shape matrix and asserts the merge-installed `AnimationFrame`/`PlacementId`/`ParentGuid`/`ParentLocation`/`Physics.Parent` equal what the route's flags would install. Not a restated table in the test body — the production classifier is the oracle (process rule (e): no test that re-encodes the constant under test). 2. **The withhold is at the merge boundary.** §8 test 5 asserts at `RuntimeEntityObjectLifetime.TryApplyPosition` level: an accepted Position whose wire cell differs from the committed cell leaves `canonical.FullCellId == beforeCell` immediately after the call. It MUST NOT be asserted at `OnPosition` level — W2 legitimately commits the wire cell there, and an OnPosition-level assertion would fail and tempt a weakening (trap T2). 3. **Publish-delta conservation.** One `Rebucketed` delta per cell-changing accepted Position on the steady-state path (§3 D2 item 1), and one child propagation per parent cell change (item 2), both asserted at the observable, both parent classes (must-remain-true item 4). 4. **The refused-force shape.** A ForcePosition whose drive execution refuses/contends writes NO residency change anywhere (§8 test 6) — merge, W2 (unreached), receipt (none). This is the assertion that makes AD-60's retirement mean something. 5. Sabotage-verify every new discriminating test in BOTH directions (process rule (e)) — the canonical half and the presentation/snapshot half separately, per route 7's finding. ## 7. Retirement evidence — AP-131 and AD-60's legacy half (same commit as the change) **AP-131** retires because: 1. The literals at `InboundPhysicsStateController.cs:662-663` no longer exist; the merged flags are computed by the §2-S1 truth table. A grep in the commit message shows no production caller passes unconditional `true/true` (the executor's `:1991-1992` threading is the classified path, already retail-exact). 2. §8 tests 1-4 pin the new behaviour against the classifier-as-oracle, sabotage-verified: (a) `Apply` + animated entity → wire frame NOT installed (sabotage `installPlacementFrame: true` fails it); (b) `Apply` + non-animated → wire frame installed (sabotage `false` fails it); (c) `ForcePosition` + parented entity → parent fields retained (sabotage `clearParent: true` fails it); (d) the matrix consistency test. 3. The row's risk column ("animated entity installs a placement frame retail would skip; ForcePosition unparents where Gate A never reaches `unset_parent`") describes code that no longer exists — the two production behaviours are now the classifier's own rows (`RuntimeAuthoritativePositionRouteClassifier.cs:311-333` and `:336-465`). **AD-60's legacy half** retires because: 1. `RuntimeEntityObjectLifetime.cs:1926` reads `refreshPosition: false`; `RefreshDerivedState`'s `SetFullCell(position.LandblockId, …)` is unreachable from the steady-state merge. 2. §8 tests 5-6 pin the withhold at the merge boundary and the refused-force shape. 3. The row rewrite (§3 D4) names W2/W3 as the surviving, separately-filed wire-cell channels, so the retirement cannot be misread as "wire acceptance never changes residency anywhere" — it means "never inside the merge, never ahead of classification, never for a packet whose placement was declined", with the post-routing adoption for non-placement outcomes filed where it belongs (AP-135 / the W2 citation). ## 8. Test plan Focused Runtime tests (`tests/AcDream.Runtime.Tests`), each asserting the layer that broke historically (merged snapshot fields, canonical cell, publish deltas — never source-text pins): 1. `Apply`, animated remote (`MotionTableId != 0`): merged snapshot keeps `old.Physics.AnimationFrame` / `old.PlacementId`; pose and timestamps still merge. Sabotage: force `installPlacementFrame: true`. 2. `Apply`, non-animated remote: merged snapshot installs `update.PlacementId ?? 0u`. Sabotage: force `installPlacementFrame: false`. 3. `ForcePosition`, parented local player: merged snapshot retains `ParentGuid` / `ParentLocation` / `Physics.Parent`; heading preservation (`:797-807`) unchanged. Sabotage: force `clearParent: true`. 4. Matrix consistency (proof obligation 1): {Apply, ForcePosition} × {animated, not} × {parented, not} × {player `0x5…`, creature `0x8…`} — merge outcome == classifier route flags' outcome. 5. Withhold at the merge boundary (proof obligation 2): wire cell ≠ committed cell → `canonical.FullCellId` unchanged after `TryApplyPosition`; `Snapshot.Position` DID refresh (the pose half of the merge is not the withheld half); a subsequent canonical placement commit still changes `FullCellId`. 6. Refused-force shape (proof obligation 4): drive a ForcePosition whose execution refuses (destination outside the service window) through the drive controller; assert no residency write from merge, receipt, or rebound — `FullCellId` is the pre-packet value throughout. 7. Publish-delta conservation (proof obligation 3): one `Rebucketed` per cell-changing accepted Position; one child propagation per parent cell change; player parent AND creature parent. 8. The existing `InboundPhysicsStateController` unit tests that assert the UNCONDITIONAL flags are **rewritten, never delete-only** (the scoping's §3 row, carried): each becomes its classified-flag counterpart. ## 9. Gates - Focused tests above. - Complete Release suite: `$env:ACDREAM_PAK_PATH` set, `dotnet test AcDream.slnx -c Release -m:1`. **Re-measure the baseline; do not inherit.** Recorded figures: 11,112 passed / 4 skipped / 0 failed at `392c1e22` (C5a contract) and 11,106 at `6921a027` post-C5a (the tasking figure). C5b's net count moves only by test rewrites/additions; the commit message reconciles the net explicitly. Three known flakes, filed separately, never conflated, never chased: **#302** (`PortalProjectionTests.ClipToRegion_FrameOwnedStore_…`, App.Tests), **#308** (`NakEmissionTests.LossSoak_…`, Core.Net.Tests, full-suite load), **#321** (`DatSoundCacheTests` concurrent-decode-dedup, Core.Tests, full-suite load — filed 2026-08-05, deliberately separate from the other two; do NOT fold it in). If any appears, re-run and say which. - **Connected gate: YES — argued, not assumed.** The change is on the hottest wire path (every steady-state Position) and the campaign's standing discipline is a connected gate per live-path behaviour change. But the gate's pass evidence must be positive, because C5b's *improvement* is absence-of-signal (a placement frame NOT installed; an animation NOT popping — the #319/rule-(g) unfalsifiable-criterion trap). Recipe (two-client, Release, `ACDREAM_RETAIL_UI=1`, `ACDREAM_PROBE_REMOTE_TELEPORT=1`, pre-C5c so the probe still exists): 1. **Pickup-then-drop** a ground item, several times — the positive probe assertion: `[remote-teleport]` lines appear with `hookRan=True placement=Committed`, proving the pre-merge-fed cell-less/teleport classification still fires and commits after the merge stopped being a residency writer. (Gate 4's closure at `af828a8a` established this recipe reaches the arm; the probe label may read `teleport-ts` for the short-circuit reason the handoff records — that is expected, not a gap.) 2. **A walking/running creature across ≥1 landblock boundary**, observed: visible, smooth, correct cell, no freeze, no origin-snap — the W2 regression watch (the one writer C5b leans on hardest). 3. **`@teleto`/`@teleloc` a creature** (never a player character — rule (g)): teleport arm unchanged, leash re-armed, no rubber-band, no run-in-place. 4. **Local `@teleto` + one portal recall**: the force/portal paths are now placement-authoritative for residency — arrival pose correct, leash armed, movement immediate. 5. **An animated creature idling/fighting at UpdatePosition cadence**: watch for NEW animation popping (regression) — recorded as an observation only, explicitly NOT a pass criterion (the absence of the OLD pop is the improvement and is test-gated, §8 test 1). Graceful close per the standing rule. **No new probe flag for C5b**: the discriminating signals are either absence-of-signal (ungateable) or already carried by `[remote-teleport]`; adding a seventh temporary probe for one session and stripping it in C5c is churn without evidence value. ## 10. Size and split call Calibrated against the campaign (route 3 ~418, #319 76, 4b-3 ~250-700): | piece | production lines | test lines | |---|---|---| | D1 truth table + comment rewrites | ~25-40 | ~200-350 (matrix + rewrites) | | D2 withhold + comment + publish-edge pins | ~15-25 | ~100-250 | | Register rewrites + #320/AP-130 amendments | 0 (docs) | 0 | **Total ~40-65 production, ~300-600 test** — well under the ~500-line split threshold, and dramatically under the scoping's ~150-400 estimate, because the scoping assumed route plumbing (it did not see that the flags are disposition-determined; §12 item 2). **One landing.** If the test rewrite balloons past ~600 lines, the split line falls between D1's flag tests and D2's withhold tests — but the PRODUCTION change lands atomically: D1+D2 in one commit is one coherent behaviour (classify-then-merge); a half-flipped intermediate (classified flags with the wire stamp, or vice versa) is exactly the mixed-residency state this campaign keeps paying for. Single implementer, dual reviews (retail-conformance + architecture) per the campaign's discipline, both re-run on the final diff. ## 11. Traps (routes 6/7 style) - **T1 — reading `hasAnimations` post-merge.** Compute from `old` (the pre-merge snapshot) inside `TryApplyPosition`. For Position packets the merge cannot change `MotionTableId`, so the failure mode is latent, not live — pin the discipline anyway, because the NEXT field added to this computation may not share the property. - **T2 — the withhold assertion at the wrong boundary.** W2 re-stamps the wire cell at `:2329` for every generic-tail packet, BY DESIGN. A test asserting "FullCellId unchanged after OnPosition" fails and invites a weakening or a W2 gate. The assertion lives at `RuntimeEntityObjectLifetime.TryApplyPosition` (§8 test 5). This is the slice's "weaken a test to make the withhold pass" trap. - **T3 — the moved publish edges.** `Rebucketed` moves from the merge's ternary to `CommitRebucket`; child propagation moves with it. Pin the observable (counts), never the source site; do not delete the coverage when the old assertion site goes quiet. - **T4 — the ForcePosition placement-frame half is inert** (`appliedPlacement` keeps `old.PlacementId` under either flag). The force-half discriminating assertion is PARENT RETENTION. Sabotaging the placement frame on a ForcePosition proves nothing. - **T5 — do not gate W2 "for symmetry".** The local player's canonical cell freshness runs THROUGH W2 (echo → generic tail → `CommitRebucket`; AP-146/#320). Gating it freezes the player's cell between teleports and #319's child equality inherits the freeze. W2 is also the far arm's destination-streaming nudge. - **T6 — the missile arm returns before W2** (`:2256`). Post-D2 its residency is placement-receipt-only; fixtures driving refused/deferred missile placements must not assert wire-cell residency. - **T7 — do not "improve" `hasAnimations`** to a live animation-queue read. AP-130 files the static proxy deliberately; the merge consumes the same proxy and the row is amended, not escalated. - **T8 — no while-here unification** of the two merge callsites, the executor's replay merge, or the timestamp gate. - **T9 — flake discipline.** #302/#308/#321 are three separate filed flakes; a full-suite red in any of them is re-run and named, never folded into "the flake class" and never masked (#321's filing note: no retry, no Skip, no delay). ## 12. Scoping claims found wrong or stale at HEAD `02578441` 1. **The scoping's core premise for the withhold is false.** §2: withholding S2 "moves residency changes onto placement/simulation commits only — the retail rule". At HEAD the steady-state path has TWO MORE wire-cell writers downstream of the merge (W2 `LiveEntityNetworkUpdateController.cs:2329` → `CommitRebucket`; W3 `TryAdoptWireCellAfterRouting:1613`), and C5b keeps both (§3 D3). The retirement must be scoped to the merge boundary and the survivors named in the register rewrite, or AD-60's retirement is a shell game. This is the contract's load-bearing correction. 2. **The size estimate over-counts.** Scoping §7: "~150-400 changed on the hottest inbound path". The flags are disposition+hasAnimations-determined (§2-S1 truth table; retail runs both gates BEFORE the `MoveOrTeleport` branch decision) — no route construction, no `playerDistance` threading, no signature changes. Re-estimate: ~40-65 production lines (§10). 3. **Stale line citations (all re-verified):** `TryApplyPosition` at `RuntimeEntityObjectLifetime.cs:1789` (scoping `:1781`); the threaded overload's signature at `InboundPhysicsStateController.cs:673` (scoping `:681` — that is its flags parameter line); S2's call at `RuntimeEntityObjectLifetime.cs:1923-1926` (C5a's `:1926` correct; the register's `:1338` still stale); `LiveEntityInboundAuthorityGate` lives under `src/AcDream.App/Physics/` (scoping gave no directory; `:161` exact). Everything else in scoping §1b/§2 holds. 4. **#320's edge list goes stale the day D2 lands** — its "accepted inbound Position (`RefreshSnapshot` → `RuntimeEntityRecord.cs:234`)" writer becomes the generic-tail rebucket's `CommitRebucket`. Amendment mandated in the implementation commit (§3 D4), or the next reader files a phantom regression. 5. **The scoping's §5 cell-less falsification session is overtaken:** gate 4 CLOSED at `af828a8a` (the `cause=cellless` label was a probe-artifact — the pickup-then-drop recipe exercised the arm all along). C5b does not inherit that investigation; its connected gate reuses the same recipe as positive evidence (§9). Route-7 thickening and #316's measurement are C5c-ledger items, not C5b design inputs — the implementation session checks the ledger for their status before starting, and does not block on them. ## 13. What C5b does NOT do - Does NOT wire the continuation executor into the steady-state path (#275's alternative branch — the issue's "or delete it with the route"; the steady-state merge stays a live, now-classified, production caller). - Does NOT gate, move, or delete W2/W3 (§3 D3). - Does NOT touch the OnPosition routing, arms, constraint partition, generic render-pose gate, or interpolation machinery. - Does NOT touch the timestamp gate, the executor, the initial-residence branch, or the projectile arm's routing. - Does NOT strip any probe (C5c), fix #316/#317/#320, or re-open AP-145's seam. - Does NOT add a live animation-queue read (AP-130 stands). --- ## 14. Implementation outcome (appended at landing, 2026-08-05) Both design decisions landed exactly as pinned: `installPlacementFrame: !force && !hasAnimations, clearParent: !force` at the S1 call site, and `refreshPosition: false` at S2. No signature changed, no route was plumbed, `playerDistance` was never needed, W2/W3 were not touched. The production diff is 129 insertions / 38 deletions across two files, of which the BEHAVIOUR is **seven lines** — four computing `force`/`hasAnimations`, two flag arguments, one `refreshPosition` argument. Everything else in that delta is the §3-D1 mandated comment rewrites. Well inside §10's ~40-65 estimate; the estimate itself was counting comments. **§5's blast-radius survey missed three consumer sites.** All three are D2-caused, all three were found by the test suite rather than by reading, and all three turned out to be the intended semantics rather than regressions — but the survey did not enumerate them, so they are recorded here: 1. **`DatLiveEntityProjectionMaterializer`'s self-projection branch** (`src/AcDream.App/Rendering/DatLiveEntityProjectionMaterializer.cs`, the `residence is AwaitRuntimePlacement && expectedCanonical.FullCellId != 0u && !HasActiveInitialCreateResidence` gate). This is a Class-D window site: it runs inside `OnPosition`'s prologue recovery (`:1973` / `:2089`/`:2102`), ahead of W2. Pre-D2 it read the just-merged wire cell and installed the spatial bucket there; post-D2 it reads 0 for a withdrawn/inventory-only record and correctly declines to project from an unplaced wire claim. Production still installs the bucket in the SAME `OnPosition` call, at W2 (`:2329`) — verified by reading every `return` between the recovery call and W2: none is conditioned on `IsSpatiallyProjected` or `FullCellId`. Two `LiveEntityHydrationControllerTests` cases asserted the bucket at the recovery boundary and were extended to drive the production W2 step; this is the same shape as trap T2, one layer up. 2. **`ProjectileController.SyncPresentationFromResolvedBody`** (`entity.ParentCellId = record.FullCellId`). Trap T6 named the missile arm's residency but not this presentation writer. On a REFUSED missile placement the entity's pose still moves to the destination (acdream's `StoreAcceptedDestinationPose` fallback) while `ParentCellId` now stays at the committed source cell. The MAJOR-1 invariant the covering test exists to protect (`ParentCellId == record.FullCellId != body.CellPosition .ObjCellId`) is unchanged and is now asserted as that identity rather than as a wire-cell constant. 3. **The `Rebucketed` publish edge does NOT always move to `CommitRebucket`.** §3 D2 item 1 says the merge's ternary "becomes always-`Updated`". It does not, and the ternary is deliberately kept: the `Physics.SetPosition.Forget (canonical, restoreCancelledPark: true)` call a few statements earlier can roll a wakeable lost-cell park back, and `RestoreParkWithdrawal` restores canonical residency at the body's committed cell. That is a real cell edge produced inside `TryApplyPosition` by a placement owner and must still publish as `Rebucketed`. Collapsing the ternary would have silently downgraded it. **One §5 verdict was checked and stands:** `LiveEntityHydrationController :1077`'s `FullCellId != 0` gate is unreachable from this change — it sits inside the `canonicalSpawn.Position is null` branch, and a merged steady-state Position always carries one. **Not done, deliberately:** no automated OnPosition-level test drives the full pickup→drop→reproject sequence (no such fixture exists; the collapse matrix fixture does not cover pickup). The claim that production reprojects a dropped item at W2 rests on the code reading above plus the two hydration tests now driving recovery-then-W2 in production order. §9's connected gate recipe item 1 (pickup-then-drop, several times) is the positive evidence for it and has not been run.