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>
29 KiB
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:
- R1 (MAJOR) — retail's
enter_cellgates the ENTIRE cell write and the recursion onthis->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 enumeratedenter_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. - 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.
RebucketEquippedChildPresentationhas zero references intests/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. - R3 (MAJOR) —
TickChilddiscards the demoted call'sbool, and the newHasCommittedParentguard introduces a silent-false path the oldRebucketLiveEntitydid not have. Contract D4 said "assert the record has a committed parent relation"; the code returnsfalseand 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 readsif ((*(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/DoParentEventgates are NOT readable from this source. Binary Ninja lowered both toif (-((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 againstupdate_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 inInboundPhysicsStateController, which I verified atInboundPhysicsStateController.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) andRuntimeEntityRecord.RefreshDerivedState(:230-242).RefreshDerivedState— reached from the record constructor (:29, no children possible) andRuntimeEntityDirectory.RefreshSnapshot(:234-247, now hooked with an explicitpreviousCellcompare).LiveEntityRecord.FullCellId/.CanonicalLandblockIdsetters (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) callEntities.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.RemoveCommittedParentReferenceswould, and is not called.- The only work between the two positions is
CollisionReports.LeaveWorld→SetPosition.Forget→SuspendObjectClock→SetFullCell(0,0). I checked everyChildrenAttachedToParentconsumer (RuntimeSetPositionState.cs:6015-6063,ArmLostFamilyDeadlines/CancelLostFamilyDeadlines): both enumerateoperation.Record's own children, i.e. the picked-up entity's subtree, whichEndChildProjectiondoes 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:
Assert.Equal(newCell, child.WorldEntity!.ParentCellId!.Value);
TickChild sets that field at line 403:
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.
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.
_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
EnqueueDormants 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.RetryChildrenWaitingForParentiterates 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 "mirrorsRetryWaitingDescendants's role"; it mirrors a subset. - The drive never calls
MarkProjected, so_recoveryByChildretains every committed relation headless, and it never handles the recovery branch the graphicalResolveAndTryRealizehandles (:835-844). Benign today (recovery is a re-projection concept that does not exist headless, and the entries are cleared byEndChildProjection/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_StandaloneParentEventCommitsChildToParentsExactCellnow carries two<summary>elements, the first of which describes drive-controller route latching.FirstEntryDriveServesOneRouteAtATimeAndScopesClearToTheOwnernow 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
- §0 item 1 dropped
enter_cell'spart_array != 0guard (@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's
DoPickupEvent/DoParentEventgate 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. - §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.
- §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_arrayregister 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.