The steady-state accepted-Position merge did two things retail never does,
on every single Position packet: it installed the wire placement frame and
unparented unconditionally, and it derived the record's FullCellId from
bare wire acceptance. Both are now correct, and they land together - a
half-flipped intermediate (classified flags with the wire stamp, or vice
versa) is exactly the mixed-residency state this campaign keeps paying for.
WHY the flags need no route. SmartBox::HandleReceivedPosition @0x00453FD0
decides both pre-placement writes BEFORE MoveOrTeleport is consulted: Gate A
@0x0045400C returns @0x0045409D ahead of unset_parent @0x00454129 and ahead
of the HasAnims SetPlacementFrame gate @0x00454137. Neither gate reads the
near/far/teleport classification. So the two flags are a pure function of
(disposition, hasAnimations) and are computable inside the merge, pre-merge,
with no signature change, no route construction and no playerDistance - the
scoping's ~150-400-line route-plumbing estimate over-counted because it did
not see this. That truth table IS
RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition's own
ApplyPlacementFrameBeforeRouting/UnparentBeforeRouting rows; the classifier
stays the oracle and the equality is pinned by test, not by a shared path,
so each computation remains separately sabotage-verifiable.
WHY the cell is withheld. HandleReceivedPosition reads the wire objcell_id
into a LOCAL @0x00453FE3 and hands it only to BlipPlayer / TeleportPlayer /
MoveOrTeleport / ConstrainTo; it never assigns the object's cell. The
object's cell moves inside the placement family (SetPositionInternal
@0x00515BD0 to set_cell, enter_world) or per-frame transit, and nowhere
else. The continuation executor has encoded that rule since the executor
slice; this caller now matches it verbatim.
WHAT DELIBERATELY SURVIVES. Two steady-state wire-cell writers stay,
downstream of the merge and outside the classification window: the
OnPosition prologue rebucket (W2, into CommitRebucket), which is also the
local player's own cell-freshness path, and the post-routing wire-cell adopt
for non-placing arms (W3, AP-135). Gating W2 "for symmetry" would freeze the
player's canonical cell between teleports and #319's child-cell equality
would inherit the freeze. AD-60's rewrite names both so the retirement
cannot be misread as "wire acceptance never changes residency anywhere".
REGISTER. AP-131 RETIRED - the unconditional literals no longer exist; the
caller was corrected, not deleted, so the row's own "deleted at the
production cutover" framing is overtaken. AD-60's legacy half RETIRED and
the row REWRITTEN rather than deleted, naming W2/W3 (route 4b-3's D8
precedent: a silent whole-row deletion would hide surviving channels).
AP-130 amended - the merge consumes the same static HasAnimations proxy,
deliberately not escalated to a live animation-queue read. AP-146 and #320
amended - their "accepted inbound Position (RefreshSnapshot into
RuntimeEntityRecord.cs:234)" local-player cell writer is now the generic
tail's CommitRebucket, and a ForcePosition (which returns before that tail)
is placement-receipt-authoritative. #275 closed.
HEADLINE BEHAVIOURAL DELTA, stated once: a refused or contended local
ForcePosition now leaves FullCellId at the last committed cell where the
merge used to stamp the refused packet's wire cell. Retail cannot refuse
(AD-62) and its body keeps its last placed cell, so the new shape is the
retail-reachable one.
THREE CONSUMER SITES THE CONTRACT'S BLAST-RADIUS SURVEY MISSED, all
D2-caused, all found by the suite rather than by reading, all intended
semantics rather than regressions (recorded in the contract's new section
14):
(1) DatLiveEntityProjectionMaterializer's self-projection branch reads
FullCellId inside OnPosition's prologue recovery, ahead of W2. It now
correctly declines to project from an unplaced wire claim; production
installs the bucket at W2 in the same call (verified: no return between
the recovery call and W2 is conditioned on IsSpatiallyProjected or
FullCellId). Two hydration tests asserted the bucket at the recovery
boundary and now drive the production W2 step - the same shape as trap
T2, one layer up.
(2) ProjectileController.SyncPresentationFromResolvedBody writes
ParentCellId = record.FullCellId. On a refused missile placement that is
now the committed source cell. The MAJOR-1 invariant is unchanged and is
now asserted as the identity it always meant rather than as a wire-cell
constant.
(3) The merge's Rebucketed ternary does NOT become always-Updated as the
contract predicted, and is deliberately kept: the
Forget(restoreCancelledPark: true) above it can roll a wakeable
lost-cell park back, and RestoreParkWithdrawal restores canonical
residency. That is a real cell edge produced inside this method by a
placement owner.
TEST-COUNT RECONCILIATION. Baseline measured at this HEAD by stashing the
change: Runtime.Tests 1176, App.Tests 4135 (4132 passed / 3 skipped),
solution 11,106 passed / 4 skipped - matching the recorded figure at
6921a027 exactly. Post-change: Runtime.Tests 1195, App.Tests 4135 unchanged,
solution 11,125 passed / 4 skipped / 0 failed. Net +19, entirely new Runtime
tests: 3 facts plus a 12-row matrix theory in
InboundPhysicsStateControllerTests, 1 fact plus a 2-row theory in the new
RuntimeSteadyStatePositionMergeTests, and 1 fact in
RuntimeAcceptedPositionDriveControllerTests. No test was deleted; five
existing tests were rewritten in place, never delete-only. No new skip; none
of #302/#308/#321 appeared.
SABOTAGE VERIFICATIONS (each new discriminating test, both directions;
production line broken, suite run, line restored):
installPlacementFrame (!force && !hasAnimations) to (!force)
5 fail: ApplyOnAnimatedEntity_NeverInstallsTheWirePlacementFrame plus
the 4 animated non-force matrix rows.
installPlacementFrame to false
6 fail: ApplyOnNonAnimatedEntity_InstallsTheWirePlacementFrame,
PositionPlacementAbsentAndPresentZeroBothApplyRetailZero plus the 4
non-animated non-force matrix rows.
clearParent (!force) to true
3 fail: ForcePositionOnParentedLocalPlayer_RetainsTheParentAttachment
plus the 2 force+parented matrix rows.
clearParent (!force) to false
4 fail: the 4 Apply+parented matrix rows.
refreshPosition false to acceptedPosition
4 fail: AcceptedPosition_WithholdsTheWireCellAtTheMergeBoundary,
ContendedForcePosition_WritesNoResidencyAnywhere,
ReentrantNewerPositionDuringPickupDiscardSuppressesStalePickupDelta,
MissileFarRefused_...ParentCellIdAgreesWithCommittedCell. Confirmed a
second time by the baseline measurement above, where the withhold test
was the sole red.
CommitRebucket publishes Updated instead of Rebucketed
2 fail: both parent classes of
CellChangingAcceptedPosition_ConservesOneRebucketAndOneChildPropagation.
RuntimeEntityDirectory.SetFullCell drops PropagateFullCellToChildren
2 fail: the same two rows.
T4 respected: the ForcePosition placement-frame half is inert
(appliedPlacement keeps old.PlacementId under either flag value), so the
force row's discriminating assertion is parent retention, never the frame.
NOT DONE, deliberately: the executor is still not wired into the
steady-state path (#275's alternative branch); W2/W3 are untouched; no probe
added or stripped; AP-130's proxy not escalated; no while-here unification
of the two merge callsites. No automated OnPosition-level test drives the
full pickup / drop / reproject sequence (no fixture covers pickup at that
layer); the contract's connected gate recipe item 1 is the positive evidence
for it and has NOT been run - this commit is not connected-gated.
Contract: docs/research/2026-08-05-c5b-contract.md (committed here, with its
section 14 implementation outcome appended).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
46 KiB
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:
- The wire cell is never written to the object.
arg3->objcell_idis read into a local @0x00453FE3 and flows only into the localvar_48Position handed toBlipPlayer/TeleportPlayer/MoveOrTeleport/ConstrainTo. Nowhere in this function is the object's cell assigned. The object'scellchanges 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"). - 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 theSetPlacementFramegate @0x00454137 — a ForcePosition never unparents and never installs a placement frame. acdream's exact analog exists upstream:PhysicsTimestampGate.TryAcceptPositionEvent:190-203produces theForcePositiondisposition only whenisLocalPlayer && IsNewer(FORCE_POSITION_TS) && teleport == _timestamps[Teleport]— retail's Gate A pair, verified at HEAD. A remote entity can never receive theForcePositiondisposition (isLocalPlayerguard at:190), so the flag truth table below needs no entity-kind term. - The two pre-placement writes are gated on exactly two facts.
unset_parent@0x00454129 runs for every accepted non-Gate-A Position (including packetsMoveOrTeleportwill return 0 for — retail unparents BEFORE learning the routing outcome).SetPlacementFrame@0x00454142 is gated onHasAnims(arg2) == 0alone. Neither gate reads the near/far/teleport classification — which is why acdream's merge flags are a function of (disposition, hasAnimations) and nothing else. MoveOrTeleport@0x00516330 reads the BODY's own state at entry: TELEPORT_TS @0x00516375 andthis_1->cell == 0@0x00516386 — the committed cell, read before any placement — then contactarg4@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'sFullCellIdin the classification window (§4, "strengthened invariant").store_position@0x00515CE2 isSetPositionInternal's no-transition branch (placement ran, no walkable transition →GotoLostCell@0x00515CF2), not aMoveOrTeleportpath. A retail placement that fails leaves the wire pose inm_positionand 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
ForcePositionplacement-frame half is inert: fordisposition is not Apply,appliedPlacementkeepsold.PlacementIdunder either flag value (:815-819). Gate A's force half discriminates ONLY through parent retention (:855-857). - The
Applyplacement-frame half is live exactly whenhasAnimations == true: today the wire frame (update.PlacementId ?? 0u) is stamped onto an animated entity's snapshot where retail'sHasAnimsgate skipsSetPlacementFrame. 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):
- The merge's publish-kind ternary (
:1948,beforeCell != canonical.FullCellId ? Rebucketed : Updated) becomes always-Updated— the merge can no longer change the cell. TheRebucketededge for a wire-cell-changing packet moves to the prologue rebucket'sCommitRebucket(RuntimeEntityObjectLifetime.cs:1956-1987), which TODAY early-outs publish-less (previous == fullCellIdat:1973— because the merge already stamped it) and after D2 publishes. Net observable: exactly oneRebucketeddelta 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. PropagateFullCellToChildrenfor a parent whose accepted Position changes its cell moves from the merge's directory wrapper (RuntimeEntityDirectory.cs:240-246) toCommitRebucket's publicSetFullCell(: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 (player0x5…and creature0x8…; 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:2329RebucketLiveEntity(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_worldclock 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 = wirefor every non-placing arm, writing through toFullCellIdviaCommitCanonicalCell. 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
hasAnimationslocal; 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'sCommitRebucket(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:
- Classification reads pre-merge inputs only. 4b-3's D1 binds in full:
PreMergeCommittedCellIdis threaded exactly as today (RuntimeEntityObjectLifetime.cs:1904-1907), measured by the SAMETryApplyPositioncall, 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. - The D4 constraint-arm partition (4b-3) does not move. One
post-operation
ConstrainTosite, its partition table unchanged. C5b touches no arming code. - AP-135's bookkeeping writes stay — the server-cell adopt
(
RemoteMotion.CellId) andLastServerPos/LastServerPosTimesamples inLiveEntityNetworkUpdateController, on both airborne no-op neighbourhoods. - 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). - 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). - 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 toApply, andValidAcceptedAuthority'sPreviousTeleport == AcceptedTeleportrule handles the classified side). - 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
InContactgates. - The local player's cell-freshness path (echo → generic tail → W2 →
CommitRebucket) is unchanged. AP-146 stays accurate after its §D4 amendment. - The initial-residence branch of
TryApplyPosition(:1800-1860) is untouched — it is the executor's enqueue path, already classified. Physics.ObserveLocalWorldFramekeeps reading the wire landblock directly (:1887-1889); it is not aFullCellIdconsumer.- 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'sthis->cell != 0predicate shape. CORRECT; unchanged in value for generic-tail packets (W2), changed only for refused-force (retail's shape).IsAffectedCollisionResident(RuntimeSetPositionState:3949) and its!HasCommittedParentcompanion 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 != 0refusal (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:492child propagation andRuntimeEntityObjectLifetime:1546-1555parent-attach propagation: write-side (D2's propagation-source move, pinned in §3 D2 item 2).ProjectileController:279/:568/:930and 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 fromSetPosition, 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)
- The truth table is the classifier's. §8 test 4 drives both the merge
and
RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition(viaRuntimeAcceptedPositionRouteRequests.Build) over the packet-shape matrix and asserts the merge-installedAnimationFrame/PlacementId/ParentGuid/ParentLocation/Physics.Parentequal 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). - The withhold is at the merge boundary. §8 test 5 asserts at
RuntimeEntityObjectLifetime.TryApplyPositionlevel: an accepted Position whose wire cell differs from the committed cell leavescanonical.FullCellId == beforeCellimmediately after the call. It MUST NOT be asserted atOnPositionlevel — W2 legitimately commits the wire cell there, and an OnPosition-level assertion would fail and tempt a weakening (trap T2). - Publish-delta conservation. One
Rebucketeddelta 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). - 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.
- 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:
- The literals at
InboundPhysicsStateController.cs:662-663no longer exist; the merged flags are computed by the §2-S1 truth table. A grep in the commit message shows no production caller passes unconditionaltrue/true(the executor's:1991-1992threading is the classified path, already retail-exact). - §8 tests 1-4 pin the new behaviour against the classifier-as-oracle,
sabotage-verified: (a)
Apply+ animated entity → wire frame NOT installed (sabotageinstallPlacementFrame: truefails it); (b)Apply+ non-animated → wire frame installed (sabotagefalsefails it); (c)ForcePosition+ parented entity → parent fields retained (sabotageclearParent: truefails it); (d) the matrix consistency test. - 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-333and:336-465).
AD-60's legacy half retires because:
RuntimeEntityObjectLifetime.cs:1926readsrefreshPosition: false;RefreshDerivedState'sSetFullCell(position.LandblockId, …)is unreachable from the steady-state merge.- §8 tests 5-6 pin the withhold at the merge boundary and the refused-force shape.
- 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):
Apply, animated remote (MotionTableId != 0): merged snapshot keepsold.Physics.AnimationFrame/old.PlacementId; pose and timestamps still merge. Sabotage: forceinstallPlacementFrame: true.Apply, non-animated remote: merged snapshot installsupdate.PlacementId ?? 0u. Sabotage: forceinstallPlacementFrame: false.ForcePosition, parented local player: merged snapshot retainsParentGuid/ParentLocation/Physics.Parent; heading preservation (:797-807) unchanged. Sabotage: forceclearParent: true.- Matrix consistency (proof obligation 1): {Apply, ForcePosition} ×
{animated, not} × {parented, not} × {player
0x5…, creature0x8…} — merge outcome == classifier route flags' outcome. - Withhold at the merge boundary (proof obligation 2): wire cell ≠
committed cell →
canonical.FullCellIdunchanged afterTryApplyPosition;Snapshot.PositionDID refresh (the pose half of the merge is not the withheld half); a subsequent canonical placement commit still changesFullCellId. - 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 —
FullCellIdis the pre-packet value throughout. - Publish-delta conservation (proof obligation 3): one
Rebucketedper cell-changing accepted Position; one child propagation per parent cell change; player parent AND creature parent. - The existing
InboundPhysicsStateControllerunit 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_PATHset,dotnet test AcDream.slnx -c Release -m:1. Re-measure the baseline; do not inherit. Recorded figures: 11,112 passed / 4 skipped / 0 failed at392c1e22(C5a contract) and 11,106 at6921a027post-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 (DatSoundCacheTestsconcurrent-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):- Pickup-then-drop a ground item, several times — the positive probe
assertion:
[remote-teleport]lines appear withhookRan=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 ataf828a8aestablished this recipe reaches the arm; the probe label may readteleport-tsfor the short-circuit reason the handoff records — that is expected, not a gap.) - 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).
@teleto/@teleloca creature (never a player character — rule (g)): teleport arm unchanged, leash re-armed, no rubber-band, no run-in-place.- Local
@teleto+ one portal recall: the force/portal paths are now placement-authoritative for residency — arrival pose correct, leash armed, movement immediate. - 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.
- Pickup-then-drop a ground item, several times — the positive probe
assertion:
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
hasAnimationspost-merge. Compute fromold(the pre-merge snapshot) insideTryApplyPosition. For Position packets the merge cannot changeMotionTableId, 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
:2329for 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 atRuntimeEntityObjectLifetime.TryApplyPosition(§8 test 5). This is the slice's "weaken a test to make the withhold pass" trap. - T3 — the moved publish edges.
Rebucketedmoves from the merge's ternary toCommitRebucket; 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
(
appliedPlacementkeepsold.PlacementIdunder 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"
hasAnimationsto 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
- 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; W3TryAdoptWireCellAfterRouting: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. - 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
MoveOrTeleportbranch decision) — no route construction, noplayerDistancethreading, no signature changes. Re-estimate: ~40-65 production lines (§10). - Stale line citations (all re-verified):
TryApplyPositionatRuntimeEntityObjectLifetime.cs:1789(scoping:1781); the threaded overload's signature atInboundPhysicsStateController.cs:673(scoping:681— that is its flags parameter line); S2's call atRuntimeEntityObjectLifetime.cs:1923-1926(C5a's:1926correct; the register's:1338still stale);LiveEntityInboundAuthorityGatelives undersrc/AcDream.App/Physics/(scoping gave no directory;:161exact). Everything else in scoping §1b/§2 holds. - #320's edge list goes stale the day D2 lands — its "accepted inbound
Position (
RefreshSnapshot→RuntimeEntityRecord.cs:234)" writer becomes the generic-tail rebucket'sCommitRebucket. Amendment mandated in the implementation commit (§3 D4), or the next reader files a phantom regression. - The scoping's §5 cell-less falsification session is overtaken: gate 4
CLOSED at
af828a8a(thecause=celllesslabel 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:
DatLiveEntityProjectionMaterializer's self-projection branch (src/AcDream.App/Rendering/DatLiveEntityProjectionMaterializer.cs, theresidence is AwaitRuntimePlacement && expectedCanonical.FullCellId != 0u && !HasActiveInitialCreateResidencegate). This is a Class-D window site: it runs insideOnPosition'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 SAMEOnPositioncall, at W2 (:2329) — verified by reading everyreturnbetween the recovery call and W2: none is conditioned onIsSpatiallyProjectedorFullCellId. TwoLiveEntityHydrationControllerTestscases 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.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'sStoreAcceptedDestinationPosefallback) whileParentCellIdnow 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.- The
Rebucketedpublish edge does NOT always move toCommitRebucket. §3 D2 item 1 says the merge's ternary "becomes always-Updated". It does not, and the ternary is deliberately kept: thePhysics.SetPosition.Forget (canonical, restoreCancelledPark: true)call a few statements earlier can roll a wakeable lost-cell park back, andRestoreParkWithdrawalrestores canonical residency at the body's committed cell. That is a real cell edge produced insideTryApplyPositionby a placement owner and must still publish asRebucketed. 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.