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>
504 lines
29 KiB
Markdown
504 lines
29 KiB
Markdown
# C4 route 7 — retail-conformance review (independent)
|
||
|
||
**Date:** 2026-08-04
|
||
**Reviewer role:** independent retail-conformance reviewer, review-only (no edits,
|
||
no commits, no fixes).
|
||
**Subject:** the uncommitted working-tree diff at branch
|
||
`claude/acdream-physics-divergence-5aa784`, HEAD `ca96ea5e`
|
||
(`git diff HEAD` + the two untracked files
|
||
`docs/research/2026-08-04-c4-route-7-contract.md` and
|
||
`tests/AcDream.Runtime.Tests/Entities/RuntimeEntityChildCellPropagationTests.cs`).
|
||
The route-7 contract and the route-3 scoping docs are inputs, not subject.
|
||
|
||
---
|
||
|
||
## VERDICT: **FAIL**
|
||
|
||
Narrow fail. The retail mechanism this slice ports is **correct** — I re-read
|
||
every cited address in `docs/research/named-retail/acclient_2013_pseudo_c.txt`
|
||
and the propagation research is accurate on all of them (see §A). The directory
|
||
chokepoint is genuinely the sole funnel (§B). `ConstrainTo` is never armed (§C).
|
||
`TryCommitParent` still holds exactly zero `LeaveWorld` calls (§D). D7 adopts
|
||
retail's real order and is genuinely inert (§E).
|
||
|
||
The fail is on three counts, all small to fix:
|
||
|
||
1. **R1 (MAJOR)** — retail's `enter_cell` gates the ENTIRE cell write *and the
|
||
recursion* on `this->part_array != 0` @0x00510ed8. The shipped propagation
|
||
has no analogue and **no register row**. The propagation research itself
|
||
flagged this guard as "Guard, load-bearing" (§3); the contract's §0 item 1
|
||
enumerated `enter_cell`'s five writes and silently dropped the guard, and the
|
||
code inherited the omission. Register rule 1: a deviation found without a row
|
||
is a bug twice over.
|
||
2. **R2 (MAJOR)** — D4's presentation half is **untested**. The one App-layer
|
||
assertion is satisfied by a line that runs before, and independently of, the
|
||
demoted call. `RebucketEquippedChildPresentation` has zero references in
|
||
`tests/` repo-wide. This is exactly the layer contract §6 test 10 named
|
||
("the child's render entity moved buckets (spatial index / visibility
|
||
state)") and exactly the #184 / route-4a-R1 regression D4's own text calls its
|
||
specific risk.
|
||
3. **R3 (MAJOR)** — `TickChild` discards the demoted call's `bool`, and the new
|
||
`HasCommittedParent` guard introduces a silent-false path the old
|
||
`RebucketLiveEntity` did not have. Contract D4 said "**assert** the record has
|
||
a committed parent relation"; the code returns `false` and the caller ignores
|
||
it. Route 5's A1 defect class, verbatim.
|
||
|
||
Everything else is MINOR. Build is green (`dotnet build AcDream.slnx -c Debug`:
|
||
0 errors) and the focused Runtime suites pass (21/21, including all 12 new
|
||
propagation tests and the 2 new headless D5 tests). Per process rule 5 that is
|
||
not evidence, and it is not what this verdict rests on.
|
||
|
||
---
|
||
|
||
## A. Retail ground truth — verified first-hand, not inherited
|
||
|
||
Every claim below was re-read from
|
||
`docs/research/named-retail/acclient_2013_pseudo_c.txt` at the stated address.
|
||
|
||
| Claim under review | Verdict |
|
||
|---|---|
|
||
| `change_cell` @0x00513390 delegates: `leave_cell` @0x0051339f, `enter_cell` @0x005133af, early return @0x005133b5; removal tail @0x005133c1 zeroes only **this**'s `objcell_id`, `cell = nullptr` @0x005133d8. **No child loop of its own.** | ✓ CONFIRMED verbatim |
|
||
| `enter_cell` @0x00510ed0 self-recurses over `children->objects.data[i]` @0x00510f03; per level writes `CObjCell::add_object` @0x00510ee2, `objcell_id` @0x00510f1e, part-array cell id @0x00510f2b, `cell` pointer @0x00510f35, lights @0x00510f3e. Unbounded depth. | ✓ CONFIRMED verbatim |
|
||
| `leave_cell` @0x00510f50 mirrors it: self-recursion @0x00510f84, `remove_object` @0x00510f5e, `cell = nullptr` @0x00510fa7, and **never** writes `objcell_id` (source of the stale-id asymmetry). Per-level guard `this->cell != 0` @0x00510f5b prunes an already-cell-less subtree. | ✓ CONFIRMED verbatim |
|
||
| **The depth-1 loop @0x0051539c-@0x005153d8 is the SAME-CELL fast path.** It sits inside `if (this->cell == curr_cell)` @0x0051536d; the `else` @0x00515372 is `change_cell`. The loop writes child `+0x4c` (`m_position.objcell_id`) @0x005153bd and child `+0x10`'s part-array cell id @0x005153cc — **id only, never `+0x90` (`cell`)**, and **not** recursive. | ✓ CONFIRMED — this is the design's load-bearing claim and it holds |
|
||
| `update_object` @0x00515d10 early-returns on `parent != 0` @0x00515d40 (`this_3->parent != 0 \|\| this_3->cell == 0 \|\| state & 0x1000000`), clearing `transient_state & ~0x80` and returning. A child never runs its own tick. | ✓ CONFIRMED verbatim |
|
||
| `DoPickupEvent` @0x00452240 = timestamp gate, stamp write @0x00452278, `unset_parent` @0x0045227f, `leave_world` @0x00452286. **No placement, no `ConstrainTo`, no `HandleReceivedPosition`.** | ✓ CONFIRMED — the `unset_parent`-before-`leave_world` ORDER is unambiguous |
|
||
| `DoParentEvent` @0x00452290 = gate, `SetParentedState(1)` for a non-player parent gaining its first child @0x004522f4, `set_parent` @0x00452305, `SetPlacementFrame` @0x00452313. No placement, no `ConstrainTo`. | ✓ CONFIRMED verbatim |
|
||
| `set_parent` @0x00515a90 has **exactly ONE** `leave_world` @0x00515ac1, after `unset_parent` @0x00515aba; then `parent =` @0x00515ac6, `if (edi->cell != 0)` @0x00515ad1, `change_cell` @0x00515ad6, `UpdateChild` @0x00515b0e, `recalc_cross_cells` @0x00515b15, NoDraw inheritance @0x00515b26. | ✓ CONFIRMED — one, and only one, `leave_world` |
|
||
| Per-move path calls the **non-recursive** `calc_cross_cells` @0x0051551b / shadow rebuild @0x0051553e-4c; the recursive `recalc_cross_cells` @0x00515a30 runs at attach only. | ✓ CONFIRMED |
|
||
|
||
**Research-document defects found (2, both minor, neither changes the verdict):**
|
||
|
||
- **The "read verbatim" blocks are normalized, not verbatim.** e.g. research §2
|
||
prints `if ((state & 0x1000) == 0)` where the file reads
|
||
`if ((*(uint8_t*)((char*)((int16_t)this->state))[1] & 0x10) == 0)`. Same bit,
|
||
but the doc's own §7 ledger calls these "read directly from the pseudo-C
|
||
(verbatim, cited)". Paraphrase presented as transcript. Harmless here; a
|
||
hazard if a future reader diffs against the file.
|
||
- **The `DoPickupEvent` / `DoParentEvent` gates are NOT readable from this
|
||
source.** Binary Ninja lowered both to `if (-((eax_4 - eax_4)) != 0)` — a
|
||
literal always-false expression — because the x87/flag-based wrapped-sequence
|
||
comparison was lost. The contract's §2 row cites "gate
|
||
@0x0045224B-0x00452274" as if it were legible; only its *shape* (a
|
||
wrap-aware sequence compare against `update_times[0]`) is. **Flagged as
|
||
unverifiable rather than guessed.** It does not affect D7 (the ORDER is
|
||
legible) or D6 (whose argument is that acdream already enforces the
|
||
POSITION_TS gate in `InboundPhysicsStateController`, which I verified at
|
||
`InboundPhysicsStateController.cs:180-186/:263-271/:308-314`).
|
||
|
||
---
|
||
|
||
## B. D2's chokepoint claim — independently verified
|
||
|
||
Contract §12 open question 1 asks whether any canonical cell write bypasses the
|
||
funnel. Repo-wide grep for `SetFullCell` / `RefreshDerivedState` /
|
||
`CanonicalLandblockId =` over `src/**` (excluding bin/obj) returns:
|
||
|
||
- `RuntimeEntityRecord.SetFullCell` (`RuntimeEntityRecord.cs:244-251`) — two
|
||
callers only: `RuntimeEntityDirectory.SetFullCell` (`:348-356`, now hooked)
|
||
and `RuntimeEntityRecord.RefreshDerivedState` (`:230-242`).
|
||
- `RefreshDerivedState` — reached from the record constructor (`:29`, no
|
||
children possible) and `RuntimeEntityDirectory.RefreshSnapshot` (`:234-247`,
|
||
now hooked with an explicit `previousCell` compare).
|
||
- `LiveEntityRecord.FullCellId` / `.CanonicalLandblockId` setters
|
||
(`LiveEntityRuntime.cs:189-204`) route to `_directory.SetFullCell`;
|
||
`LiveEntityRecord.RefreshDerivedState` (`:353-354`) routes to
|
||
`_directory.RefreshSnapshot`. **No App-side bypass.**
|
||
- All eight producers (`CommitRebucket`, `RuntimePhysicsState.CommitCanonicalCell`,
|
||
`RuntimeSetPositionState`'s four writes, the withdrawal family, the executor's
|
||
`:2188`) call `Entities.SetFullCell`.
|
||
|
||
**Conclusion: the chokepoint holds. No hole.** I also confirmed
|
||
`canonicalLandblockId` is uniformly `(cell & 0xFFFF0000) | 0xFFFF` at every
|
||
producer, so D2's skip-on-equal-`FullCellId` cannot leave a stale landblock id.
|
||
|
||
Cycle termination and re-entrancy were checked by construction: the propagation
|
||
writes fields and bumps a version only, never mutates the relation tables it is
|
||
iterating, and `ChildrenAttachedToParent` returns the stored `List<uint>` or
|
||
`Array.Empty` (0 B).
|
||
|
||
---
|
||
|
||
## C. Route 7 arms nothing — verified
|
||
|
||
`ConstrainTo` / `ConstrainPhase` / `PositionManager` / park / service-window /
|
||
`CanAttemptDestination` appear **nowhere** in the diff's production hunks. The
|
||
D8 inversion is respected. `RuntimeEntityChildCellPropagationTests
|
||
.NeverArmPartition_D8_...` asserts the SetPosition operation ledger is unchanged
|
||
across attach + five crossings + pickup + delete, and the committed-relation
|
||
count converges to zero. ✓
|
||
|
||
## D. T8 — `TryCommitParent` `LeaveWorld` omission preserved
|
||
|
||
`RuntimeEntityObjectLifetime.TryCommitParent` (`:1400-1453`) still has zero
|
||
`Physics.CollisionReports.LeaveWorld` calls; the F4 comment (`:1430-1437`) is
|
||
intact and its retail citation (@0x00515A90's single `leave_world`) is correct
|
||
per §A. No second `LeaveWorld` was added anywhere in the parent commit family. ✓
|
||
|
||
## E. D7's inertness — verified, and the implementer's honest report is CORRECT
|
||
|
||
The implementer reported that reverting D7's ordering leaves all 12 propagation
|
||
tests green, and reported it rather than manufacturing a test. **That is true,
|
||
and "unobservable" is genuinely true against today's code**, for a reason worth
|
||
writing down:
|
||
|
||
- `EndChildProjection(guid)` → `RemoveCommittedChild(guid)` removes the entity
|
||
as a **child** (`_lastAcceptedByChild` + its parent's committed list). It does
|
||
**not** touch `_committedChildrenByParent[(guid, incarnation)]` — the entity's
|
||
own children. `RemoveCommittedParentReferences` would, and is not called.
|
||
- The only work between the two positions is
|
||
`CollisionReports.LeaveWorld` → `SetPosition.Forget` → `SuspendObjectClock` →
|
||
`SetFullCell(0,0)`. I checked every `ChildrenAttachedToParent` consumer
|
||
(`RuntimeSetPositionState.cs:6015-6063`, `ArmLostFamilyDeadlines` /
|
||
`CancelLostFamilyDeadlines`): both enumerate `operation.Record`'s **own**
|
||
children, i.e. the picked-up entity's subtree, which `EndChildProjection` does
|
||
not alter. Nothing else reads the child-relation table in that window.
|
||
- Even in a hostile A→B/B→A cycle both orders converge to the same values.
|
||
|
||
So: **no observable difference exists that anyone failed to construct.** The
|
||
correctness is contingent on "no consumer reads the child-relation table between
|
||
those two points" — a property of today's code, not a semantic equivalence — so
|
||
adopting retail's real order is right on principle and the change should stand.
|
||
See R5 for the half that was missed.
|
||
|
||
---
|
||
|
||
## Findings
|
||
|
||
### R1 — MAJOR — retail's `part_array != 0` guard is dropped, with no register row
|
||
|
||
**File:** `src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs:381-403`
|
||
(`PropagateFullCellToChildren`) and
|
||
`src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs:1517-1535` (D1).
|
||
**Retail address contradicted:** `CPhysicsObj::enter_cell` @0x00510ed8.
|
||
|
||
Retail:
|
||
|
||
```
|
||
00510ed0 void __thiscall CPhysicsObj::enter_cell(CPhysicsObj* this, CObjCell* arg2)
|
||
00510ed8 if (this->part_array != 0) <-- the ENTIRE body, including the
|
||
00510ee2 CObjCell::add_object(...) child recursion, is inside this
|
||
00510f03 enter_cell(child, arg2) guard
|
||
00510f1e objcell_id = ...
|
||
00510f35 this->cell = arg2
|
||
```
|
||
|
||
A child with a null `part_array` receives **nothing** — no `add_object`, no
|
||
`objcell_id`, no `cell` pointer — and **its whole subtree is skipped**, because
|
||
the recursion is inside the guard. The propagation research called this out
|
||
explicitly and named it load-bearing (§3, "Guard, load-bearing"). The route-7
|
||
contract's §0 item 1 lists the five writes and omits the guard entirely, and the
|
||
shipped propagation writes the child's cell unconditionally.
|
||
|
||
**Why it matters in acdream specifically:** `FullCellId` is the residency/liveness
|
||
predicate at 45+ sites (the contract's own §0 item 6). Writing a nonzero cell for
|
||
an object retail would leave nowhere makes it "resident" to every one of those
|
||
predicates — the #184 shape, arrived at from the other direction. The graphical
|
||
host's `ValidateParentProjection` checks `parent.HasPartArray`
|
||
(`EquippedChildRenderController.cs:902`) — the **parent**'s, not the child's —
|
||
so it is not the analogue.
|
||
|
||
**Correct behaviour:** either port the per-level guard (acdream has a
|
||
`HasPartArray` notion on the graphical record, but not on the canonical one, so
|
||
this may be a deliberate impossibility headless), **or** file a register row
|
||
stating that acdream's canonical record has no part-array concept, that the
|
||
guard is therefore not reproducible at the canonical layer, and what the
|
||
consequence is. Register rule 1 requires the row in the same commit. Right now
|
||
neither exists, and AP-142 — which is otherwise a careful, honest row — reads as
|
||
if the port were complete.
|
||
|
||
### R2 — MAJOR — D4's presentation half has zero effective coverage
|
||
|
||
**File:** `tests/AcDream.App.Tests/Rendering/EquippedChildProjectionWithdrawalTests.cs:334-374`
|
||
(assertions at `:368-373`), against
|
||
`src/AcDream.App/Rendering/EquippedChildRenderController.cs:403` vs `:409-415`.
|
||
|
||
The test's only presentation assertion is:
|
||
|
||
```csharp
|
||
Assert.Equal(newCell, child.WorldEntity!.ParentCellId!.Value);
|
||
```
|
||
|
||
`TickChild` sets that field at **line 403**:
|
||
|
||
```csharp
|
||
child.Entity.ParentCellId = parent.ParentCellId; // :403 — unconditional
|
||
...
|
||
if (TryResolveExactAttachment(child, out parent)
|
||
&& parent.ParentCellId is { } parentCellId) // :409
|
||
{
|
||
_liveEntities.RebucketEquippedChildPresentation(...); // :412 — the demoted call
|
||
}
|
||
```
|
||
|
||
The assertion is therefore satisfied whether or not
|
||
`RebucketEquippedChildPresentation` runs, returns `false`, or is deleted. Grep
|
||
confirms `RebucketEquippedChildPresentation` has **zero** references under
|
||
`tests/`. The spatial bucket (`_spatial.RebucketLiveEntity`), the visibility
|
||
resolution (`IsLiveEntityProjectionResident`), and
|
||
`PublishProjectionVisibilityChanged` — the things
|
||
`RebucketLiveEntityPresentationOnly` actually does — are unasserted.
|
||
|
||
Contract §6 test 10 required exactly this and required a sabotage check. The
|
||
test's own doc comment claims sabotage verification, but the sabotage described
|
||
("reverting TickChild to call the public `RebucketLiveEntity` again makes the
|
||
final assertion fail, because that call always bumps `SpatialAuthorityVersion`")
|
||
only exercises the **canonical** half. The presentation half — "left behind at a
|
||
landblock boundary", D4's named specific risk, and #184's layer — is unguarded.
|
||
|
||
**Correct behaviour:** assert the spatial index / visibility state moved (the
|
||
route-4a R1 lesson), and sabotage-verify by no-op'ing
|
||
`RebucketEquippedChildPresentation`.
|
||
|
||
### R3 — MAJOR — the demoted call's status is discarded, and its new guard can fail silently
|
||
|
||
**File:** `src/AcDream.App/Rendering/EquippedChildRenderController.cs:412-415`;
|
||
`src/AcDream.App/World/LiveEntityRuntime.cs:1085-1101`.
|
||
|
||
```csharp
|
||
internal bool RebucketEquippedChildPresentation(uint serverGuid, uint parentCellId)
|
||
{
|
||
if (!_directory.ParentAttachments.HasCommittedParent(serverGuid))
|
||
return false; // NEW failure mode
|
||
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
||
|| record.WorldEntity is not { } entity)
|
||
return false; // NEW failure mode
|
||
...
|
||
}
|
||
```
|
||
|
||
`TickChild` ignores the return and still returns `true` (`:417`). The old
|
||
`RebucketLiveEntity` had no committed-relation precondition, so both of these are
|
||
paths that previously could not exist. `EndChildProjection` /
|
||
`RemoveCommittedChild` run at several points while a projection is still
|
||
installed (`EquippedChildRenderController.cs:270`, `:1175`, `:1274`, `:1316`;
|
||
`RuntimeInitialCreateContinuationExecutor.cs:1998`, `:2189`), so a tick landing in
|
||
that window now silently stops moving the draw bucket, and the pose loop reports
|
||
success.
|
||
|
||
Contract D4 pinned "the entry point is child-scoped (**assert** the record has a
|
||
committed parent relation…)". A silent `false` swallowed by the caller is the
|
||
route-5 A1 class the contract's own predecessor list names: *"an App glue site
|
||
discarding the Runtime seam's status and advancing presentation on write-nothing
|
||
outcomes."*
|
||
|
||
**Correct behaviour:** either throw/assert on the guard (it is an invariant, not a
|
||
condition), or propagate the `false` into `TickChild`'s result so the existing
|
||
`WithdrawForPoseLoss` path handles it. Note this finding is only *observable*
|
||
today because of R2 — with a real bucket assertion, a fix for R3 would be
|
||
test-covered.
|
||
|
||
### R4 — MINOR — contract §6 test 2 shipped 2 of 4 writer families; P2/P4/P5/P6/P8 are unstated
|
||
|
||
**File:** `tests/AcDream.Runtime.Tests/Entities/RuntimeEntityChildCellPropagationTests.cs`.
|
||
|
||
Shipped: (a) `CommitRebucket` and (d) the wire merge (`RefreshSnapshot`).
|
||
**Missing:** (b) `RuntimePhysicsState.CommitCanonicalCell` (the simulation commit
|
||
— the writer that actually fires per tick in live play) and (c) a
|
||
`RuntimeSetPositionState` canonical placement commit. I verified by reading that
|
||
both reach `Entities.SetFullCell` (`RuntimePhysicsState.cs:2148`,
|
||
`RuntimeSetPositionState.cs:2743/:3660/:5003/:5224`), so the hook does fire — the
|
||
gap is regression coverage, not present-tense correctness. P5 (per-writer-family
|
||
re-entrancy) has no test at all; P6 (0 B/crossing) has none; P2 asserts only
|
||
`ObjectClock.IsActive` on one path, not "no workset / not a spatial root / body
|
||
inactive after N crossings and a teleport"; P4 (the child broadphase story) and
|
||
P8 (the `Rebucketed` consumer enumeration) are stated nowhere in the tree.
|
||
|
||
For P8 I did the enumeration myself: `RuntimeEntityChange.Rebucketed` has **no**
|
||
production consumer that branches on it (only `GameRuntimeEvents.cs:43`'s enum
|
||
member and two test assertions). D2's silent-publication default is safe. That
|
||
should be written into the commit, per §5, rather than left for a reviewer.
|
||
|
||
### R5 — MINOR — D7 is half-applied; the executor's replayed pickup keeps the inverted order
|
||
|
||
**File:** `src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs:2183-2189`.
|
||
|
||
```csharp
|
||
_entities.AdvancePositionAuthority(canonical);
|
||
_physics.CollisionReports.LeaveWorld(canonical); // leave_world
|
||
... SetPosition.Forget / SuspendObjectClock / SetFullCell(0,0)
|
||
_entities.ParentAttachments.EndChildProjection(canonical.ServerGuid); // unset_parent LAST
|
||
```
|
||
|
||
This is the dormant-residence replay of the **same** wire event
|
||
(`SmartBox::DoPickupEvent` @0x00452240), whose retail order is `unset_parent`
|
||
@0x0045227f **before** `leave_world` @0x00452286. D7 corrected
|
||
`RuntimeEntityObjectLifetime.TryApplyPickup` and left this one. Inert for the
|
||
same reason (§E), but the new comment at
|
||
`RuntimeEntityObjectLifetime.cs:1304-1306` — "This was inverted;
|
||
EndChildProjection now runs first" — now reads as a codebase-wide statement that
|
||
is only half true. (The `TryApplyPosition` unparent edge at `:1908` **is**
|
||
correctly scoped out as route 4's surface in contract §9; this one is not.)
|
||
|
||
### R6 — MINOR — D5's deferred-residence flavor is neither driven nor declared a non-goal
|
||
|
||
**File:** `src/AcDream.Runtime/Session/RuntimeLiveEntitySessionController.cs:339-379`.
|
||
|
||
`ResolveAndCommitChildAttachment` runs `Resolve` → `TryCommitParent` →
|
||
`CommitProjection` → `CommitAcceptedParentCellless` synchronously. When the
|
||
child has a pending initial residence, `Resolve`'s accept callback lands in
|
||
`RuntimeEntityObjectLifetime.TryApplyParent`'s dormant branch
|
||
(`:1354-1391`), which advances the gate via `TryAcceptDeferredParent` and
|
||
`EnqueueDormant`s a `Parent` continuation, returning `true` — so the relation is
|
||
**staged**, and the immediately following `TryCommitParent` is gated on
|
||
`gate.PositionTimestamp == positionSequence && child.PositionSequence ==
|
||
positionSequence` (`InboundPhysicsStateController.cs:308-314`), which the
|
||
deferred merge has not yet satisfied. The drive returns `false`.
|
||
|
||
Nothing re-drives it: the executor's `CommitParentAttachment` deliberately does
|
||
**not** commit the attach ("it is the App-layer `EquippedChildRenderController`'s
|
||
job" — `RuntimeInitialCreateContinuationExecutor.cs:2110-2123`), and headless has
|
||
no such controller and no post-drain retry hook. So headless, a ParentEvent
|
||
arriving during a pending initial residence leaves the child staged and
|
||
cell-less indefinitely.
|
||
|
||
Not a regression (headless committed nothing before), and the invariant-9
|
||
deferrals are correctly untouched. But the plan doc's new closure text and D5's
|
||
doc comment both read as if the headless gap is closed. Contract D5 required
|
||
"state what is deliberately not driven"; this case is not stated. **Flagging the
|
||
`TryCommitParent` rejection as inferred-from-reading, not executed** — no test
|
||
covers it.
|
||
|
||
### R7 — MINOR — AP-143 under-describes what the headless drive skips
|
||
|
||
**File:** `docs/architecture/retail-divergence-register.md:173`;
|
||
`src/AcDream.Runtime/Session/RuntimeLiveEntitySessionController.cs:339-379`.
|
||
|
||
AP-143 records only the `Setup.HoldingLocations` check. The graphical
|
||
`ValidateParentProjection` (`EquippedChildRenderController.cs:894-919`) also
|
||
rejects **self-parenting** (`:897-898`) and requires the parent to have a
|
||
**part array** (`:902` — retail's @0x00510ed8 guard, and the closest thing
|
||
acdream has to it). The headless drive performs neither. Self-parenting turns
|
||
out to be inert by construction (the D1 gate sees `parent.FullCellId == 0`
|
||
because the child was just zeroed, and D2's skip-on-equal terminates the
|
||
one-node cycle), but the row should say what it skips, not one of three things.
|
||
Contract §6 gave zero coverage of the headless rejection paths.
|
||
|
||
### R8 — MINOR — the headless drive is not the "same protocol" for nested/recovery cases
|
||
|
||
**File:** `src/AcDream.Runtime/Session/RuntimeLiveEntitySessionController.cs:381-393`.
|
||
|
||
- The graphical retry is `_relationRecoveryOrder.RealizeDescendants(parentGuid,
|
||
Relations.ChildrenWaitingForParent, ResolveAndTryRealize)`
|
||
(`EquippedChildRenderController.cs:925-931`) — a **transitive, parent-first**
|
||
descent. `RetryChildrenWaitingForParent` iterates the direct waiters on one
|
||
guid only, so after committing a child, grandchildren waiting on **that** child
|
||
are not retried in the same pass. The doc comment claims it "mirrors
|
||
`RetryWaitingDescendants`'s role"; it mirrors a subset.
|
||
- The drive never calls `MarkProjected`, so `_recoveryByChild` retains every
|
||
committed relation headless, and it never handles the recovery branch the
|
||
graphical `ResolveAndTryRealize` handles (`:835-844`). Benign today (recovery
|
||
is a re-projection concept that does not exist headless, and the entries are
|
||
cleared by `EndChildProjection`/`EndGeneration`/`Clear`), but undeclared.
|
||
|
||
Given AP-142 explicitly ships unbounded-depth recursion as a design commitment,
|
||
a depth-limited headless realize is worth a sentence somewhere.
|
||
|
||
### R9 — MINOR — a doc comment now describes the wrong test method
|
||
|
||
**File:** `tests/AcDream.Runtime.Tests/Session/RuntimeLiveEntitySessionControllerTests.cs:316-331`
|
||
and `:426-431`.
|
||
|
||
The pre-existing `<summary>` for
|
||
`FirstEntryDriveServesOneRouteAtATimeAndScopesClearToTheOwner` (the C3c-R1 F6
|
||
latch) was left in place and the two new tests were inserted **after** it, so:
|
||
|
||
- `DirectSink_D5_StandaloneParentEventCommitsChildToParentsExactCell` now carries
|
||
**two** `<summary>` elements, the first of which describes drive-controller
|
||
route latching.
|
||
- `FirstEntryDriveServesOneRouteAtATimeAndScopesClearToTheOwner` now carries the
|
||
D5 *deferred-parent* summary.
|
||
|
||
Exactly the endemic stale-comment class this campaign keeps hitting, in the
|
||
commit that was supposed to be checking for it.
|
||
|
||
### R10 — MINOR — AP-142 clause (b) overstates the equivalence
|
||
|
||
**File:** `docs/architecture/retail-divergence-register.md:172`;
|
||
`src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs:391-395`.
|
||
|
||
The skip guard (`child.FullCellId == fullCellId → continue`) does two things:
|
||
it subsumes retail's same-cell id refresh (accurate, as the row says) **and** it
|
||
prunes the child's entire subtree. Retail's `enter_cell` has no such pruning —
|
||
it recurses unconditionally (@0x00510f03) and only `leave_cell` prunes
|
||
(@0x00510f5b, on `cell != 0`). So acdream matches retail exactly on the removal
|
||
side and diverges on the entry side. Currently unreachable-by-construction (after
|
||
D4 nothing writes a grandchild's cell independently of its parent), but the row
|
||
claims a clean equivalence it does not have, and this is precisely the class of
|
||
"row asserting behaviour the code lacks" in the other direction.
|
||
|
||
### R11 — MINOR — the demotion drops `SynchronizePhysicsBodyActiveState` on a child's first projection
|
||
|
||
**File:** `src/AcDream.App/World/LiveEntityRuntime.cs:923-927` (legacy branch,
|
||
no longer reached for children) vs `:995-1067` (presentation-only branch).
|
||
|
||
The legacy `RebucketLiveEntity` ran
|
||
`record.SuspendObjectClock(); SynchronizePhysicsBodyActiveState(record);` when
|
||
`!wasProjected && !isOrdinaryRoot` — true on a child's first tick after realize.
|
||
`RebucketLiveEntityPresentationOnly` does neither. The canonical clock is
|
||
suspended by Runtime (`CommitAcceptedParentCellless`'s
|
||
`Entities.SuspendObjectClock`), and `SynchronizePhysicsBodyActiveState` no-ops
|
||
when `record.PhysicsBody is null` (the equipped-item case), so this is very
|
||
likely inert — but P2's "body (if any) inactive" is the obligation that would
|
||
have proven it, and it is unproven. **Flagged as unverified rather than
|
||
asserted.**
|
||
|
||
---
|
||
|
||
## Register discipline — assessed row by row
|
||
|
||
| Row | Assessment |
|
||
|---|---|
|
||
| **AP-142** (parented-child single-field cell model) | Clauses (a) and (c) describe shipped behaviour accurately (verified: `PropagateFullCellToChildren` + `WithdrawCommittedChildrenToCellless` + the D1 half). Clause (b) overstates — see R10. **Missing the `part_array` guard clause — see R1.** Anchors all correct. |
|
||
| **AP-143** (headless holding-location skip) | Accurately describes the skip and the reason. Under-describes the scope — see R7. The claim "repo-wide grep confirms nothing under `src/AcDream.Content`/`AcDream.Bake` carries `HoldingLocations`" is consistent with what I saw; the row correctly refuses to extend the bake format. |
|
||
| **AP-136** writer-list shrink | ✓ Correct: line 289 now reads "…demoted the third, `EquippedChildRenderController.TickChild`, to a presentation-only bucket move that no longer touches `record.FullCellId`". Matches the code. |
|
||
| **AP-138** writer-list shrink | ✓ Correct, same shape. |
|
||
| `RuntimeSetPositionState.cs:4536-4548` doc comment | ✓ Corrected; singular "writer" now, projection materializer only. Matches the code. |
|
||
| `RuntimeRemotePlacementDriveController.cs:1613-1626` doc comment | ✓ Corrected. Matches the code. |
|
||
| 4b-3 contract closure note | ✓ Honest and correct. It states plainly that the old `cause=cellless` recipe no longer fires, explains why that is the retail-faithful direction, marks test 4 as a synthetic fixture that must not be relabelled, and says the replacement provocation is UNESTABLISHED rather than inventing one. This is the best-written doc change in the diff. |
|
||
| Placement-cutover plan gap statement | ✓ Correctly moved to past tense and closed, with the one caveat that "the direct headless regression test … now passes" is true for the non-deferred flavor only (R6). |
|
||
| No row deletions | ✓ AP-124/131/132/133/135 untouched. |
|
||
|
||
---
|
||
|
||
## Contract defects found
|
||
|
||
1. **§0 item 1 dropped `enter_cell`'s `part_array != 0` guard** (@0x00510ed8),
|
||
which the research it cites called load-bearing. The contract enumerated the
|
||
five writes and not the gate around them; the implementation inherited the
|
||
omission. This is the root of R1 — process rule 1 ("the contract causes the
|
||
defect") in action.
|
||
2. **§2's `DoPickupEvent`/`DoParentEvent` gate citations are not readable from
|
||
the named-retail source** (BN lowered both to a literal always-false
|
||
expression). The contract presents them as verified. Nothing downstream
|
||
depends on it, but "✓" is the wrong mark.
|
||
3. **§4 D4 said "assert"; §6 test 10 said "assert the child's render entity
|
||
moved buckets".** The implementation did neither, and the contract's own
|
||
sabotage instruction ("break D2's propagation and confirm THIS test fails on
|
||
the canonical half **while presentation still moves** — if it stays green,
|
||
the test is asserting the wrong layer") was applied to the canonical half
|
||
only. The contract was right; it was not followed. R2/R3.
|
||
4. **§5's proof obligations are not stated anywhere in the working tree.** P4 and
|
||
P8 in particular were explicitly "must prove, not assume". P8's answer is
|
||
benign (I checked), P4's is unwritten.
|
||
|
||
## Propagation research defects found
|
||
|
||
Two, both cosmetic, listed in §A: the "verbatim" blocks are normalized
|
||
paraphrase, and §7's read/inferred ledger is otherwise scrupulous and correct.
|
||
**The verdict, the addresses, the recursion claim, the same-cell-fast-path
|
||
disambiguation, the `update_object` clincher, and the cross-cells trap are all
|
||
correct.** The research is the strongest artifact in this slice; the contract
|
||
weakened one of its findings on the way through (R1).
|
||
|
||
---
|
||
|
||
## What would flip this to PASS
|
||
|
||
- R1: a `part_array` register clause (or the guard, if a canonical analogue
|
||
exists).
|
||
- R2: an App test that asserts the spatial bucket / visibility state moved, with
|
||
the demoted call sabotage-verified.
|
||
- R3: assert-or-propagate on `RebucketEquippedChildPresentation`'s two guards.
|
||
|
||
R4–R11 are worth folding in but none of them alone blocks the slice.
|