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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

418 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

# C4 route 7 — 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 (B1B7) 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 23 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 6164 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).