acdream/docs/research/2026-08-04-c4-route-7-retail-review-round2.md
Erik cd3129e9d6 fix(physics): C4 route 7 — child cell propagation moves from a render tick into Runtime
Retail re-cells children when their parent crosses a cell, recursively, to
unbounded depth. acdream did it from a RENDER tick, so headless parented
children were cell-less forever and the canonical cell had two writers. This
slice makes Runtime the sole authority and demotes App's tick to
presentation-only. Contract:
docs/research/2026-08-04-c4-route-7-contract.md; the research that unblocked
it is docs/research/2026-08-04-retail-parent-cell-propagation.md (ca96ea5e).

Retail: SetPositionInternal @0x00515330 branches on `this->cell == curr_cell`
@0x0051536d; the changed branch reaches change_cell @0x00513390, whose
delegates leave_cell @0x00510f50 and enter_cell @0x00510ed0 self-recurse over
children and write the FULL identity (add_object @0x00510ee2, objcell_id
@0x00510f1e, part-array cell id @0x00510f2b, cell pointer @0x00510f35).
change_cell itself has no child loop.

THE TRAP, recorded because it nearly shipped: the depth-1 loop
@0x0051539c-0x005153d8 is the SAME-CELL fast path (objcell_id and part-array
id only, deliberately not the cell pointer), NOT the propagation. An
implementer who finds it first concludes "depth-1, id-only" and strands every
equipped item at a landblock boundary — the #184 class. The clincher against
that reading: update_object @0x00515d10 early-returns on `parent != 0`
@0x00515d40, so a child never runs its own physics tick and parent
propagation is the ONLY mechanism maintaining its cell.

Route 7 performs NO placement (DoPickupEvent @0x00452240 = unset_parent +
leave_world; DoParentEvent @0x00452290 = set_parent + SetPlacementFrame), so
it arms ConstrainTo nowhere — the leash rule INVERTS relative to routes
2/4/5, and both reviewers confirmed nothing arms.

Propagation is an ITERATIVE WORKLIST, not recursion. The first implementation
recursed with a depth-64 cap; both reviews independently found the cap left a
truncated tail at a stale NON-ZERO cell — permanently unrecoverable, logged
only under a probe flag, and on the withdraw path exactly the #184 shape
AP-142 clause (a) exists to reject. Shipping a fresh #184 instance inside the
slice that fixes stranded children was not acceptable, so the cap was removed
rather than tuned. The worklist retires the cap, the constant, its register
clause, and the failure mode together. Termination: every record on the stack
is already at the target pair, so nothing can be pushed twice and a hostile
A->B->A cycle collapses without a visited set.

The child write deliberately bypasses the public RuntimeEntityDirectory
.SetFullCell and calls the record method directly. This is LOAD-BEARING:
the public method re-enters PropagateFullCellToChildren, which opens with
_propagationWorklist.Clear() — routing children through it mid-drain would
wipe the shared stack and silently drop every unprocessed sibling. Any future
side effect added to the public SetFullCell must be mirrored by hand at that
call site.

Deliberate divergence, recorded not disguised: retail's removal path leaves
children with a null cell pointer but a STALE nonzero objcell_id @0x005133c1.
acdream does not reproduce it, because FullCellId != 0 is the liveness
predicate at 45+ sites — faithful porting would mark dead children live.
AP-142 records this; clause (d) records that acdream cannot gate propagation
on HasPartArray the way enter_cell gates on part_array @0x00510ed8, because
the flag's only writers are graphical and headless never sets it — the reason
is Slice J LAYERING, not a semantic difference (retail's part_array is itself
a mesh-construction product, single assignment site makeAnimObject
@0x0050e930 -> CPartArray::CreateSetup @0x0050e93e).

D7 adopts retail's unset_parent-before-leave_world order @0x0045227f ->
@0x00452286, applied to BOTH pickup paths including the dormant executor
replay. Its inertness was verified by reverting it and finding all 12
propagation tests still green — reported honestly rather than papered over
with a manufactured test, and independently confirmed by both reviewers.

ClassifyLeaveWorld and its request/cause types are DELETED: retail has no
classification here, and method-per-cause IS the retail dispatch shape.
Wiring it would have forced a vacuous teleport-sequence predicate with the
#307 shape.

Two review rounds plus a coordinator-required third pass; 5 MAJORs. One was a
handoff failure worth recording: enter_cell's part_array guard was correctly
identified as load-bearing by the research, dropped by the contract when it
enumerated the writes, and inherited as an omission by the code — a right
finding that evaporated across two handoffs with nobody re-reading the source.
Another was a test that survived deleting the entire behaviour it claimed to
pin, because its assertion read a field written unconditionally one line
earlier.

NoProjection is structurally unreachable from TickChild (TryResolveExactAttachment
performs a strictly stronger form of the same guard one call earlier). Kept as
a fail-safe, unit-tested directly, and documented in two places rather than
wrapped in a fabricated end-to-end test.

Headless regression test — the direct gate for this defect, which FAILED
before this work because no code path existed:
RuntimeLiveEntitySessionControllerTests
.DirectSink_D5_StandaloneParentEventCommitsChildToParentsExactCell.

Probe: ACDREAM_PROBE_CHILD_CELL=1 emits [child-cell] lines at all four write
sites (attach / headless-attach / propagate / withdraw / delete). TEMPORARY.

Complete Release suite MEASURED at 11,079 passed / 4 skipped / 0 failed
(baseline 11,063 at cff52c44, +16). An allocation flake appeared once under
load and was proven NOT this slice by reachability — RuntimeCollisionReportingState
contains zero SetFullCell and zero ParentAttachments references.

STILL OWED: the two-client connected gate (equip/unequip, carry across
landblock boundaries, pickup, loot, reconnect) with ACDREAM_PROBE_CHILD_CELL=1,
and a session counts only if [child-cell] cause=propagate lines appear.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 23:53:05 +02:00

366 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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, R1R11); the parallel architecture review is
`docs/research/2026-08-04-c4-route-7-architecture-review.md` (A1A10).
**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 (N1N7) 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 `<para>` 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
`<para>`).
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.