acdream/docs/research/2026-08-04-c4-route-4b-3-architecture-review-round2.md
Erik 8c269ad1a6 docs: C4 route 4b-3 contract and both dual-review rounds
The pinned contract plus the four review documents behind 6dc7ba51:
round 1 (retail FAIL 3 MAJOR / architecture FAIL 2 MAJOR) and round 2
(both PASS on the fixed diff).

Process findings worth carrying into the remaining routes:

1. The contract's 13 explicit "what must REMAIN true" invariants were the
   fix for 4b-2's round-1 defect (a contract that said what must change but
   not what must stay). They did NOT prevent a round-1 FAIL here. What the
   round-1 MAJORs actually shared was a STRUCTURAL cause the invariant list
   could not express: two parallel inline copies of the same routing tail.
   An invariant list constrains behaviour; it cannot see duplication.

2. Both reviews independently found the same NPC synth-velocity defect
   (retail R3 = architecture A2). Independent convergence on one finding is
   the strongest signal this process produces — weight it accordingly.

3. The specified two-client gate could not have observed three of the four
   MAJORs: it teleported a player character, and all three live on the NPC
   arm. Caught by both reviewers before the gate ran, not after. Check that
   a gate can structurally see the defect class it is gating.

4. A reviewer named a symbol that does not exist (RuntimeCollisionReportingState
   .ForceEnd is a private helper, not the public entry point). The implementer
   silently substituted the correct one (LeaveWorld) while reporting "no
   disagreements with either review". Verify implementer claims against source
   even when they report full agreement.

5. A new test asserted only what must NOT happen, so an emptied
   ApplyWireAirborneLeftoverBookkeeping passed every test in the tree. Caught
   in round 2 and closed with positive assertions, sabotage-verified. Negative
   assertions alone cannot detect a deleted write.

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

17 KiB

C4 route 4b-3 — architecture / adversarial DELTA review, round 2 (2026-08-04)

Reviewer lens: adversary. Round 1's findings are 2026-08-04-c4-route-4b-3-architecture-review.md; the retail lane's are 2026-08-04-c4-route-4b-3-retail-review.md.

Subject: the same uncommitted working tree on claude/acdream-physics-divergence-5aa784 at base 3e002993, after the round-1 fix pass. This review re-attacks the fixes, not the whole slice — the round-1 "verified sound" list is carried forward except where a fix touched it.

Verdict: PASS

Both round-1 MAJORs (A1, A2) are fixed, and I verified each fix produces the retail-correct outcome rather than merely silencing the symptom. The retail lane's R1/R2/R3 are fixed. The RunRemoteArmTail / ApplyWireAirborneLeftoverBookkeeping extraction — the thing most likely to introduce a new defect — is behaviour-identical at all five call sites; I walked each one against the pre-fix code and found no reordering, no dropped step, and no widened or narrowed guard. Six MINORs below, none blocking.

Build dotnet build AcDream.slnx -c Release: succeeded, 0 warnings. Focused suites at Release --no-build: Runtime 1,125 / 0 skips, App 4,081 / 3 skips (up 3 from round 1's 4,078 — the three new App tests).


Part 1 — the extraction, call site by call site

The blast radius of the fix pass is small and I confirmed it: only LiveEntityNetworkUpdateController.cs, one new method on LiveEntityRuntime.cs, the App teleport test file, and three docs carry round-2 markers.

RunRemoteArmTail — 3 call sites

Signature returns RemoteContactRouting?; null means "the currency guard tripped, write nothing further". The guard inside it is (Arm is FarSnapPlacement or TeleportPlacement) && (!isCurrentPositionOwner() || !ReferenceEquals(positionRecord.RemoteMotionRuntime, remote)).

site pre-fix sequence post-fix verdict
player teleport dispatch (:2333) routing → unconditional guard → return routing → arm-gated guard → return identical. ApplyRemoteContactRouting returns TeleportPlacement iff OwnsTeleportPlacement(route), which the caller has already evaluated on the same earlyRemoteRoute with the same pure static predicate. The arm at this site is provably always TeleportPlacement, so arm-gating the guard cannot weaken it.
player grounded routing (:2566) routing → arm-gated guard → return same, via helper identical, including the R5 guard-before-arm order (TryArmConstraintAfterOperation is still the first statement after the null check, :2596).
NPC dispatch (:2818) routing → arm-gated guard → return from OnPosition routing → helper returns nullif (npcRoutingResult is null) return; inside the same if (!snapSuppressedByStick || isTeleportRoute) block identical, same lexical position, same effect.

Specific things I checked for and did not find:

  • willBeDrTicked moved inside the helper. The player grounded site used to hoist it into a local (HEAD:2317). I grepped every use in both the HEAD and current file: its only consumer was ever the routing call itself. Nothing downstream reads it. No loss.
  • Delegate identity. runTeleportHook now closes over canonical/remote and reuses the caller's isCurrentPositionOwner for both the hook's per-step currency checks and the post-routing guard. Previously two separately allocated but semantically identical lambdas. No change in what is tested.
  • Guard-before-arm (R5) at all three sites. Held. The helper cannot arm — it has no access to ToConstraintArm and its doc says so — and all three callers arm immediately after the null check.
  • The NPC sticky-suppressed path still arms. npcArm is initialised to UnroutedCatchUp and TryArmConstraintAfterOperation sits outside the if (!snapSuppressedByStick || isTeleportRoute) block, exactly as at HEAD. The extraction did not pull it inside.

The LANDING TRANSITION block — confirmed untouched and non-conflicting

:2445-2532 still gates on !rmState.Body.InContact, still returns, and still arms with its own hard-coded NearInterpolate. It therefore cannot double-run with the shared tail, and — importantly — it is what makes playerArm provably never AirborneSnap at the grounded-routing site (the free-flight case has already returned). The only edit is its comment, which round 1 flagged as false (A5) and which is now correct and complete: it enumerates SetPositionSimple / null / Rejected* as classifications that also reach the block, and states why NearInterpolate is the right arming value for every one of them.

ApplyWireAirborneLeftoverBookkeeping — 2 call sites

The helper writes exactly three fields: remote.CellId, remote.LastServerPos, remote.LastServerPosTime.

  • NPC site (:2717-2722) — new, and the R1 fix. Correctly gated !update.IsGrounded && !isTeleportRoute, placed after the IsAirborneNoOperation return (so NoPositionOperation never reaches it) and before the synth-velocity block, the sticky probe, and routing. I confirmed the gate is exhaustive: for RuntimeAcceptedPositionSource.PositionEvent a wire-airborne packet can only classify NoPositionOperation (handled above), null, or Rejected* — the classifier's effectiveContact widening applies only to SameIncarnationCreate, which this path never carries.
  • Player site (:2390-2395) — the helper adds a remote.CellId write the hand-rolled block did not have. I chased this specifically as the classic extraction hazard and it is benign: RuntimePhysicsState.CommitCanonicalCell:2145 short-circuits on fullCellId == record.FullCellId before SetFullCell and before raising CellCommitted, and the player arm wrote the identical p.LandblockId ~180 lines earlier with no placement in between. The redundant write is a true no-op — no spurious rebucket event, no version bump.

Part 2 — the individual fixes

A1 — ToConstraintArm(AirborneSnap) → NearInterpolate, switch made total

Correct, and correct for the right reason: AirborneSnap is acdream's body-contact carve-out for a packet whose wire contact bit said grounded, so retail's MoveOrTeleport returns nonzero and HandleReceivedPosition arms @0x00454272. Mapping it to an arming value restores the 1-arm count and makes the App-layer partition exactly the contract's D4 table.

The throwing default cannot be reached in production. RemoteContactArm has exactly five members (AirborneSnap, SteadyStateInterpolate, FarSnapPlacement, TeleportPlacement, UnroutedCatchUp) and all five have explicit cases; default(RemoteContactArm) is AirborneSnap (value 0), which is handled. The only two producers of the argument are ApplyRemoteContactRouting's returns and the NPC arm's RemoteContactArm.UnroutedCatchUp initialiser. Reaching _ requires an out-of-range cast, which nothing performs. The throw is a genuine "can't-happen" guard, not a live hazard — this was the right call over a silent zero-arm fallback.

The test genuinely discriminates. NpcAirborneSnap_LandingPacket_StillArmsTheLeash asserts host.PositionManager.Constraint goes from null to non-null across the packet. I verified PositionManager.Constraint is lazily created only inside ConstrainTo (PositionManager.cs:27-28, :62), so its presence is direct proof of an arm rather than an inference. Under the pre-fix mapping the packet arms zero times and Constraint stays null — the test fails.

A2 / R3 — NPC synth-velocity and cycle-apply gated on !isTeleportRoute

Correct. isTeleportRoute is hoisted above both the D2 check and the velocity block, and both the install (:2741) and the RemoteServerControlledVelocityCycle.Apply call (:2886) carry it. The fix's own claim that leaving ServerVelocity stale is safe holds up: its only consumers are the now-excluded cycle-apply and RuntimeRemotePhysicsUpdater's stale-velocity watchdog, which only zeroes.

Two incidental changes in the same block are behaviour-neutral and I checked them rather than assumed: the !IsPlayerGuid(update.Guid) guards were dropped from the synth condition and the else if collapsed to else. Both were already dead — the whole NPC section sits after if (IsPlayerGuid(update.Guid)) { … } and every path inside that block returns (the last statement before its closing brace at :2666 is return;), so the section is unreachable for player guids.

NpcTeleport_DoesNotInstallASynthesizedVelocity seeds LastServerPos/ LastServerPosTime so a broken implementation has a real distance and interval to synthesise from. It fails pre-fix (HasServerVelocity would be true).

R1 — the D2 shape on the NPC arm

Correct and now genuinely shared. The AP-137 sentence "unifies player and NPC remotes on one behaviour" is true against the code as of this round — I re-read the row and the two call sites.

NullClassifiedNpc_WireAirbornePacket_WritesOnlyBookkeepingNoBodyOrShadow discriminates: pre-fix the packet reaches ApplyRemoteContactRouting's free-flight carve-out and hard-snaps Body.Position = worldPos, which the test's Assert.Equal(spawnBodyPose, …) catches.

R2 — the teleport hook's collision-end action → ForceEndCollisionReporting

This was the fix I was asked to attack hardest, and it does not carry any of this family's failure modes. RuntimeCollisionReportingState.LeaveWorld is named for the collision-reporting state machine's own leave transaction, not the entity lifecycle's. I traced it end to end:

  • LeaveWorld:870-887_admissionBlocked.Add(key)ForceEnd:1435-1454EndExpiredObjectCollisions(force: true)TrimEmptyOwner. The only mutations are the owner's collision table, the reverse-owner index, and the report queue. Nothing writes body.InWorld, TransientStateFlags.Active, record.ObjectClock, FullCellId, spatial residency, or visibility. Invariant 3 is not at risk.
  • _admissionBlocked is released in a finally, so the subsequent placement's TryPrepareSetPositionBatch:230-235 — which does refuse while a key is in _leaving or _admissionBlocked — is not blocked by the hook that ran immediately before it.
  • _mutationRevision is bumped by LeaveWorld, but the placement's batch captures expectedMutation after the hook (:238) and the prepare→install window is entirely inside the placement, so the bump cannot invalidate the teleport's own batch.
  • Publish swallows observer exceptions (:1489-1494), and _leaving guards re-entrancy, so a force-end cannot escape as a throw into the packet path.
  • TrimEmptyOwner runs on the exit path, so CaptureOwnership's owner count cannot leak an empty entry — no ledger regression.

Dropping the old ShadowObjects.Suspend is also safe and is arguably the better half of the fix: the far-snap arm (shipped, user-accepted) has never suspended the shadow before its own placement, so the teleport arm is now consistent with it rather than uniquely different, and the arm tail's TryPublishRemoteSyncRemoteShadowToBody (ungated at this call site) still re-seeds the registry at the resolved pose. See MINOR B2 for the one thing about R2 that should be stated rather than left implied.


MINOR findings

B1 (MINOR) — contract test-plan item 11 is still unwritten, and it is now the only thing guarding the #42 class

NullClassifiedNpc_WireAirbornePacket_… asserts what must NOT happen (no body write, no shadow republish) but never asserts what MUST happen: that ApplyWireAirborneLeftoverBookkeeping actually wrote remote.CellId and LastServerPos/LastServerPosTime. A regression that emptied the helper body would pass every test in the tree. Those writes are exactly AP-135 — the free-fall sweep gates on rm.CellId != 0 and without it "an airborne remote falls through the floor" (#42), and the first grounded packet after an arc would synthesise its velocity across the whole jump. Two Assert.Equals on the existing test close it.

B2 (MINOR) — R2's fix is correct plumbing, but its observable half still goes nowhere; say so before someone records it as closed

IRuntimeCollisionReportObserver has zero production implementationsgit grep OnCollisionReport returns the interface, the dispatch loop, and four test doubles. CollisionReports.Subscribe has no production caller. So the bidirectional DoCollisionEnd notification retail fires at @0x00514620 still reaches no gameplay consumer; what the fix actually delivers today is the table clear (real, and it does prevent the later ~1 s stale-end and any mis-attribution) plus a correctly-wired channel for when a consumer appears. The hook's new doc doesn't overclaim, but the retail review's phrasing ("neither side receives the immediate ObjectCollisionEnd") will read as closed. State the residual — it is a pre-existing gap in the collision-report plumbing, wider than this slice, and it deserves one sentence rather than a silent inheritance.

B3 (MINOR, disclosed) — no test for R2

Judged acceptable but worth filing. A test would assert against a channel nothing production reads (B2), so its value is pinning the retail mapping, not a behaviour. The cheap version is one assertion that the teleporting owner's collision table is empty after an OnPosition teleport packet — that would fail if a future edit reverted the action to ShadowObjects.Suspend, which is the actual regression risk. Not blocking.

B4 (MINOR, disclosed) — the per-packet runTeleportHook closure

Two delegates + two display classes per remote accepted Position (the outer runTeleportHook lambda and the caller's isCurrentPositionOwner), allocated whether or not the packet is a teleport. The implementer's stated blocker — that ApplyRemoteContactRouting's Func<bool> parameter is injected by existing tests — is real but the parameter is internal, so it is a refactor cost, not a compatibility one. Judged acceptable to defer: this is the per-packet network path (5-10 Hz per remote), not Slice I's per-frame resolve path where the 0-B target applies, and the objects are gen0-transient. File it with the probe-family cleanup rather than churning the seam now.

B5 (MINOR, disclosed) — the stress test's teleport step

Round 1's A3 stands as written and is judged acceptable to leave. The scenario it covers (Hidden → DeferredShadowRestore → landblock churn → UnHide) no longer has any teleport-specific machinery to interact with, because this slice deletes _activePlacementOwners and the placement is now synchronous inside OnPosition. The hand-built step is honestly documented as reproducing the arm tail's observable state, and the arm tail itself is directly covered by LiveEntityNetworkRemoteTeleportPresentationTests. The residual loss — the interaction between a mid-teleport Hidden edge and the placement — is a degenerate case now, not an untested one.

B6 (MINOR, carried) — proof obligation 1 is still unstated

Unchanged from round 1's A9. The contract requires the ParkCollisionResidents-overlap-throw unreachability argument (with 4b-1's B2 caveat that the guarded property is TryAcquireCollisionPrefixMutationPermission's HasOldPrefixPlacementDebt refusal — a stall, not a throw) in the implementation commit's contract-conformance section. The work is still uncommitted so I cannot verify it; the property itself holds by reading (the teleport arm adds packets to the same one-operation-per-key machinery and opens no new operation shape).


Round-1 MINORs — spot-check

Verified fixed, not re-derived: A4 (TeleportRefused_… now arms sticky before the packet and asserts GetStickyObjectId() == 0 afterward — a real discriminator, since only the teleport arm's hook calls UnStick, so the UnroutedCatchUp look-alike I identified can no longer pass it); A5 (the landing-block comment is now correct and complete); A7 (R1, above); A8 (wasCellless deleted). A10 was accepted and restated correctly. The retail lane's R4 (the prologue comment's CommittedCellId claim) and R5 (acdream-architecture.md / code-structure.md still describing the deleted classes) are both fixed in the diff.


Gate status observed

  • dotnet build AcDream.slnx -c Releasesucceeded, 0 warnings, 0 errors.
  • tests/AcDream.Runtime.Tests -c Release --no-build1,125 / 0 / 0.
  • tests/AcDream.App.Tests -c Release --no-build4,081 / 3 skipped / 0.
  • Complete Release suite: still not run and not recorded. The contract is explicit that the new figure must be measured, not inherited from 11,027. Outstanding.
  • Two-client connected teleport gate: outstanding. It must use an NPC target (@teleto a creature into view), not a second player character — both round-1 MAJORs lived on the NPC arm, and RemoteServerControlledVelocityCycle.Apply early-returns for 0x50xxxxxx guids, so a player-remote teleport exercises neither fix. Confirm at least one [remote-teleport] line per teleport with the expected cause.