# C4 route 7 — retail-conformance review, ROUND 2 (delta) **Date:** 2026-08-04 **Reviewer role:** independent retail-conformance reviewer, review-only. **Subject:** the uncommitted working tree at HEAD `19ebf043`, after the round-1 fix pass. Round 1 is `docs/research/2026-08-04-c4-route-7-retail-review.md` (verdict FAIL, R1–R11); the parallel architecture review is `docs/research/2026-08-04-c4-route-7-architecture-review.md` (A1–A10). **Scope:** delta only. Round 1's §A retail verification (every address in `enter_cell` / `leave_cell` / `change_cell` / `SetPositionInternal` / `update_object` / `set_parent` / `DoPickupEvent` / `DoParentEvent`) stands unchanged and is not re-litigated here. --- ## VERDICT: **PASS** All three round-1 MAJORs are closed, and closed properly rather than argued away: - **R1** — the implementer's pushback is **CORRECT and my premise was wrong in the letter**: `RuntimeEntityRecord.HasPartArray` does exist on the canonical record. I re-verified the writer enumeration independently and it is complete and correct (§1). R1's *conclusion* — that this is a real, unrecorded retail divergence needing a register row — was right, and AP-142 clause (d) is an honest row that does not mischaracterise retail's intent. - **R2 / A1** — the D4 test now queries real `GpuWorldState` landblock membership before and after the tick. I verified by construction that it cannot pass with the demotion removed (§2). - **R3 / A2** — the typed-disposition fork is **sound**, and the implementer's choice of the sanctioned alternative over the primary suggestion is the better call for a reason the review did not state (§3). R5, R7, R9, R10, R11 and all three contract corrections landed. Build green (0 errors); focused suites 22/22 Runtime + 28/28 App. Per process rule 5 that is not the basis of this verdict. Seven new MINORs (N1–N7) below, none blocking. Two of them (N1, N5) are the comment-precision class this campaign keeps hitting, and one (N6) is a new untested control-flow branch created by the R3 fix itself. --- ## 1. R1 — the pushback, verified on all three questions ### (a) Is the `HasPartArray` writer enumeration complete, and are both writers graphical-only? — **YES, verified.** Repo-wide grep over `src/` + `tests/` (excluding bin/obj) for `HasPartArray`: | site | role | |---|---| | `src/AcDream.App/Rendering/DatLiveEntityProjectionMaterializer.cs:203` | `SetHasPartArray(expectedCanonical, true)` — **App / graphical** | | `src/AcDream.App/Rendering/EquippedChildRenderController.cs:609` | `childRecord.HasPartArray = true` — **App / graphical** (the row cites `:591`; see N3) | | `src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs:1167` | `SetHasPartArray(canonical, false)` — Runtime, but a *clear*, not a set | | `RuntimeEntityDirectory.cs:495-498`, `LiveEntityRuntime.cs:250-253/:534-537` | plumbing (setter/forwarder), no value origin | | 6 sites under `tests/` | test-only | | `ProjectileController.cs:681/:720`, `RemotePhysicsUpdater.cs:101`, `LiveEntityAnimationScheduler.cs:240`, `EquippedChildRenderController.cs:920` | **readers only** | **No Runtime, Headless, or otherwise presentation-independent site ever sets `HasPartArray = true`, for a child or for an ordinary root.** The implementer's claim is exactly right, and the consequence it draws is right too: gating D1/D2 on `child.HasPartArray` would permanently strand every headless committed child — the functional inverse of this slice's purpose. My round-1 R1 asserted "acdream's propagation has no analogue"; the *field* has an analogue, the *canonical layer* does not. The correction is accepted. ### (b) Is AP-142 clause (d) an honest description? — **YES, with two precision defects (N1, N2).** The row states the guard's address (@0x00510ed8), its full effect (the write **and** the recursion **and** the subtree skip), that acdream writes unconditionally, why gating is not available, and puts the consequence in the risk column. It does not claim fidelity it lacks. That is what a divergence row is supposed to look like. ### (c) Is retail's guard moot under acdream's structure, or a real behavioural difference? — **A REAL behavioural difference, correctly accepted.** Retail's guard is not decorative. `enter_cell`'s `CObjCell::add_object` @0x00510ee2 maintains the cell's object list, which retail uses for *both* drawing and collision/visibility. So a part-array-less object in retail is neither drawn nor cell-resident, and acdream — where `FullCellId` is the residency predicate at 45+ sites — will mark such a child resident. The row says this plainly and puts "acdream celling a child retail would leave nowhere" in the risk column. **No mischaracterisation of retail's intent.** This is the question I was watching for and the row passes it. ### (b')/R10 — clause (b)'s over-claimed equivalence: **CORRECTED, accurately.** The row now reads: *"this is a clean equivalence only on the REMOVAL side. The skip ALSO prunes the child's whole subtree on a same-value WRITE, which retail's `enter_cell` does not do — it recurses over children unconditionally (@0x00510f03); only `leave_cell` prunes (@0x00510f5b, on `cell != 0`). Currently unreachable-by-construction … but it is an asymmetry, not a proven equivalence."* I re-read both addresses: `enter_cell`'s recursion @0x00510f03 is inside the `part_array` guard but has no per-child cell test, and `leave_cell`'s @0x00510f5b prune is exactly as described. **The correction is verbatim-accurate.** R10 closed. --- ## 2. R2 / A1 — the D4 presentation test: verified it cannot pass without the demotion `tests/AcDream.App.Tests/Rendering/EquippedChildProjectionWithdrawalTests.cs:321-402`. Verified by construction, not by taking the sabotage note on trust: 1. `fixture.Spatial` is a **real `GpuWorldState`** (`:1443`, `internal GpuWorldState Spatial { get; } = new()`), and `CopyLiveEntitiesNearLandblock` (`src/AcDream.App/Streaming/GpuWorldState.cs:493-520`) is a genuine per-landblock query over `_loadedLiveByLandblock`, radius 0, that early-returns empty if the world is unavailable. 2. The **pre-assertions** (`Assert.Contains(old, child)` + `Assert.DoesNotContain(new, child)`) do double duty: they establish a distinct starting bucket *and* they prove the query is live — a dead or always-empty query would fail the `Contains` immediately. This is what makes the post-assertion load-bearing rather than vacuous, and it is the exact property round 1's version lacked. 3. `TickChild`'s **only** spatial mutation for the child is the demoted call at `EquippedChildRenderController.cs:409-431`. `CommitRebucket` (the Runtime producer driven before the tick) writes canonical fields only and touches no spatial index. So stubbing `RebucketEquippedChildPresentation` leaves the child in `oldLandblock` and `Assert.Contains(newBuffer, child)` fails. **R2 closed.** The test now asserts the layer that historically broke. --- ## 3. R3 / A2 — the typed-disposition fork: sound, and the deviation from the review's primary suggestion is the better call `LiveEntityRuntime.cs:45-61` (the enum), `:1088-1148` (the method), `EquippedChildRenderController.cs:406-431` (the caller). The implementer took the *sanctioned alternative* (branch explicitly, do not fail the tick on `NotAttached`) over the review's primary suggestion (assert / propagate as a failure). **That is correct, for a reason worth pinning:** - `NotAttached` genuinely coincides with an in-flight subtree withdrawal. `OnChildBecameUnparented` already owns that teardown and drives `BeginProjectionSubtreeWithdrawal`. Returning `false` from `TickChild` would route the same child into `WithdrawForPoseLoss` in the same frame — a **second concurrent withdrawal trigger on a subtree that already has one in flight**. The implementer's stated worry is real, not defensive hand-waving. - `NoProjection` is the branch that *should* fail the tick, and it does. Its first guard (`TryGetCurrent` / `WorldEntity is not { }`) is unreachable from `TickChild` — `TryResolveExactAttachment` (`:379`, and again at `:409`) already required `IsCurrentRecord(child.ChildRecord)` and a non-null `WorldEntity` — so in practice `NoProjection` only arises from `RebucketLiveEntityPresentationOnly` returning `false`, i.e. a projection operation displaced mid-flight, where withdrawal is the right response. So the fork partitions cleanly: benign-decline vs displaced-projection, with the new failure path landing only on the genuinely-broken case. **R3 closed** (see N6 for the one gap it leaves). A9 was also folded in: the new method now carries the active-initial-create- residence gate (`:1136-1141`), making its "can never become a general bypass" doc claim true at the entry point rather than by call-ordering three files away. --- ## 4. Round-1 findings — closure status | # | Status | |---|---| | R1 | **CLOSED.** Premise corrected by the implementer (verified §1a); AP-142 clause (d) filed and honest (§1b/c). N1/N2/N4 are precision follow-ups on the row's text, not on the decision. | | R2 | **CLOSED.** §2. | | R3 | **CLOSED.** §3. | | R4 | **PARTIAL — confirmed still non-blocking, now more comfortably so.** Writer families (b) `RuntimePhysicsState.CommitCanonicalCell` and (c) `RuntimeSetPositionState` still have no dedicated propagation test, and P2 is still only asserted on one path. Three things reduce the residual risk below the blocking line: the hook is at the *funnel*, not per-caller (so a missed writer is structurally impossible, not merely unobserved); **both** reviewers have now independently traced the complete `SetFullCell` caller set (the architecture review's 13-row table, which I spot-checked against my own round-1 grep and found consistent); and the connected gate's pass criterion is `cause=propagate` probe lines at each landblock crossing, which *is* the physics/placement writer families in live play. P8 and P4 are now stated in-code (`RuntimeEntityDirectory.cs`'s doc `` blocks) with A3's correction folded in. | | R5 | **CLOSED.** `RuntimeInitialCreateContinuationExecutor.cs:2184-2190` — `EndChildProjection` now precedes `LeaveWorld` on the dormant replay, matching @0x0045227F-before-@0x00452286. Both pickup paths are one shape, and the fix is real code, not a narrowed comment. | | R6 | **CLOSED as documentation.** `RuntimeLiveEntitySessionController.cs:345-368` now states the gap in full — the `TryCommitParent` position-timestamp gate, why nothing re-drives it, why `RetryChildrenWaitingForParent` does not cover it (the relation is staged, not unresolved), that it is not a regression, and that invariant 9 is untouched. That is the right disposition for a pre-existing gap surfaced by a new drive. | | R7 | **CLOSED.** AP-143 now names all three skipped checks with the correct retail anchors and an inertness argument for (1) and (2). Verified against the code: self-parent `:915-916`, `HasPartArray` `:920`, `HoldingLocations` `:924-937`. (Line citations are stale — N3.) | | R8 | **CLOSED / withdrawn.** My nested-descendant concern was wrong: a grandchild's own `ParentEvent` reaches `ResolveAndCommitChildAttachment` directly, and if it arrives before its parent spawns, `RetryChildrenWaitingForParent(childGuid)` fires on that spawn. No transitive descent is needed for this host. The new `ChildrenUnresolvedForParent` (`ParentAttachmentState.cs:642-663`) scopes the sweep correctly and its doc states the narrower claim honestly ("This is a narrower claim than '0 B'"). | | R9 | **CLOSED.** The C3c-R1 F6 summary is back on `FirstEntryDriveServesOneRouteAtATimeAndScopesClearToTheOwner` (`:422-430`); the D5 tests carry their own. | | R10 | **CLOSED.** §1(b'). | | R11 | **CLOSED in substance, incomplete in its stated proof.** See N7. | **Contract corrections — all three landed and are accurate:** §0 item 1 now carries the `part_array` guard correction with the "the contract causes the defect" attribution (`:75-87`); §2's two gate rows are marked UNVERIFIABLE with the BN always-false-lowering explanation and the note that D7/D6 do not depend on them (`:236-237`); test 10's sabotage instruction now requires **two** runs with round 1's failure recorded as the cautionary example (`:724-736`). --- ## 5. New findings (round 2) ### N1 — MINOR — AP-142 clause (d) overstates the semantic gap between retail's `part_array` and acdream's `HasPartArray` **File:** `docs/architecture/retail-divergence-register.md:172`. **Retail address:** `CPhysicsObj::makeAnimObject` @0x0050e930. The row says acdream's flag *"means 'the renderer built a mesh,' not retail's 'this CPhysicsObj has ANY part array.'"* Retail's flag has exactly one assignment site: ``` 0050e930 int32_t CPhysicsObj::makeAnimObject(CPhysicsObj* this, IDClass<_tagDataID,32,0> arg2, int32_t arg3) 0050e93e class CPartArray* eax = CPartArray::CreateSetup(this, arg2, arg3); 0050e94d this->part_array = eax; ``` `part_array` **is** the product of building the object's parts from its Setup — i.e. retail's flag also means "the client built this object's parts". The two are near-synonyms, not a broad/narrow pair. The real reason acdream cannot gate on it is a **layering** commitment, not a semantic mismatch: Slice J made the canonical Runtime layer presentation-independent by design, so the only place the flag can be set is App, and headless has no part-construction step at all. The row's operative claim and its conclusion survive intact. The framing should be corrected so a future reader does not conclude retail's guard was looser than it is — that misreading would make the divergence look smaller than it is. ### N2 — MINOR — clause (d)'s risk column scopes the divergence as headless-only; the graphical host has the same window **File:** `docs/architecture/retail-divergence-register.md:172` (risk column); `src/AcDream.App/Rendering/EquippedChildRenderController.cs:609` vs `:876-880`. The risk column argues (d) is *"a headless-only concern, since the graphical `HasPartArray` gate is already implicitly satisfied by the time `TickChild` can run — a child's own `WorldEntity`/mesh must exist for `TickChild` to reach the rebucket call at all."* That is true of the **rebucket** and false of the **canonical write**, which is what clause (d) is about. The graphical realize order is: ``` PrepareAndTryRealize: CommitStagedParent → CommitProjection → CommitAcceptedParentCellless ← D1 writes the child's cell HERE → WithdrawPriorProjection → TryRealize └─ :609 childRecord.HasPartArray = true ``` So the graphical host also cells a child before its part-array flag is true. Retail has no equivalent window: a `CPhysicsObj`'s `part_array` is built at object creation (`makeAnimObject`), long before any `set_parent` @0x00515A90 can run. The window is bounded (nothing reads the child's cell between those two calls in the same synchronous realize) and inert, but the scoping sentence is wrong as written and should say "predominantly headless, plus a bounded graphical realize-ordering window". ### N3 — MINOR — same-commit stale line citations in both new register rows - AP-142 cites `EquippedChildRenderController.cs:591` for the `HasPartArray` writer; it is at **`:609`**. - AP-143 cites `:897-898` (self-parent) and `:902` (`HasPartArray`) in `ValidateParentProjection`; they are at **`:915-916`** and **`:920`**. Both are off by the +18 lines this same commit's D4 edit inserted above them — i.e. the citations were written against the pre-fix file and not re-checked after. Contract process rule 6 ("trust the symbol") makes them recoverable, and the symbols are named, so this is cosmetic. It is listed because "verify every comment the fix touched" is a standing rule and these were touched by the fix. ### N4 — MINOR — clause (e)'s depth-cap residue is the shape clause (a) declares unacceptable, and is strictly worse on the withdraw path **File:** `docs/architecture/retail-divergence-register.md:172` (clause e); `src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs:349-357/:389-397`; `tests/AcDream.Runtime.Tests/Entities/RuntimeEntityChildCellPropagationTests.cs:430-486`. Clause (a) says acdream deliberately refuses to reproduce retail's stale-nonzero-cell-under-a-dead-object residue, *because* a stale nonzero `FullCellId` reads as "resident" to every acdream predicate. Clause (e)'s cap re-introduces precisely that residue past 64 levels — the shipped test asserts it directly (`Assert.Equal(originalCell, tail.FullCellId)`). On a **crossing** that is a stale-but-plausible cell (cosmetic). On a **withdraw** (`SetFullCell(x, 0, 0)`) the tail keeps a nonzero cell forever, which is the #184 invisible-but-solid shape verbatim. Clause (e) discloses "left at its PRIOR cell rather than partially propagated", so nothing is hidden — but the (a)/(e) interaction is not noted, and the withdraw case is the one worth a sentence. The architecture review's suggested iterative worklist would retire both. ### N5 — MINOR — the `NotAttached` test's third assertion cannot distinguish decline from a same-destination move **File:** `tests/AcDream.App.Tests/Rendering/EquippedChildProjectionWithdrawalTests.cs:404-447`. The comment says *"the draw bucket did NOT move, because the guard correctly declined (NotAttached) and `RebucketLiveEntityPresentationOnly` was never called"*, and the assertion is `Assert.Contains(CopyLiveEntitiesNearLandblock(oldLandblock), child)`. In that fixture the parent's `ParentCellId` is never changed, so a rebucket that *did* run would target the child's current bucket and the assertion would pass identically. It cannot fail either way. The test's load-bearing assertions — the tick still counted a pose-composition visit and the child is still in `AttachedEntityIds` (i.e. it was **not** treated as pose loss) — are sound and do pin the fork's benign branch. Only the third comment overclaims what its assertion demonstrates. Same class as round 1's R2, one severity lower. Moving the parent to a new cell first would make it real. ### N6 — MINOR — the `NoProjection` branch is a new withdrawal trigger with no test **File:** `src/AcDream.App/Rendering/EquippedChildRenderController.cs:418-424`. `TickChild` now returns `false` on `NoProjection`, which routes the child into `WithdrawForPoseLoss` via `Tick()`'s `failed` list — a control-flow edge that did not exist before this slice (the old `RebucketLiveEntity` call's result was never consulted). Round 2 added a test for `Moved` and one for `NotAttached`; `NoProjection` has none. It is the branch with real consequences, and it is reachable only via `RebucketLiveEntityPresentationOnly` returning `false` (a displaced projection operation) — exactly the kind of narrow path that regresses silently. ### N7 — MINOR — R11's "proven, not merely assumed" enumerates two of three `PhysicsBody` constructors **File:** `src/AcDream.App/World/LiveEntityRuntime.cs:1113-1124` (the R11 ``). The doc argues: a committed child's `Snapshot.Position` is always null, and *"the ONLY production `PhysicsBody` constructors reachable from a live projection (`DatLiveEntityProjectionMaterializer`'s static scheduler, `ProjectileController`) both require a non-null `spawn.Position`"*. Repo-wide grep for `GetOrCreatePhysicsBody` returns a **third** production constructor: `src/AcDream.Runtime/Entities/RuntimeRemoteFirstEntryState.cs:425`, which builds from `lease.InitialCreate.Physics` — and a committed child *can* hold an initial-create lease (that is the whole dormant-residence machinery). This matters slightly more than a bookkeeping nit because `RuntimeEntityRecord.SuspendObjectClock` (`RuntimeEntityRecord.cs:94-98`) does **not** synchronise a body's `TransientStateFlags.Active` bit — it only deactivates the clock — so if a committed child ever did hold a body, the dropped `SynchronizePhysicsBodyActiveState` would be a real loss rather than a no-op. The conclusion is still very likely correct (first-entry admission should require a placement position, which a parented child never has), but **I did not resolve that third path and am flagging it rather than guessing.** One sentence covering `RuntimeRemoteFirstEntryState` would convert this back to a genuine proof. This is the same defect shape as the architecture review's A3 (an enumeration argued against an incomplete set). --- ## 6. Verification performed - Re-read `CPhysicsObj::makeAnimObject` @0x0050e930 in `docs/research/named-retail/acclient_2013_pseudo_c.txt` (N1's basis) and re-confirmed `enter_cell` @0x00510f03 / `leave_cell` @0x00510f5b for R10's correction. - Independent repo-wide grep of `HasPartArray` (§1a) and `GetOrCreatePhysicsBody` (N7). - Read `GpuWorldState.CopyLiveEntitiesNearLandblock` and the `ControllerFixture` to confirm the D4 test queries a real spatial index (§2). - Traced `TickChild` → `RebucketEquippedChildPresentation` → `RebucketLiveEntityPresentationOnly` for the disposition fork's reachability (§3). - `dotnet build AcDream.slnx -c Debug` → 0 errors. - `AcDream.Runtime.Tests` filtered to `ChildCellPropagation` + `RuntimeLiveEntitySessionControllerTests` → **22 passed / 0 failed**. - `AcDream.App.Tests` filtered to `EquippedChildProjectionWithdrawalTests` → **28 passed / 0 failed**. - Contract §0 item 1, §2 rows, and §6 test 10 re-read against the code they describe. ## 7. Recommended before commit (none blocking) 1. N1 + N2: two sentences in AP-142 clause (d) — correct the `part_array` semantics with the @0x0050e930 anchor, and re-scope the risk column to include the bounded graphical realize-ordering window. 2. N3: fix the four line citations (`:591`→`:609`, `:897-898`→`:915-916`, `:902`→`:920`). 3. N7: one sentence covering `RuntimeRemoteFirstEntryState.cs:425`, or drop the word "proven". 4. N4: one clause noting that (e)'s residue on the *withdraw* path is (a)'s rejected shape. 5. N5: move the parent's cell in the `NotAttached` test, or soften its comment. 6. N6: a `NoProjection` test, whenever the withdrawal path is next touched.