# Issue #319 contract — the player-parented child's canonical cell (2026-08-05) **Scope:** fix the hard-coded `ParentInstanceSequence: 0` that files every CreateObject-carried parent relation under the wrong key for player parents (`EquippedChildRenderController.OnSpawn`), restore route 7's D1/D2 propagation for player-parented children (local AND remote players), structurally gate the two call sites whose inertness today is an accident of the zero cell, and pin the child's source of truth for a client-authoritative parent. **This slice does NOT make the local player's canonical cell track ordinary movement** — that is Decision 1's call, argued in §2, filed as its own follow-up. Pinned at HEAD **`af828a8a`**, clean tree, branch `claude/acdream-physics-divergence-5aa784`. (The dispatch named `2687d893`; the branch advanced one docs commit — `af828a8a`, the gate-4 probe-label closure — before this contract was written. Nothing in that commit changes this contract's premises; it strengthens §6's probe rule.) **Line numbers are as-of `af828a8a` and WILL go stale; every citation also names the symbol — trust the symbol.** Predecessor documents, binding where they still apply: - [`2026-08-05-local-player-child-propagation.md`](2026-08-05-local-player-child-propagation.md) — **the settling investigation. Its §§1–6 evidence is BINDING**; this contract re-verified its load-bearing claims at HEAD and confirms them (one nuance on §5.1 item 3, resolved in §3.3 below). - [`2026-08-04-c4-route-7-contract.md`](2026-08-04-c4-route-7-contract.md) — the regressing route's contract. Its §3 "must REMAIN true" invariants all still bind and are re-asserted in §5; its D1/D2/D3/D4 designs are NOT re-opened. - [`2026-08-05-c4-closeout-handoff.md`](2026-08-05-c4-closeout-handoff.md) — carries the CORRECTED route-7 gate criterion (positive equality assertion, dual parent class). §7 of this contract instantiates it. - [`2026-08-04-retail-parent-cell-propagation.md`](2026-08-04-retail-parent-cell-propagation.md) — retail `set_parent`/`enter_cell` recursion, settled. Not re-derived. - `docs/ISSUES.md` #319 — the defect record. This contract does not restate its root-cause chain; it builds on it. --- ## 0. The defect, established — binding, do not re-derive `EquippedChildRenderController.cs:134` (`OnSpawn`) hardcodes `ParentInstanceSequence: 0` into `Relations.AcceptCreateObjectRelation`. Correct for creatures/statics (genuinely sequence 0); wrong for players, whose `ObjectInstance` is `Character.TotalLogins` (ACE `Player_Networking.cs:34-37`), parsed into `RuntimeEntityRecord.Incarnation` (`CreateObject.cs:749` → `WorldSession.cs:233` → `RuntimeEntityRecord.cs:43`). The relation files under `(playerGuid, 0)` while the record carries `TotalLogins`, so both route-7 write sites miss: - **D1 attach re-cell** — `RuntimeEntityObjectLifetime.CommitAcceptedParentCellless`'s `parent.Incarnation == parentInstanceSequence` gate (`:1537`) → false. - **D2 crossing propagation** — `RuntimeEntityDirectory.PropagateFullCellToChildren`'s `ChildrenAttachedToParent(guid, current.Incarnation)` (`:478-480`) → `Array.Empty()`, forever. `TryCommitParent` never validates the sequence; the attach succeeds silently. Scope: the local player and every remote player, for every CreateObject-carried equip (i.e. every login and every first-observe). User-visible consequence NIL — verified in the investigation §5, not assumed. `ParentEvent`-carried relations (mid-session equip) are unaffected: `ParentAttachmentState.Enqueue`/`Resolve` carry and validate the wire's real sequence (`ParentAttachmentState.cs:396-411`, `:440-454`). --- ## 1. Retail ground truth — does retail distinguish a player parent? **No. The player/non-player split is entirely an acdream artifact of the (guid, incarnation) relation key meeting a wire message that carries no parent instance sequence.** Verified for this contract, not inherited: | claim | anchor | status | |---|---|---| | The CreateObject attach path looks the parent up **by GUID alone** in the live object table and calls `set_parent` — no instance-sequence read anywhere in the attach | `ACCObjectMaint::CreateObject` @0x00558870: `PhysicsDesc::get_parent_id` @0x00558a18, `CObjectMaint::GetObjectA(this, parent_id)` @0x00558a2d, `CPhysicsObj::set_parent(result, arg2, location)` @0x00558a3e | ✓ read at the pseudo-C | | The reverse direction (parent's CreateObject naming its children) is the same shape: per-child hash lookup by GUID, `GetNullObject` **placeholder** when the child is not yet constructed, then `set_parent` — again no instance gate | `CObjectMaint::SetChildren` @0x00509370: hash walk @0x005093b2-0x005093ca, `GetNullObject` @0x005093e6, `set_parent` @0x005093f8 | ✓ read at the pseudo-C | | `set_parent` / `enter_cell` / `leave_cell` / `change_cell` contain no player test of any kind — they operate on `CPhysicsObj*` uniformly | `set_parent` @0x00515A90 (order enumerated in route-7 contract §2 row 3); `enter_cell` @0x00510ed0; `leave_cell` @0x00510f50; `change_cell` @0x00513390 | ✓ (route-7 contract §2, re-affirmed; no player branch exists in any of the four bodies) | | Retail's `ObjectInstance` sequence lives in the physics-descriptor timestamp block and gates **message staleness**, never the parent relation — the relation is by live object pointer | timestamp block slot 8; AP-132's retail half ("retail's queue-by-GUID replay is pointer-only", @0x004535D0 ~92310-92326, `QueueBlobForObject` @0x005092D0) | ✓ (AP-132, register) | | Retail's player cell is NEVER stale: `SetPositionInternal` writes the player's own cell on every physics tick, so "parent cell propagation from a stale parent cell" is unrepresentable in retail | `CPhysicsObj::SetPositionInternal` @0x00515330 (propagation research, binding) | ✓ | | **CORRECTION (retail-conformance review, 2026-08-05): this row understated its own anchor.** `SetPositionInternal` @0x00515330 does not merely write the mover's own cell — in the same-cell branch it writes `this->m_position.objcell_id = objcell_id` @0x00515385, THEN walks `this->children` and writes each child's cell id directly (`*(uint32_t*)((char*)eax_2 + 0x4c) = objcell_id_1` @0x005153BD, `CPartArray::SetCellID` @0x005153CC, looping @0x005153AE–@0x005153D8); the cross-cell branch delegates to `change_cell` @0x00515372. Retail's D2 equivalent (per-tick child cell propagation) lives INSIDE THE SAME FUNCTION as the player's own per-tick cell write — a strengthening of the equality invariant and of AP-146, not a contradiction. (AP-142 clause (b) already cited this range; only this table's summary understated it.) | same, ranges above | ✓ | Consequences, pinned: 1. **The retail-faithful semantic for a CreateObject-carried relation is "attach to the current holder of the parent GUID"** — late-bound, no wire incarnation to honor, because the wire supplies none. acdream's mapping: the relation adopts the parent's **live incarnation at resolve/commit time**. This is NOT a weakening of AP-132's incarnation posture: AP-132 gates relations whose wire event **names** a specific parent incarnation (`ParentEvent.ParentInstanceSequence`); a CreateObject relation names none, so adopting the live value honors exactly what the server sent — a GUID. AP-132's row gains a clarifying sentence in the fix commit (§4 F4). 2. **The local player's coarse canonical cell is an acdream divergence with no register row.** Retail writes the player's cell per tick; acdream's canonical `FullCellId` for the local player is written only at login activation (`RuntimeSetPositionState.cs:2741-2745`), accepted inbound Position/ForcePosition (`RuntimeEntityDirectory.RefreshSnapshot` → `RuntimeEntityRecord.cs:234`), and teleport/portal commit (`RuntimeSetPositionState.cs:5001-5007`; `LocalPlayerTeleportController.cs:255`). Ordinary WASD passes a landblock id (`LocalPlayerProjectionController.Project`, `:85-98`, low 16 bits forced to `0xFFFF` in BOTH branches) that `LiveEntityRuntime.RebucketLiveEntity` explicitly preserves the old cell for (`:935-938`). Register rule 1 applies: **this divergence gets its row in the fix commit regardless of Decision 1's direction** (§4 F4). --- ## 2. DECISION 1 — the child follows the parent's CANONICAL cell; the key fix alone is correct and complete for #319; the player-cell-tracking question is real, pre-existing, and files separately **The call is (c)** — neither (a) nor (b) as framed. (b)'s framing ("staleness is harmless everywhere") is false without the §3 gates; (a)'s framing ("stale is strictly worse than zero, so #319 must make the player's cell track") rests on an argument that dissolves under the consumer enumeration below. The pinned design: > **The committed child's canonical `FullCellId` EQUALS its committed > parent's canonical `FullCellId` at every stable observation point — no > more, no less.** That is route 7's §3.1 headline invariant verbatim, and > it is an EQUALITY invariant, not a freshness invariant. The parent's own > canonical-cell freshness is a property of the PARENT's record, owned by > the parent's own write paths — pre-existing, unchanged by this fix, and > documented as its own divergence (§1 item 2). #319's fix makes the child > inherit the parent's value exactly; it does not, and must not, invent a > different cell authority for the child than the parent itself has. ### 2.1 Why (a)'s "stale defeats 45+ liveness sites" argument fails for the child The 45+ `FullCellId != 0` predicates (route-7 contract §0 item 6) read a **spatial root's** cell. A committed child is structurally excluded from every one of them by a NON-cell clause, verified at HEAD: - `LiveEntityRuntime.GetRootObjectClockDisposition` (`:2602-2612`) and `HasSpatialRuntimeProjection` (`:3340-3345`): both require `ProjectionKind is LiveEntityProjectionKind.World`; a committed child is `Attached` — excluded on the first clause regardless of cell value. - The collision-retirement sweep `RuntimeSetPositionState.IsAffectedCollisionResident` (`:3930-3946`): triple-gated — spatial-roots-only iteration (`ParkCollisionResidents` `:3749-3756`), `_physics.IsSpatialRoot` (`:3942`), and an explicit `!ParentAttachments.HasCommittedParent(record.ServerGuid)` (`:3944-3945`). A committed child can never be parked by a retiring prefix, at any cell value. - Route 7's own P4 record (`RuntimeEntityDirectory.cs:451-465`): a committed child is never a spatial root, joins no workset, has no shadow row. What remains is exactly four consumers that can read a CHILD's cell, and each is resolved individually: 1. **`LiveRenderProjectionJournal.Project` (`:270-277`)** — prefers `record.FullCellId` over the `entity.ParentCellId` fallback when nonzero. Traced to its consumers at HEAD: the dynamics **draw visibility** routes read `record.Source.ParentCellId` (the fresh, TickChild-maintained presentation field) — `RenderScenePViewFrameProduct.BuildDynamicLastRoute` (`:1441-1481`, `ParentCell(in record)` for the indoor `SphereVisibleInCell` test) and `BuildOutsideDynamicRoutes` (`:1356-1361`, same source). `Residency.FullCellId` feeds only (i) the indoor per-cell candidate index `_cellDynamics` (`ArchRenderScene.AddToIndices` `:639`), consumed by the look-in candidate enumeration (`BuildLookInRoutes` → `LoadCell` `includeDynamics: true`, `:1319-1322`), and (ii) the `RenderSceneShadowRuntime` residency assertion (`:403`). **Decisive fact: the LOCAL PLAYER's own record already sits in this exact index under this exact staleness today** — the player's `FullCellId` is nonzero-stale during ordinary play, and the player renders correctly everywhere, because visibility is `Source.ParentCellId`-driven. Post-fix the child is filed **beside its parent, under the same value** — equality with the parent in the render residency index is the consistent state, not a new staleness class. (P-R1 in §6 pins this with the shadow-runtime assertion in mind.) 2. **The hydration filter** (`LiveEntityHydrationController.OnLandblockLoaded` `:551-557`) — wakes undesirably; gated structurally in §3.1. 3. **`RestoreShadow`** (`LiveEntityPresentationController.cs:216-236`) — wakes undesirably; gated structurally in §3.2. 4. **The unwield/drop Position classification** (route-7 contract §11; `RuntimeAuthoritativePositionRouteClassifier`) — wakes DESIRABLY: a committed child's pre-merge cell becomes "deterministically the parent's (nonzero whenever the parent is celled)", which is **route 7 §11's own stated intent and retail's own predicate population** (retail's `unset_parent` does no cell work, so a wielded child's unwield Position reaches `MoveOrTeleport` with the parent's nonzero cell). #319 had silently re-created the pre-route-7 cell-less population for player-parented children; the fix restores the §11 direction. Note the `af828a8a` finding: at a drop ACE also advances TELEPORT_TS and the classifier is a short-circuit OR (`RuntimeAuthoritativePositionRouteClassifier.cs:391`), so the observed label stays `teleport-ts` either way. One test pins the stale-cell flavor (§6 test 6). Additionally, `RuntimeTraceRecorder.OnEntity` (`GameRuntimeEvents.cs:246-253`) stops recording `0` for the player's weapon — a diagnostic improvement, not a risk. **Conclusion:** with the two §3 gates in place, no consumer of the CHILD's cell behaves worse with stale-but-equal than with zero, several behave better, and the route-7 invariant is restored exactly. The (a) framing's "wrong answer looks right" applies to the PARENT's record — which this fix does not touch and which is exactly as stale before and after. ### 2.2 Why the player-cell-tracking half does NOT ride in #319 Making `LocalPlayerProjectionController.Project` pass the exact `movement.CellId` (available in both branches) instead of the coarsened landblock would touch, at minimum: - the deliberate landblock-preserve contract at `LiveEntityRuntime.cs:935-938` (a generic rebucket rule, not player-specific — changing its input population changes it for the one caller that relies on it); - `Rebucketed` delta publication cadence: today the player NEVER publishes a `Rebucketed` entity delta during WASD (the preserve path early-outs at `CommitRebucket`'s `previous == fullCellId`, `RuntimeEntityObjectLifetime.cs:1965-1972`); exact-cell commits would publish per crossing (EnvCell crossings are the high-frequency case) — a consumer enumeration in route 7's P8 class; - the accepted-Position classification inputs for the LOCAL player (route 2 and the 4b-3 `PreMergeCommittedCellId` measurement at `TryApplyPosition:1801-1814`): a fresh committed cell changes the pre-merge population on live correction paths that AP-136/AP-138 spent four review rounds pinning; - the portal-space freeze interaction (`LocalPlayerProjectionController.Project:100-103` — "the teleport owner alone projects the destination while the local controller deliberately retains its frozen source cell"): a canonical exact-cell writer must not race the teleport owner; - the `isOrdinaryRoot` family (`LiveEntityRuntime.cs:915-918`, `:3213`, `:3323`) and the animation-scheduler local-player exclusion (`LiveEntityAnimationScheduler.cs:183-227`). That is a local-player movement-spine slice with its own contract, its own review, and its own gate — calibrated against the campaign, comfortably larger than route 7's 127 production lines once its verification surface is counted, for zero behavioral need identified today. **It files as a follow-up issue in the fix commit** (§4 F4), carrying one specific hazard this contract identified and could NOT close (§9 item 2): whether the local player is a `RuntimePhysicsState` spatial root, and if so whether a stale-cell landblock retirement (a player WASD-ing beyond the streaming radius from its last teleport, with no intervening teleport or inbound Position) can sweep the player into `ParkCollisionResidents`. The child is provably immune (the `HasCommittedParent` gate); the player is not obviously so, and the connected routes that pass today teleport between stops, which refreshes the cell and may be masking it. --- ## 3. DECISION 2 — the three inert-because-zero sites, one verdict each ### 3.1 Hydration filter — WAKES WRONGLY; add the structural gate in this slice `LiveEntityHydrationController.OnLandblockLoaded` computes `projectionCellId = projection?.ProjectionCellId ?? Snapshot.Position?.LandblockId ?? candidate.FullCellId` (`:551-553`) and admits candidates on `projectionCellId != 0` + landblock match + `SetupTableId` (`:554-557`). A committed child today falls through all three sources to `0` (its `ProjectionCellId` is unset, its `Snapshot.Position` is null per route-7 §0 item 10) and is skipped. With a nonzero canonical cell it becomes a candidate whenever its (= the parent's) canonical landblock loads — e.g. at every login, where the equipment's cell names the activation landblock — and takes the full legacy `RebucketLiveEntity` branch (`:594-596`), which writes `entity.ParentCellId` (`LiveEntityRuntime.cs:885-898`) AND reaches `CommitRebucket` — **a second canonical cell writer for a child, the exact two-writer defect route 7 exists to remove**, plus a one-frame presentation overwrite that TickChild then repairs. **Waking is not desirable, and the current inertness is an accident of the bug.** Pinned: the candidate loop excludes records with a committed parent — gate on `ParentAttachments.HasCommittedParent(candidate.ServerGuid)` (or the equivalent projection-kind test), the same structural exclusion the retirement sweep already uses (`RuntimeSetPositionState.cs:3944-3945`). Rationale is retail-anchored, not defensive: a retail child is never independently re-placed by cell load — `update_object`'s `parent != 0` early-out (@0x00515D40) means the parent's own propagation is the only mechanism, and acdream's analog (D2) needs no hydration assist. The gate is correct TODAY (it changes nothing for a zero-cell child) and required post-fix; it lands in the same commit as the key fix, before it in sequence. **CORRECTION (retail review round 2, D1, 2026-08-05): the "falls through to 0" and "correct TODAY (changes nothing for a zero-cell child)" claims above are true only for a PLAYER-class parent — false for a creature/static-class parent.** Pre-fix, `EquippedChildRenderController.OnSpawn`'s hardcoded `ParentInstanceSequence: 0` genuinely MATCHES a creature-class parent's real incarnation (creatures/statics are sequence 0), so D1's `parent.Incarnation == parentInstanceSequence` gate already passes for that class at HEAD, the relation commits, and the child already carries a nonzero canonical `FullCellId` pre-fix — meaning a creature-parented child is ALREADY a hydration candidate today and already takes the legacy `RebucketLiveEntity` branch this gate removes. §3.2's twin premise received this same correction (architecture review A4); this paragraph did not, until now. The gate's direction is still correct (route 7's P4/D4), and it is still required in this slice — but it is a live behaviour change for the creature class, not merely a post-fix necessity for the player class. See the §7 gate's Half B for the connected-gate watch step this requires. ### 3.2 `RestoreShadow` — WAKES WRONGLY; add the structural gate in this slice `LiveEntityPresentationController.RestoreShadow` (`:216-236`) no-ops today on `record.FullCellId == 0` (`:220`). Its other two guards do NOT protect a committed child: `IsSpatiallyProjected` is set `true` by the presentation-only rebucket the child takes every frame (`LiveEntityRuntime.cs:1043`, inside `RebucketLiveEntityPresentationOnly`), and `IsSpatiallyVisible` follows the child's ordinary visibility. On a Hidden→Visible edge (`RetailHiddenTransition.BecameVisible`, `:187-204` — reachable for a child both via its own transition and via the parent's unhide cascading through `_setDirectChildrenNoDraw`), a nonzero cell would install a `ShadowObjects` broadphase row for an equipped weapon at the parent's canonical cell — contradicting route 7's P4 record ("a committed child never joins a workset or shadow list", `RuntimeEntityDirectory.cs:451-465`), AP-142's model, and retail (a child's broadphase state is established at attach/unparent edges only; route-7 contract §0 trap 5a). An invisible-but-solid weapon row at a possibly-stale cell is the #184 shape. **Pinned:** `RestoreShadow` refuses records with a committed parent (same predicate as §3.1; checking `ProjectionKind` alone is acceptable if the implementer shows it is equivalent for every reachable record). Same commit, same rationale: correct today, required post-fix. ### 3.3 `RuntimeInitialCreateResidenceState.Begin` — waking is a NON-EVENT; verify with a test, no production change The investigation flagged `Begin`'s `record.FullCellId != 0u` refusal (`:583`) and `TryConvertToCellessRoute`'s (`:1041`) as unknowns. Resolved at HEAD: **the refusal is a defensive invariant, not a reachable gate.** `Begin`'s only production caller, `RuntimeEntityObjectLifetime.InitializeAcceptedCreateResidence` (`:2736-2771`), ZEROES a nonzero cell first — `:2751-2752`, `if (canonical.FullCellId != 0u) Entities.SetFullCell(canonical, 0u, 0u)` — before calling `Begin`. So a re-CreateObject that opens a fresh residence on a record inheriting a nonzero child cell zeroes it (flowing through the D2 chokepoint, correctly zeroing any grandchildren per AP-142 clause (a)), opens the `Parented` residence, and D1 re-cells at the attach commit — the same sequence a first create runs. `TryConvertToCellessRoute`'s refusal concerns an OPEN lease on a record with a committed cell — for a committed child the lease was already forgotten at the attach commit's cancellation prefix, so the arm is unreachable for the waking population. **Pinned:** no production change at this site. One test (§6 test 5) drives a re-CreateObject against an attached, non-zero-cell child (same incarnation → the `TryApplyAttachedAppearance` path; new incarnation → the replacement path with `InitializeAcceptedCreateResidence`) and asserts byte-identical outcomes to today's, plus the zero-then-re-cell sequence staying inside the transaction (no observable cell-less escape — route 7's D1 atomicity clause). --- ## 4. The fix, pinned ### F1 — the key: CreateObject-carried relations late-bind to the parent's live incarnation `EquippedChildRenderController.OnSpawn` stops writing the literal `0`: - **Parent snapshot known at accept time** → stage the relation with the parent's live `InstanceSequence` — the identical lookup `ResolveRelations` already hands to `Resolve` (`EquippedChildRenderController.cs:834-836`, `_liveEntities.TryGetSnapshot`). - **Parent not yet known** → do NOT stage under a guessed key. Route the relation through the existing unresolved/deferred machinery (the `RetryWaitingDescendants` / `WaitOwner.Parent` shape) so it resolves when the parent arrives, adopting the parent's incarnation THEN. Retail anchor for the late-bind semantic: `GetObjectA`/`GetNullObject` by GUID (§1) — the wire named a GUID, not an incarnation; matching "the current holder" is the faithful mapping. The implementer chooses the mechanism (a wildcard-incarnation relation resolved in `Resolve`, or deferring the `AcceptCreateObjectRelation` call itself); the pinned constraints: **no relation is ever staged or committed with an incarnation that does not equal the parent's live incarnation at that moment, and the `Resolve` early-return on staged relations (`ParentAttachmentState.cs:425-426`) must not strand a deferred CreateObject relation.** - **The commit-time tripwire** (the investigation's own recommendation, adopted): the parent-relation commit path asserts `relation.ParentInstanceSequence == parentRecord.Incarnation` whenever the parent is active — a mismatch REFUSES the commit loudly (throw or logged refusal per the codebase's commit-refusal idiom, implementer's choice — pinned outcome: never a silent success under a mismatched key). This is what converts any future producer regression from "silently inert propagation" into a failing test/session. This one change fixes the local player's login equipment AND every remote player's observed equipment (same producer). `ParentEvent`-path behavior is untouched. ### F2 — the two structural gates (§3.1, §3.2) Both are committed-parent exclusions at sites that must never handle an attached child. Both are behavior-preserving at HEAD (the excluded population currently reaches neither site's active arm) and land in the same commit so the key fix never ships without them. Sabotage rule: each gate's test must fail with the gate deleted AND the key fix present (§6 tests 3–4). ### F3 — probe truthfulness `ACDREAM_PROBE_CHILD_CELL` stays as-is (it is TEMPORARY, C5c strips it), but the fix commit re-runs the §7 gate expectations against it. No probe code change is required for this slice: the corrected gate criterion is a positive read of the child's cell, not a probe-line count. ### F4 — bookkeeping, in the fix commit (register rules 1–2 binding) - **AP-142 gains a clause** (or a new AP row if the reviewer prefers — implementer's call, one of the two): the CreateObject-carried relation late-binds to the parent's live incarnation because the wire carries no parent instance sequence; retail's attach is GUID-only (`GetObjectA` @0x00558a2d, `SetChildren` @0x005093f8, `GetNullObject` placeholder @0x005093e6). AP-132's row gains the clarifying sentence distinguishing the two producers (ParentEvent = wire-named incarnation, gated; CreateObject = no wire incarnation, late-bound). - **NEW register row — the local player's coarse canonical cell** (§1 item 2): intentional-architecture-or-stopgap classification to be argued in the row itself; anchors `CPhysicsObj::SetPositionInternal` @0x00515330 vs the three acdream writers; risk column names the §2.2 park-sweep question and the child-inherits-the-coarseness consequence (the child's cell is stale-but-equal wherever the parent's is). - **Follow-up issue filed**: local-player canonical cell tracking — carrying §2.2's enumeration and §9 item 2's unresolved spatial-root question as its first verification step. - **ISSUES.md**: #319 → Recently closed with the commit SHA; the entry's "do NOT rush the fix" block is answered by this contract's §2/§3. - **Route-7 contract**: append a dated supersession note under its §7 gate (the criterion correction is already recorded in the closeout handoff; the note points here). - **AP-142 clause (b)/(c) untouched**; no row deletions. --- ## 5. What must REMAIN true (route 7 §3, re-asserted for this slice) 1. Route 7 invariants 1–13 hold verbatim. Specifically re-tested here: equality invariant (1), child never self-simulating (2), no placement machinery on the route (3), **no `ConstrainTo`** (4), `TryCommitParent` keeps zero `LeaveWorld` calls (6), presentation advances and is asserted (7), **no per-crossing child shadow/cross-cell rebuild** (8), ledger convergence (13). 2. **The removal path still propagates ZERO** (AP-142 clause (a)) — the key fix must not perturb withdrawal/delete/EndGeneration edges; the §6 matrix re-runs them with a player-range parent. 3. **`RebucketLiveEntityPresentationOnly` does not become a canonical writer again** — the D4 demotion stands; the presentation rebucket stays keyed on the child guid alone. 4. **The `ParentEvent` path's incarnation gating (AP-132) is unchanged** — a wire-named stale incarnation still discards/queues exactly as today. 5. **No new canonical cell writer is introduced.** The fix adds zero `SetFullCell` call sites; it only makes the existing D1/D2 sites find their children. 6. **The dormant-residence deferrals are untouched** (route 7 invariant 9); the continuation executor's parent replay path benefits from F1 automatically (it routes through the same commit pair). --- ## 6. Test plan Rules (route 5 §7 / route 7 §6, verbatim where they apply): assert the layer that broke; positive facts, not absences; every new test fails against a broken implementation; sabotage-verify both halves of dual-layer assertions; never derive a test's workload from the constant under test. **The structural rule this issue adds — MANDATORY: every scenario runs as a dual-parent-class matrix.** One parent in the player range (`0x5…`, spawned with a nonzero incarnation — use a value > 1 so an off-by-one cannot pass), one in the creature/dynamic range (`0x8…`, incarnation 0), asserting IDENTICAL outcomes. #319 exists precisely because every prior test and both captured gate logs used sequence-0 parents; a matrix in which the two classes can diverge silently is the defect's habitat. Where a fixture helper spawns parents, the helper takes the incarnation as a parameter — no baked-in zero. Focused tests (`tests/AcDream.App.Tests` for the controller/producer, `tests/AcDream.Runtime.Tests` for the propagation surface): 1. **The key itself (F1), CreateObject path, matrix.** A CreateObject-carried equip against a live parent with incarnation N (N=0 creature, N=7 player): relation commits under `(parentGuid, N)` (`TryGetCommittedParent` returns N), D1 re-cells the child to the parent's exact cell at attach (`cause=attach` behavior — assert the cell equality, not the probe), and a subsequent parent canonical-cell change through each writer family reaches the child (D2). Sabotage: restore the literal `0` and confirm the PLAYER row fails while the creature row passes — the test must be able to see exactly #319. 2. **The deferred-parent flavor (F1), matrix.** Child CreateObject arrives BEFORE the parent's: relation stays unresolved (never staged under a guessed key — assert no committed relation exists), parent arrives with incarnation N, relation resolves and commits under N, child celled. Companion: parent arrives cell-less, later gains a cell → D2 catch-up re-cells the child (route 7 §6 test 1's companion, now for the CreateObject producer). 3. **Hydration gate (§3.1).** An attached child with nonzero canonical cell; its landblock reloads; assert the child is NOT a hydration candidate (no legacy rebucket, `entity.ParentCellId` untouched by the hydration pass, no `CommitRebucket` invocation for the child). Sabotage: delete the gate → test fails. 4. **Shadow gate (§3.2).** Attached child, nonzero cell, driven through a Hidden→Visible edge: assert NO `ShadowObjects` row exists for the child after the transition (assert against the engine's shadow table, not a cache — the AP-145/#318 lesson). Sabotage: delete the gate → fails. 5. **Residence non-event (§3.3).** Re-CreateObject shapes against an attached nonzero-cell child (same-incarnation refresh; new-incarnation replacement): outcomes byte-identical to the zero-cell baseline; the replacement path's zero-then-re-cell stays inside one transaction (no observable cell-less window — reuse route 7 test 1's observation technique). 6. **Unwield classification population (§2.1 item 4).** An attached child whose parent cell is nonzero (and deliberately DIFFERENT from the wire Position's cell, the stale flavor) receives an unparent Position: classification takes the TELEPORT_TS/distance route (never the cell-less arm), the drop commits at the WIRE position, and — the §11 guard — the 4b-3 synthetic `PreMergeCommittedCellId == 0` fixtures are NOT relabeled: they remain synthetic. 7. **Withdrawal/delete matrix (invariant 2).** Pickup of the parent, delete of the parent, `EndGeneration` — child (and a grandchild) go cell-less, both parent classes. 8. **The commit-time tripwire (F1).** A relation carrying a wrong incarnation reaching the commit path is REFUSED loudly; assert the refusal is observable (exception or logged refusal + no committed relation), not a silent success. 9. **Ledger convergence** (route 7 invariant 13): teardown/reset with a player-parented committed child present. The existing route-7 suites run unmodified — zero expectation changes outside the new tests is the tripwire that F1/F2 changed no non-child behavior. --- ## 7. The gate (connected, user-run) — the CORRECTED criterion, instantiated Release build, `ACDREAM_RETAIL_UI=1`, `ACDREAM_PROBE_CHILD_CELL=1`, live ACE, graceful close. Run BOTH halves; a session that runs only the creature half is a not-run for this issue (process rule (g): the gate must be able to see the defect — the defect is player-class-only). **Half A — player parent (the local player, `0x5000000A`):** 1. Login with equipment. **PASS requires a `[child-cell]` line for the player's child at attach-or-activation** (`cause=attach` if the player was celled first, else the D2 catch-up line when login activation commits the player's first cell) **AND the child's `FullCellId` read equal to the player's, nonzero.** Today this line does not exist (`c4-gates.log:238`, `c5-gates.log:343` — attach with no probe line); its appearance is the direct #319 signal. 2. Carry across ≥2 landblock boundaries and back, plus one EnvCell-to-EnvCell dungeon traversal. **PASS: child `FullCellId` EQUALS the player's at every checkpoint — a zero is a FAILURE, not a silence.** Expectation set honestly by §2: during WASD the player's canonical cell does not change, so `cause=propagate` lines are NOT expected at these crossings for the player half — equality (both records carrying the same value) is the assertion. This expectation is itself part of the gate record: if `propagate` lines DO appear here, something changed the player's cell writers and the session gets investigated, not celebrated. 3. Portal recall / portal transit while equipped. **PASS: `cause=propagate` fires for the player's child at the arrival commit** (the teleport writer is the player's live cell-change edge), child equal to the player's destination cell, equipment present and following. 4. Unequip/re-equip mid-session (the ParentEvent path): `cause=attach` with the true incarnation — this was the investigation §7 prediction; observing it closes that loop. 5. Reconnect with equipment: re-attach produces the same line set as step 1. **Half B — creature parent (`0x7…`/`0x8…`, e.g. observe an armed NPC or `@teleto` a wielding creature):** the route-7 recipe unchanged — `cause=propagate` in double digits across several crossings, child equal to parent at checkpoints. This half proves the fix did not perturb the already-working class. **ADDED (architecture review A4, 2026-08-05):** this half must ALSO watch the creature-parented weapon's shadow/collision row across a Hidden→Visible edge (`@hide`/`@unhide` an armed NPC, or a sit/stand-equivalent state toggle if available) — F2's `RestoreShadow` gate (`LiveEntityPresentationController.cs`) is NOT behavior-preserving at HEAD for this class the way the contract's §3.2 originally claimed: route 7's D1 already gives a creature-parented child a nonzero cell, so this gate is a live behavior change here (the weapon's broadphase row no longer refreshes on that edge), not an inert one. Confirm no #184-shaped regression (a stale-but-solid collider) and no crash; the change is believed correct per route 7's P4 record but was never exercised by a connected session before this note. **ADDED (retail review round 2, D1, 2026-08-05):** this half must ALSO carry an armed creature/NPC across a landblock UNLOAD and RELOAD (walk it out of streaming range and back, or force a landblock reload if the harness supports it) and confirm the weapon is still attached and correctly placed afterward — F2's hydration gate (`LiveEntityHydrationController.OnLandblockLoaded`) is likewise NOT behavior-preserving at HEAD for the creature class (§3.1's correction, same D1-already-nonzero fact): pre-fix a creature-parented child was already a hydration candidate on landblock reload and took the legacy `RebucketLiveEntity` path this gate now excludes. This is precisely route 7's "left behind at a boundary" regression shape if the gate's direction were wrong; confirm it is not. Secondary check only: probe-line volume. Primary: the equality reads. Regressions to watch: route 7's list verbatim (weapon at origin/stale ground position, invisible while equipped, left behind at a boundary, invisible-but-solid, culled against parent) — none is expected, since the presentation path is untouched. --- ## 8. Size estimate and the split call Calibrated against the campaign (route 7: ~127 production lines; route 3: ~418): | piece | non-comment production lines | |---|---| | F1 key + deferred late-bind + commit tripwire | 25–60 | | F2 hydration gate | 4–12 | | F2 shadow gate | 4–10 | | F3 | 0 | | **total** | **~35–80** | Tests are the larger share (~250–450 lines, dominated by the matrix). **ONE slice, no split.** F1 without F2 wakes the two sites; F2 without F1 is inert scaffolding; both are tiny. **If Decision 1 had gone to (a), the answer would be different in kind, and it is worth stating for the record: folding player-cell tracking in would put the slice at route-3 scale or above** (the §2.2 enumeration: projection-controller change, rebucket contract change, `Rebucketed` publication audit, route-2/4b-3 classification-input audit, portal-space race analysis, spatial-root/park analysis, plus its own connected gate) **— and it would still be the wrong bundling**, because the child fix is complete and gate-verifiable without it, and the player-cell question deserves its own retail-conformance argument rather than riding a key repair. Stop and report rather than pushing through when: 1. F1's deferred path needs new relation-state machinery beyond a wildcard/deferral flag (the unresolved-queue shape should suffice; if `Resolve`'s staged early-return forces a redesign of `ParentAttachmentState`'s state machine, that is a conversation, not an ad-hoc build). 2. Any test in §6 requires touching a canonical cell writer. 3. The §3.2 gate turns out to mask a child shadow row that something DOES legitimately create today (that would contradict route 7's P4 record — file, do not fix inline). 4. The complete Release suite deviates from its measured baseline beyond the two named flakes (#302, #308). Baseline at `e0f96a55` was 11,090 / 4 / 0 — MEASURE at the implementation HEAD, never inherit. --- ## 9. Open items — reported honestly, not smoothed 1. **The look-in render index nuance (§2.1 item 1) is argued from code, not from a live A/B.** The claim "the local player already exhibits this exact staleness in `_cellDynamics` with no observed symptom" is solid at HEAD reading, but the `RenderSceneShadowRuntime:403` residency assertion (`OwnerLandblockId != expected.LandblockId` throw) was not chased to its trigger population. P-R1: the implementer runs one indoor equipped session with the fix and confirms no shadow-runtime assertion fires. If one does, that is a finding about the PLAYER's record as much as the child's — report, do not patch the child. 2. **Whether the local player is a `RuntimePhysicsState` spatial root was NOT established** (§2.2). If it is, the pre-existing stale player cell can theoretically be swept by `ParkCollisionResidents` after a long teleport-free WASD run — unexercised by the connected routes, which teleport between stops. This rides in the follow-up issue as its first verification step; it is NOT #319's blast radius (the fix does not touch the player's record). 3. **The exact deferral mechanism for a parent-unknown CreateObject relation is implementer's choice** within F1's pinned constraints; this contract did not verify how `RetryWaitingDescendants` interacts with a relation that was deliberately NOT staged (today it retries realize for staged-but-unrealizable children). If the existing machinery cannot carry it, stop condition 1 applies. 4. **Everything else in the investigation was confirmed at HEAD**: the hardcoded 0 and its structural cause (the wire carries no parent instance for CreateObject), both miss sites, the `Resolve` early-return, the commit's missing validation, the three writers of the player's canonical cell, the landblock-preserve, the hydration/shadow wake mechanics (including `IsSpatiallyProjected = true` via the presentation-only rebucket at `LiveEntityRuntime.cs:1043`), and the render fallback. One investigation unknown was RESOLVED in the child's favor: §5.1 item 3's residence question — the caller zeroes before `Begin` (`RuntimeEntityObjectLifetime.cs:2751-2752`), §3.3.