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 atcff52c44, +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>
366 lines
22 KiB
Markdown
366 lines
22 KiB
Markdown
# 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 `<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.
|