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>
418 lines
24 KiB
Markdown
418 lines
24 KiB
Markdown
# C4 route 7 — ARCHITECTURE / ADVERSARIAL review, round 2 (DELTA) — 2026-08-04
|
||
|
||
**Verdict: PASS.**
|
||
|
||
All three round-1 blocking findings (A1, A2, A3) are properly remediated, and
|
||
each remediation is sabotage-sensitive in the direction that matters. Seven new
|
||
findings (B1–B7) are recorded below; none is a correctness defect in shipped
|
||
behaviour with a demonstrated failure, and each has a one-to-three-line fix.
|
||
**B1 and B3's logging clause are recommended before the connected gate**, but
|
||
neither blocks.
|
||
|
||
Scope: delta only. Round 1's accepted conclusions — the `SetFullCell` blast-radius
|
||
enumeration, the D2 chokepoint-completeness proof, and the D7 inertness
|
||
verification — are not re-litigated. Round-1 report:
|
||
[`2026-08-04-c4-route-7-architecture-review.md`](2026-08-04-c4-route-7-architecture-review.md).
|
||
|
||
Gates re-run:
|
||
|
||
- `dotnet build` Runtime.Tests + App.Tests — 0 warnings, 0 errors.
|
||
- `dotnet test AcDream.Runtime.Tests` — **1,157 passed / 0 failed** (was 1,156;
|
||
+1 = the depth-cap test).
|
||
- `dotnet test AcDream.App.Tests --filter EquippedChild` — **37 passed / 0
|
||
failed** (was 36; the round-1 D4 test was replaced by two).
|
||
|
||
---
|
||
|
||
## 1. Round-1 blockers — re-verified
|
||
|
||
### A1 (MAJOR, "the D4 test cannot fail") — **FIXED, and the fix is sound.**
|
||
|
||
`tests/AcDream.App.Tests/Rendering/EquippedChildProjectionWithdrawalTests.cs:321-393`
|
||
(`TickChild_D4_PresentationBucketMovesToTheDestinationLandblock`).
|
||
|
||
**Is it measuring the draw bucket or a proxy?** The draw bucket.
|
||
`fixture.Spatial` is a real `GpuWorldState` (`:1443`, `= new()`), and
|
||
`CopyLiveEntitiesNearLandblock` (`src/AcDream.App/Streaming/GpuWorldState.cs:493-523`)
|
||
reads `_loadedLiveByLandblock` — the per-landblock `HashSet<WorldEntity>` the
|
||
renderer's live-entity publication actually maintains. It is not a mirror field
|
||
and not a derived view. It also `destination.Clear()`s at `:500`, so the
|
||
four sequential queries in the test do not contaminate each other (without that
|
||
clear, the `DoesNotContain` assertions would be false-negatives; with it, they
|
||
are real).
|
||
|
||
**Can it pass with the demotion removed?** No, and I checked both sabotage
|
||
directions:
|
||
|
||
- *Delete the `RebucketEquippedChildPresentation` call at
|
||
`EquippedChildRenderController.cs:411-425`* → the child never leaves
|
||
`oldLandblock`, so the post-tick `Assert.Contains(newLandblock…)` fails. This
|
||
is the failure the round-1 test could not produce.
|
||
- *Revert to the legacy `RebucketLiveEntity`* → the bucket moves (so the
|
||
`Contains` passes) but `CommitRebucket` → `RuntimeEntityRecord.SetFullCell`
|
||
bumps `SpatialAuthorityVersion` unconditionally (`RuntimeEntityRecord.cs:246-251`),
|
||
so the final `Assert.Equal(childSpatialVersionBeforeTick, …)` fails.
|
||
|
||
Both halves of the transfer are therefore pinned in opposite directions. The
|
||
test also cannot pass **vacuously** on an empty query result: the pre-tick
|
||
`Assert.Contains(oldLandblock…)` and the post-tick `Assert.Contains(newLandblock…)`
|
||
are positive assertions on two different landblocks, and a
|
||
`!_availability.IsWorldAvailable` early-return (`GpuWorldState.cs:501`) would
|
||
fail the first one. The implementer's reported sabotage ("stubbing the demoted
|
||
call to claim success failed it — empty collection") is consistent with that
|
||
structure.
|
||
|
||
### A2 (MAJOR, "silent write-nothing outcome") — **FIXED. The fork is the right call; `NotAttached`-as-benign is safe.**
|
||
|
||
I judged the fork against the alternative I originally suggested, and the
|
||
implementer's choice is better. Reasoning, done by enumeration rather than by
|
||
accepting the stated rationale:
|
||
|
||
`NotAttached` is returned only when `!HasCommittedParent(serverGuid)` or when an
|
||
initial-create residence is active. The complete set of things that can remove
|
||
`_lastAcceptedByChild[child]` is `ParentAttachmentState.RemoveCommittedChild`,
|
||
reached from exactly six call sites:
|
||
|
||
| remover | is it an unwind edge? |
|
||
|---|---|
|
||
| `CommitProjection` (`:581`) | re-adds on the next line — transient, unobservable |
|
||
| `EndChildProjection` (`:753`) | yes — pickup / Position-unparent |
|
||
| `EndGeneration(child)` (`:709`) | yes — child replaced |
|
||
| `DeleteGeneration(child)` (`:734`) | yes — child deleted |
|
||
| `RemoveCommittedParentReferences(parent)` (`:865-873`, from `EndGeneration`/`DeleteGeneration` on the PARENT) | yes — parent deleted/replaced |
|
||
| `RemoveObject` / `RemoveChild` (`:660`, `:756`) | zero production callers |
|
||
|
||
`CommitProjection` is also the **only** writer, and
|
||
`PrepareAndTryRealize` always calls it before `TryRealize` installs the
|
||
`AttachedChild` (`EquippedChildRenderController.cs:861-891`), with the recovery
|
||
branch gated on `Relations.IsCommitted`. So `_attachedByChild` non-empty ⇒
|
||
committed, **unless one of the five genuine unwind edges has fired**. There is
|
||
no state in which a child is legitimately attached, rendering, and
|
||
`HasCommittedParent` is false. `NotAttached` therefore cannot swallow a real
|
||
pose-loss — it can only fire in a window where a teardown is already in flight
|
||
and owns the child's fate.
|
||
|
||
The parent-deleted case is worth naming because it is the longest window
|
||
(`_pendingOrphanRemovalByChild` defers the withdrawal across frames,
|
||
`EquippedChildRenderController.cs:342`/`:584`): during it, D3 has already zeroed
|
||
the child's canonical cell and the bucket is frozen at the parent's last
|
||
landblock. That is unchanged from pre-route-7 behaviour (the old
|
||
`RebucketLiveEntity` would have rebucketed to the same stale
|
||
`parent.ParentCellId`), so it is not a regression.
|
||
|
||
Routing `NoProjection` into `WithdrawForPoseLoss` is also correct for the
|
||
`_projections.TryGetCurrent` failure it is named for — see **B6** for the second
|
||
condition that shares the label.
|
||
|
||
### A3 (MEDIUM, "P8 enumerated the wrong interface") — **FIXED.**
|
||
|
||
`RuntimeEntityDirectory.cs`'s `PropagateFullCellToChildren` doc now carries a
|
||
dedicated `<para>` naming `GameRuntimeEventHub` as itself an
|
||
`IRuntimeEntityObjectObserver` that fans out to `IRuntimeEventObserver`, and
|
||
`RuntimeTraceRecorder.OnEntity` as the real non-stub consumer, and explicitly
|
||
retracts "no consumer at all". The decision (publish nothing) is unchanged and
|
||
its stated basis is now true. P4's write-up in the same comment also correctly
|
||
states the `ProjectionKind is World` reason for the no-spatial-root claim rather
|
||
than the round-1 relation-based reason.
|
||
|
||
### A4, A5, A7, A9, A10 — spot-checked, all correct.
|
||
|
||
- **A4** — `RuntimeEntityObjectLifetime.CommitAcceptedParentCellless` carries the
|
||
payload-contract doc, and `Attach_ParentCelled_…` now subscribes a
|
||
`RecordingEntityObserver` and asserts the `Withdrawn` delta's
|
||
`Entity.CellId == parent.FullCellId` and `!= 0`
|
||
(`RuntimeEntityChildCellPropagationTests.cs:34-57`). Sabotage-sensitive:
|
||
moving D1's re-cell after the publish makes `withdrawn.Entity.CellId` zero and
|
||
fails two assertions. This is a real pin, not a source-text pin.
|
||
- **A5** — `RuntimeInitialCreateContinuationExecutor.cs:2184-2190`: the dormant
|
||
pickup replay now runs `EndChildProjection` before `LeaveWorld`, matching
|
||
`TryApplyPickup`. Both pickup paths are one shape.
|
||
- **A7** — the skip is now keyed on the `(FullCellId, CanonicalLandblockId)`
|
||
pair (`RuntimeEntityDirectory.cs:389-395`), with the reason stated. Cycle
|
||
termination is unaffected: A→B→A still writes A, writes B, then finds A's
|
||
**pair** already equal and stops (`PropagationChokepoint_TerminatesAHostileTwoCycle…`
|
||
still green).
|
||
- **A9** — `RebucketEquippedChildPresentation` now carries the same
|
||
`MaterializationResidence is AwaitRuntimePlacement && HasActiveInitialCreateResidence`
|
||
gate `RebucketLiveEntity` honours (`LiveEntityRuntime.cs:1140-1145`).
|
||
- **A10** — the C3c-R1 F6 summary is back on
|
||
`FirstEntryDriveServesOneRouteAtATimeAndScopesClearToTheOwner`
|
||
(`RuntimeLiveEntitySessionControllerTests.cs:422-430`); no duplicate
|
||
`<summary>` remains.
|
||
|
||
---
|
||
|
||
## 2. New findings
|
||
|
||
### B1 — MEDIUM. The `NotAttached` test's "the draw bucket did NOT move" assertion is non-discriminating.
|
||
|
||
**Where:** `tests/AcDream.App.Tests/Rendering/EquippedChildProjectionWithdrawalTests.cs:395-450`
|
||
(`TickChild_D4_NotAttachedDisposition_SkipsTheBucketMoveWithoutFailingTheTick`),
|
||
final assertion at `:445-448`.
|
||
|
||
Unlike its sibling, this test never moves the parent: both entities are spawned
|
||
at the fixture's default `Cell` and no `CommitRebucket` runs. So
|
||
`parent.WorldEntity.ParentCellId` still names the **same landblock the child's
|
||
bucket is already in** (`0x0101FFFF`). If the `HasCommittedParent` guard were
|
||
deleted outright, `RebucketLiveEntityPresentationOnly` would run and rebucket
|
||
the child to the landblock it is already in — and
|
||
`Assert.Contains(buffer, kv => kv.Value == child.WorldEntity)` on `oldLandblock`
|
||
would still pass.
|
||
|
||
**Concrete failure scenario:** delete the `HasCommittedParent` check at
|
||
`LiveEntityRuntime.cs:1137-1138`. The guard A2 exists to establish is gone; the
|
||
enum collapses to `Moved`/`NoProjection`; this test stays green.
|
||
|
||
The test's **load-bearing half is sound** — `LastFullPoseCompositionVisits` +
|
||
`AttachedEntityIds` genuinely pin "the tick was not torn down as a pose loss",
|
||
which is the fork judgement A2 asked for, and *that* half does fail if
|
||
`NotAttached` were routed to `return false`. Only the bucket clause is
|
||
decorative.
|
||
|
||
**Fix direction:** mirror the sibling — register the second landblock, drive
|
||
`CommitRebucket(parent, newCell, newLandblock)` before the tick, and assert the
|
||
child is **still in `oldLandblock`** afterwards. Then the assertion discriminates.
|
||
|
||
### B2 — MEDIUM. A6's justification comment contradicts the R6 gap comment two methods away, and the narrowing removed a (weak) recovery path.
|
||
|
||
**Where:** `src/AcDream.Runtime/Session/RuntimeLiveEntitySessionController.cs:414-433`
|
||
(`RetryChildrenWaitingForParent`'s A6 `<para>`) vs `:346-370`
|
||
(`ResolveAndCommitChildAttachment`'s R6 KNOWN-GAP `<para>`).
|
||
|
||
The A6 comment justifies scanning only `_unresolvedByChild` on the grounds that
|
||
"this drive never populates `_stagedByChild`/`_recoveryByChild` through any path
|
||
those two sweeps exist to catch". That is false: `ParentAttachmentState.Resolve`
|
||
sets `_stagedByChild[childGuid]` on accept (`:472-475`), and the R6 gap
|
||
documented two methods above is *precisely* a relation left staged-but-uncommitted
|
||
because `TryCommitParent`'s POSITION_TS gate was not satisfied during the child's
|
||
pending initial-create residence.
|
||
|
||
**Concrete failure scenario:** a headless `ParentEvent` arrives while the child's
|
||
initial residence is pending. `Resolve` stages it; `TryCommitParent` refuses; the
|
||
relation sits in `_stagedByChild` forever. Under round 1's
|
||
`ChildrenWaitingForParent` (which unioned all three tables) a later spawn naming
|
||
the same parent guid would have re-driven it; under `ChildrenUnresolvedForParent`
|
||
nothing will. Weak in practice — it needs a *new parent generation* to fire — but
|
||
the narrowing strictly reduced recovery, and the comment claims it did not.
|
||
|
||
The R6 gap itself is disclosed honestly and I accept it as an out-of-scope
|
||
residual (headless committed nothing on this path before D5 existed). The
|
||
finding is the contradiction, plus the consequence for the **contract §7 headless
|
||
gate**: its scenario is "the local player equips via the bot command surface and
|
||
crosses a boundary". If the equip lands during the item's own residence, the gate
|
||
produces no `cause=headless-attach` line and must be read as a **not-run**, not a
|
||
pass — the same rule the contract already states for `cause=propagate`.
|
||
|
||
**Fix direction:** correct the A6 `<para>` to say what is actually true (this
|
||
drive promotes unresolved relations; a staged-but-uncommitted relation is the R6
|
||
gap and is deliberately not retried here), and cross-reference the R6 paragraph.
|
||
No code change required.
|
||
|
||
### B3 — MEDIUM. The depth cap's failure mode contradicts AP-142 clause (a), is unrecoverable, and is invisible without the probe flag.
|
||
|
||
**Where:** `src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs:349-357`
|
||
(the constant) and `:433-441` (the early return).
|
||
|
||
Three separate points, in descending importance:
|
||
|
||
1. **The failure mode is the shape the model exists to avoid.** Past depth 64
|
||
the tail keeps a **stale non-zero** `FullCellId`. Under acdream's single-field
|
||
model that reads as "still resident at the old cell" at all 45+ predicate
|
||
sites — exactly the `#184` invisible-but-solid shape that AP-142 **clause (a)**
|
||
cites as the reason the removal path propagates zero rather than reproducing
|
||
retail's stale-`objcell_id` residue. The cap therefore reintroduces, as its
|
||
*chosen* failure mode, the residue the row rejects. Zeroing the truncated
|
||
subtree instead ("we cannot maintain this, so it is not resident") is both
|
||
consistent with the model and strictly safer.
|
||
2. **It is unrecoverable.** Every subsequent crossing truncates at the same
|
||
depth, so a subtree that falls past the cap once never re-syncs. And because
|
||
the skip is value-idempotent, a later same-value write prunes even earlier
|
||
(the asymmetry AP-142 clause (b)'s R10 correction already records). A
|
||
permanently-stale resident subtree is worse than a transiently-stale one.
|
||
3. **It is silent in production.** `[child-cell-depth-exceeded]` is emitted only
|
||
under `PhysicsDiagnostics.ProbeChildCellEnabled`, i.e. only when someone has
|
||
already set `ACDREAM_PROBE_CHILD_CELL=1`. A defensive guard whose premise is
|
||
"this must never happen" should be unconditionally observable — a one-shot
|
||
log or a counter on the ownership ledger — or nobody will ever learn it fired.
|
||
|
||
**Is 64 defensible?** Yes, as a number. Real chains are 2–3 deep
|
||
(player → weapon; quiver → arrow). 64 is ~20× any plausible depth and is well
|
||
inside a 1 MB stack for a two-frame-per-level recursion. **Is the cap the right
|
||
mechanism?** An iterative worklist over a pre-sized scratch would remove the
|
||
hazard entirely, keep 0 B after warmup, and need no cap, no divergence row, and
|
||
no failure mode — that is the answer I would take if this is revisited. But the
|
||
cap is a strict improvement over round 1's uncatchable process kill, so it is not
|
||
a blocker.
|
||
|
||
**Fix direction (cheapest first):** (i) make the past-cap log unconditional;
|
||
(ii) zero the truncated subtree rather than leaving it stale, and amend AP-142
|
||
clause (e) to say so; (iii) if revisited, replace the recursion with an
|
||
iterative worklist and retire clause (e).
|
||
|
||
### B4 — LOW. The reused scratch buffer is not re-entrancy-safe; round 1's fresh array was.
|
||
|
||
**Where:** `src/AcDream.Runtime/Session/RuntimeLiveEntitySessionController.cs:73`
|
||
(`_unresolvedChildrenScratch`), `:434-440` (filled, then iterated while calling
|
||
`ResolveAndCommitChildAttachment`).
|
||
|
||
`ChildrenUnresolvedForParent` **clears and refills the caller's buffer**
|
||
(`ParentAttachmentState.cs:657-660`). `RetryChildrenWaitingForParent` then
|
||
iterates that same instance while calling into
|
||
`ResolveAndCommitChildAttachment` → `TryCommitParent` /
|
||
`CommitAcceptedParentCellless` → `AcknowledgeProjectionAndPublish` → synchronous
|
||
observer fan-out through `GameRuntimeEventHub`.
|
||
|
||
**Concrete failure scenario:** an `IRuntimeEventObserver` synchronously feeds a
|
||
spawn back into the sink → nested `OnSpawned` → nested
|
||
`RetryChildrenWaitingForParent` → the shared buffer is cleared and refilled →
|
||
the outer loop's `waiting.Count` and indices now address the inner call's
|
||
contents, so children are skipped or retried twice. Requires an observer that
|
||
re-enters the sink; the shipped headless policies are empty stubs, so this is
|
||
latent, not live. The graphical sibling it mirrors
|
||
(`ChildrenWaitingForParent` → `.ToArray()`) is re-entrancy-safe by allocation,
|
||
and round 1's version inherited that safety; the A6 optimization traded it away
|
||
without a guard.
|
||
|
||
**Fix direction:** a `_retryInProgress` flag that falls back to a fresh `List`
|
||
when re-entered, or state the single-entry precondition in the doc and assert it.
|
||
|
||
### B5 — LOW. The recursion no longer routes children through the public `SetFullCell`, creating a silent divergence trap.
|
||
|
||
**Where:** `src/AcDream.Runtime/Entities/RuntimeEntityDirectory.cs:409-414` —
|
||
propagation now calls `child.SetFullCell(...)` (the **record** method) plus a
|
||
direct recursive call, where round 1 re-entered `RuntimeEntityDirectory.SetFullCell`.
|
||
|
||
The two are equivalent **today** — the public method is exactly
|
||
`EnsureKnown` + `record.SetFullCell` + propagate, and `EnsureKnown` is a
|
||
validation-only throw (`:812-820`) already subsumed by the preceding
|
||
`TryGetActive`. The change was needed to thread `depth`.
|
||
|
||
**Concrete failure scenario:** anyone later adding a side effect to
|
||
`RuntimeEntityDirectory.SetFullCell` — an index update, a delta publish, a
|
||
telemetry counter — gets it for the root and **silently not** for propagated
|
||
children, reintroducing exactly the "mapping written against one caller's
|
||
reachable set" class D2 was designed to eliminate. Nothing in the code says the
|
||
equivalence is load-bearing.
|
||
|
||
**Fix direction:** give the public method a private `depth`-carrying overload and
|
||
have the recursion call *that*, so there is one body; or add one comment line at
|
||
`:409` naming the equivalence as an invariant to maintain.
|
||
|
||
### B6 — LOW. `NoProjection` conflates two different outcomes, and one of them now triggers a teardown that round 1 did not.
|
||
|
||
**Where:** `src/AcDream.App/World/LiveEntityRuntime.cs:1131-1152`. The enum doc
|
||
(`:45-56`) defines `NoProjection` as "the graphical projection itself is gone",
|
||
but `:1147-1152` also returns it when `RebucketLiveEntityPresentationOnly`
|
||
returns `false` — which means the **projection operation was displaced by a
|
||
re-entrant callback** (`IsCurrentProjectionOperation` at `:1029`/`:1054`), i.e. a
|
||
*newer* rebucket took over. `TickChild` then returns `false` →
|
||
`Tick()`'s `failed` list → `WithdrawForPoseLoss`.
|
||
|
||
**Concrete failure scenario:** a re-entrant observer starts a newer rebucket for
|
||
the same child mid-`_spatial.RebucketLiveEntity`; the older tick concludes
|
||
"projection gone" and withdraws the child's projection that the newer operation
|
||
just legitimately re-established. Low reachability — the
|
||
`_presentationOnlySpatialMutationDepth` guard (`:3354-3360`) already suppresses
|
||
the most likely re-entrant rebucket source — but round 1 discarded the bool, so
|
||
this teardown is new behaviour introduced by the A2 fix.
|
||
|
||
**Fix direction:** either split the displaced case into its own disposition
|
||
(`Superseded`, treated as benign — the newer operation owns the bucket), or
|
||
document at `:1147` that displacement is deliberately treated as pose loss and
|
||
why.
|
||
|
||
### B7 — INFO. One other test derives a built-collection size from a live production constant.
|
||
|
||
Asked directly, so answered directly: I swept `tests/` for the shape that caused
|
||
the implementer's 64,000-node stack overflow (a test that both *reads* a
|
||
production constant and *sizes work* by it). Exactly one other match with real
|
||
blast radius:
|
||
|
||
`tests/AcDream.Core.Tests/World/LandblockLoaderTests.cs:210` —
|
||
`for (uint i = 0; i <= LandblockStaticEntityIdAllocator.MaxCounter + 1u; i++)`
|
||
builds one `Stab` per iteration. `MaxCounter = 0xFFF`
|
||
(`src/AcDream.Core/World/LandblockStaticEntityIdAllocator.cs:16`), so 4,097 cheap
|
||
allocations today. A sabotage raising it by orders of magnitude would OOM or hang
|
||
rather than stack-overflow, and the test's `Assert.Contains("4096-entry", …)`
|
||
literal pins the constant — but only *after* the build loop, so the pin does not
|
||
protect the loop. Milder than the route-7 case and pre-existing; noted, not
|
||
filed against this slice.
|
||
|
||
The other three matches are benign: `LightManagerTests` (`MaxGlobalLights = 128`,
|
||
+50 iterations of trivial work) and the two `CapacityTrimIdleFrames` frame loops.
|
||
|
||
The route-7 depth test's own remaining constant read
|
||
(`RuntimeEntityChildCellPropagationTests.cs:473`,
|
||
`for (i = 0; i <= MaxPropagationDepth; i++)`) is **correctly protected**: the
|
||
literal pin `Assert.Equal(64, RuntimeEntityDirectory.MaxPropagationDepth)` at
|
||
`:441` fires first, and the chain length at `:442` is a literal. The boundary is
|
||
pinned in both directions — a cap of 70 would give index 69 the new cell and fail
|
||
the tail assertion; a cap of 60 would leave indices 61–64 stale and fail the loop.
|
||
|
||
---
|
||
|
||
## 3. Direct answers to the coordinator's remaining questions
|
||
|
||
### The `WarmedSteadyContactRefreshDoesNotAllocate` flake — **#302 class. Not this slice.**
|
||
|
||
Stated definitely rather than hedged, because it is provable by reachability:
|
||
|
||
- The measured window
|
||
(`RuntimeCollisionReportingStateTests.cs:2375-2379`) contains **only**
|
||
`lifetime.Physics.HandleSetPositionCollisions(...)`. Entity registration and
|
||
shadow setup happen before `GC.GetAllocatedBytesForCurrentThread()` is sampled.
|
||
- `src/AcDream.Runtime/Physics/RuntimeCollisionReportingState.cs` contains **zero**
|
||
occurrences of `SetFullCell` and **zero** of `ParentAttachments` (grepped). Its
|
||
only `_entities` interactions are read-only queries (`TryGetByLocalId`,
|
||
`IsCurrent`, `SessionLifetimeVersion`, `CurrentLifetimeMutation`) plus
|
||
`StopMissileAfterCollision`, which mutates `FinalPhysicsState`, not the cell.
|
||
`StopMissileAfterCollision` is not in the complete `SetFullCell` caller set
|
||
enumerated in round 1 §0 and re-verified against this diff.
|
||
|
||
So `PropagateFullCellToChildren` is not reachable from the measured call at all,
|
||
and the fixture has no committed parent relations in any case. A
|
||
`GC.GetAllocatedBytesForCurrentThread() == 0` assertion that passes in isolation
|
||
and on re-run but fails once under full-suite load is the documented **#302**
|
||
shape (tiered-JIT / first-touch allocation on the measuring thread), and should
|
||
be treated as such: re-run and name it, never chase it. The instinct to look
|
||
rather than auto-dismiss was right — the answer is that the new code is not on
|
||
that path.
|
||
|
||
### Point 6 — deliberately-not-done items: **my "not blocking alone" assessment holds.**
|
||
|
||
- **Writer-family coverage for `CommitCanonicalCell` / `RuntimeSetPositionState`.**
|
||
Still covered *by construction*, and round 2 did not weaken that: both funnel
|
||
through the identical `RuntimeEntityDirectory.SetFullCell:359-366` line that
|
||
`CommitRebucket` (tested) and `RefreshSnapshot` (tested) use, and round 1's
|
||
chokepoint-completeness proof (`RefreshDerivedState` has exactly two callers;
|
||
`RuntimeEntityRecord.SetFullCell` exactly two) is unchanged by this diff. Two
|
||
of the four writer families are tested end-to-end; the other two are the same
|
||
three lines of code.
|
||
- **Fuller P2 across N crossings** — `NeverArmPartition_D8_…` already drives five
|
||
crossings; the clock/suspension assertions live in the attach and pickup tests.
|
||
Thin, not absent.
|
||
- **Dedicated P5/P6 tests** — still argued by construction, and the construction
|
||
argument is still valid after round 2: `PropagateFullCellToChildren` remains
|
||
field-writes + one dictionary probe + `TryGetActive`, with no callback, no
|
||
LINQ, no closure, and `ChildrenAttachedToParent` still returning the stored
|
||
`List<uint>` or the cached `Array.Empty<uint>()`. The round-2 restructuring
|
||
(direct `child.SetFullCell` instead of re-entering the directory method) makes
|
||
the path *cheaper*, not richer. **B4** is the one place round 2 introduced new
|
||
re-entrancy surface, and it is in D5's retry, not the propagation.
|
||
|
||
---
|
||
|
||
## 4. Recommended before the connected gate (non-blocking)
|
||
|
||
1. **B1** — make the `NotAttached` test's bucket assertion discriminate (move the
|
||
parent first). Two lines.
|
||
2. **B3 clause 3** — make the past-cap log unconditional. One line.
|
||
3. **B2** — correct the A6 justification comment so it does not contradict the R6
|
||
gap paragraph, and carry the "no `cause=headless-attach` line ⇒ not-run"
|
||
reading into the headless gate step. Comment only.
|
||
|
||
The **connected two-client gate remains unrun** and is still the acceptance test
|
||
for the D4 transfer, with the contract's rule intact: a session showing zero
|
||
`cause=propagate` lines during the landblock-crossing step is a not-run, not a
|
||
pass. The headless gate now carries the same caveat for `cause=headless-attach`
|
||
(B2).
|