acdream/docs/research/2026-08-04-c4-route-4b-3-architecture-review.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

386 lines
22 KiB
Markdown

# C4 route 4b-3 — architecture / adversarial review (2026-08-04)
Reviewer lens: **adversary**. Retail conformance is a separate reviewer's
scope; this review asks only "does this break something, leak something,
deadlock something, or leave an entity in a bad state?"
Subject: the uncommitted working tree on `claude/acdream-physics-divergence-5aa784`
at base `3e002993` (`git diff HEAD` plus the three untracked non-contract
files). Contract:
[`2026-08-04-c4-route-4b-3-contract.md`](2026-08-04-c4-route-4b-3-contract.md).
## Verdict: **FAIL**
Two MAJOR findings. Both are behaviour REGRESSIONS against `3e002993` — not
pre-existing residuals — and both sit on code paths the contract's own
invariant list names as load-bearing (invariant 5, the arm-count partition;
invariant 6 / D3, "the hook's whole point is that no locomotion state
survives a teleport"). Both are invisible to the specified two-client
connected gate, because that gate teleports a *player* character and both
defects are on the NPC/creature arm.
Everything else I checked held. Build is green (0 warnings); focused suites
pass **Runtime 1,125 / 0 skips** and **App 4,078 / 3 skips** at Release with
`--no-build`.
---
## What I verified and found sound (do not churn)
- **Invariant 1 (`StoresAcceptedDestination`).** `ApplyAcceptedRemoteTeleport`
(`src/AcDream.Runtime/Session/RuntimeRemotePlacementDriveController.cs:820-848`)
is byte-for-byte the far arm's shape minus the `StopInterpolating` clear,
including the currency-guarded `StoreAcceptedDestinationPose`. The partition
is extended, not re-litigated.
- **Invariant 3 (no stranded entity).** The teleport arm reaches
`SubmitAndResolve` through the *same* `TryExecuteAcceptedRemotePosition`
the far arm uses; the `DeferredCell` arm still cancels synchronously with
`restoreCancelledPark: true`. No new park shape, no new withdrawal site.
- **Invariant 10.** `TickLostCellDeadlines` / `TryDequeueExpiredLostCell` /
`ArmLostFamilyDeadlines` still have zero production callers (4 references,
all inside `RuntimeSetPositionState.cs`; the rest are tests). The reaper
stays inert.
- **Invariant 11 (route 1 unchanged).** The shared builder got an *overload*,
not a third copy, and the route-1 overload forwards
`committedCellId: canonical.FullCellId` — its pre-slice value verbatim
(`RuntimeAcceptedPositionRouteRequests.cs:41-56`).
- **D1's plumbing is honest.** `PreMergeCommittedCellId = hadCanonical ? beforeCell : null`
is measured before `RefreshSnapshot`
(`RuntimeEntityObjectLifetime.cs:1725-1770`), never re-read, and `null`
("no prior canonical record") makes `TryBuild` decline rather than fabricate
a 0. `RuntimeRemoteTeleportClassificationTests` genuinely discriminates the
fix from the shipped dead predicate — the companion test fails under the
post-merge read. This is the strongest test in the diff.
- **The `_activePlacementOwners` deletion is legitimate** — see item 1 below.
- **Shadow suspend/restore across the hook.** `RemoteTeleportHook`'s
`ReportCollisionEnd``ShadowObjects.Suspend` removes the entity from
`_entityToCells`; the arm tails' `LiveEntityShadowPublisher.TryPublishRemote`
`ShadowPositionSynchronizer.Sync``RefreshPositionRows` un-suspends it
(`ShadowObjectRegistry.cs:1336-1393`). I specifically checked for a
pose-gate that could skip the restore on a zero-distance teleport: the
OnPosition-tail overload is **ungated** (`RuntimeRemotePhysicsUpdater.cs:1175-1191`);
only the per-tick loop gates. `TeleportRefused_…`'s
`Assert.Single(fixture.Shadows.AllEntriesForDebug(), …)` is a real proof of
this, because `AllEntriesForDebug` enumerates `_entityToCells`.
**No invisible-and-intangible entity on this path.**
- **Deletion completeness.** All eight wiring sites in the contract's
inventory are cut, including the two the handoff missed
(`SessionPlayerComposition`, `LiveSessionResetManifest`). Reset coverage is
genuinely preserved: `GraphicalSessionEventRoute.cs:152` calls
`_remotePlacementDrive.DetachRoute`.
- **Register bookkeeping.** AD-42, AP-136, AP-137, AP-138 all updated in the
diff; AP-138's Risk column does gain the teleport arm as a second producer,
and AP-137 is rewritten rather than deleted. (One factual defect in the
AP-137 rewrite — see A7.)
---
## MAJOR findings
### A1 (MAJOR) — the NPC arm silently stops arming `ConstrainTo` for every airborne-body packet; the code documents the opposite
**File:** `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:1290-1311`
(`ToConstraintArm`), consumed at `:2643`.
`ToConstraintArm` maps `RemoteContactArm.AirborneSnap` through its `_` arm to
`RuntimeRemoteAcceptedPositionArm.AirborneNoOperation` — the one value
`TryArmConstraintAfterOperation` never arms — and justifies it at `:1296-1297`
with:
> *"There is no Runtime analogue of `AirborneSnap` (a caller reaching it
> always returns before any arming call) … production never calls this with
> that arm."*
**That claim is false for the NPC arm.** After
`npcRouting = ApplyRemoteContactRouting(…)` at `:2600`, the only early return
is the currency guard at `:2619-2626`, which is gated on
`FarSnapPlacement or TeleportPlacement`. Control then falls out of the
`if (!snapSuppressedByStick || isTeleportRoute)` block straight into
`TryArmConstraintAfterOperation(ToConstraintArm(npcRouting.Arm), rmState)` at
`:2641-2644`. `AirborneSnap` is returned by
`ApplyRemoteContactRouting`'s free-flight carve-out at `:1123-1150` for **any**
non-teleport classification whenever `!remote.Body.InContact`.
**Concrete failure scenario.** A creature remote (guid not `0x50xxxxxx`) is
knocked off a ledge or jumps, so its canonical body has no contact plane
(`Body.InContact == false`). ACE broadcasts a wire-grounded `UpdatePosition`
(`IsGrounded: true`) at 5-10 Hz. The classifier returns `Interpolate`
(`playerDistance < 96 m`). Routing takes the airborne carve-out →
`AirborneSnap``ToConstraintArm``AirborneNoOperation`**no arm**.
At `3e002993` the same packet armed: the post-operation site was passed the
ROUTE (`earlyRemoteRoute`), and `OwnsAfterOperationConstraint(Interpolate)` was
true with `ConstrainAfterRouting` true. So the arm count for this row goes
**1 → 0**, which is the "never zero times" half of contract invariant 5 and a
`yes` row of D4's partition table turning into a `no`. Retail's near branch
returns 1 @0x005163BE, so `HandleReceivedPosition` arms unconditionally
@0x00454272 — the divergence is in the wrong direction. The same 1→0 applies
to a far (`SetPositionSimple`) classification and to a null/`Rejected*`
classification whenever the NPC's body is out of contact.
Observable consequence: a creature that is knocked airborne on the **first**
accepted Position after spawn never sets `IsConstrained` at all, so the
`ConstraintManager` brake and `IsFullyConstrained()` (which gates
`jump_is_allowed`) stay dead for it; a creature already armed loses the
per-packet re-anchor (`ConstraintPosOffset` reset to 0) for the whole airborne
interval.
**Why no test caught it.** Proof obligation 3 asked for "a test that counts
arming calls per packet across the partition table's rows". What was delivered
(`RuntimeRemoteSteadyStatePositionTests.TryArmConstraintAfterOperation_MatchesTheCompletePartition`)
tests the Runtime *predicate* given an already-chosen arm value. Nothing tests
`ToConstraintArm`, and nothing counts arms per packet through `OnPosition`.
The mapping — the only new code in the arming path — is 100 % uncovered.
**Fix direction.** `AirborneSnap` is not `AirborneNoOperation`: retail's
airborne *no-op* is `arg4 == 0` (the WIRE contact bit, return 0), while
`AirborneSnap` is acdream's carve-out keyed on the BODY's contact for a packet
whose wire bit said grounded — retail returns nonzero for that packet and
arms. Either map `AirborneSnap` to an arming value, or (better) make
`ToConstraintArm` total with an explicit `AirborneSnap` case plus a
`_ => throw`, and add the missing end-to-end arm-count test across the D4 rows
including the body-airborne ones.
---
### A2 (MAJOR) — a teleported NPC gets a ~1,000 m/s synthesized `ServerVelocity` and an animation cycle planned from it
**File:** `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:2526-2543`
(synthesis) and `:2683-2709` (consumption).
The NPC arm synthesizes a locomotion velocity for any packet that carries no
wire velocity:
```
serverVelocity = (worldPos - rmState.LastServerPos) / (float)elapsed; // :2532
… rmState.ServerVelocity = authoritativeVelocity; HasServerVelocity = true;
```
`rmState.LastServerPos` is only advanced at `:2681`, i.e. **after** routing, so
at `:2532` it still holds the pre-teleport position. For an admin teleport the
delta is the whole teleport distance over one packet interval — e.g. 192 m /
0.15 s ≈ 1,280 m/s. The tail then calls
`RemoteServerControlledVelocityCycle.Apply(update.Guid, ae, rmState, rmState.ServerVelocity)`
at `:2704-2709`, which runs `ServerControlledLocomotion.PlanFromVelocity` and
`Sequencer.SetCycle(style, plan.Motion, plan.SpeedMod)`.
At `3e002993` this was unreachable for a teleport: the `remotePlacementRequired`
block sat in the SHARED section, **above** the `IsPlayerGuid` split and above
the NPC synth-velocity code, and it always `return`ed. The diff deleted that
block and routed the teleport through the NPC tail instead, so the synthesis
now runs on every NPC teleport.
**Concrete failure scenario.** `@teleto` a drudge 200 m away while an acdream
client observes it. The teleport arm places it correctly, then the same packet
installs `ServerVelocity ≈ 1.3 km/s` and calls `SetCycle(Run, speedMod≈huge)`.
`RemoteServerControlledVelocityCycle.Apply`'s three guards do not help: the
body is grounded after the commit (`rm.Airborne == false`), the guid is not a
player guid, and `rm.MoveTo` was just set to `Invalid` **by the teleport hook's
own `CancelMoveTo`** — so the hook actively removes the last thing that would
have suppressed this. The creature stands at the destination playing a
run/sprint cycle at an absurd speed multiplier until the stale-velocity
watchdog fires (`ServerControlledVelocityStaleSeconds = 0.60`) or the next
packet's small delta replaces it.
This is directly contrary to `teleport_hook`'s purpose (@0x00514ED0 exists so
that *no* locomotion state survives a teleport) and is exactly the
"stands with correct animation" clause of the contract's live gate — which
cannot see it, because the gate teleports a player character and
`RemoteServerControlledVelocityCycle.Apply` early-returns for `0x50xxxxxx`
guids. The player-remote arm is accidentally immune because its own teleport
block returns at `:2185`, before the player synth-velocity at `:2432-2443`.
**Fix direction.** The teleport arm must suppress the synthesized velocity for
its own packet on the NPC arm too — either by advancing
`rmState.LastServerPos` to the destination before the synthesis, or (cleaner,
and symmetric with the player arm) by giving the NPC teleport its own tail
that skips the synth/cycle step. Whichever is chosen, add a test that asserts
the NPC sequencer's cycle is unchanged across a teleport packet; today nothing
in the tree looks at the animation layer for this arm.
---
## MINOR findings
### A3 (MINOR) — the stress test's teleport step now drives zero production code
**File:** `tests/AcDream.App.Tests/World/LiveEntityLifecycleStressTests.cs:710-730`.
The contract's deletion inventory says `LiveEntityLifecycleStressTests`
"constructs the controller and calls `TryApply` — its scenario must be
re-expressed against the canonical teleport arm, not dropped."
`BeginDeferredTeleport` is now four hand-written field assignments
(`_remote.Body.Position = destination; _remote.CellId = …;
Entity.SetPosition(…); Entity.ParentCellId = …`). It exercises no placement,
no hook, no routing. `RepeatedRetailRecallMotion_HiddenTeleportUnhide_…`
(`:252-296`) would pass identically if the entire teleport arm were deleted.
The Hidden/DeferredShadowRestore half of the scenario still discriminates, so
this is a coverage loss rather than a false pass — but the mid-teleport
Hidden/UnHide interaction the fixture exists for is no longer tested against
the mechanism that now performs the teleport.
### A4 (MINOR) — `TeleportRefused_…` does not discriminate the teleport arm from `UnroutedCatchUp`
**File:** `tests/AcDream.App.Tests/Physics/LiveEntityNetworkRemoteTeleportPresentationTests.cs:157-206`.
Every assertion in this test (`body.Position == destination + offset`,
`Entity.Position == body.Position`, `IsSpatiallyVisible`, one shadow entry at
`body.Position`) is also satisfied if the packet had classified `null` and
taken `UnroutedCatchUp``ApplyInterpolate`, because the body-to-target
distance is >192 m so AP-87's `bodyToTarget > 4 m` branch hard-places the body
at exactly the same wire pose and the same NPC tail then syncs entity and
shadow. The test *does* catch removal of the `store_position` fallback, so it
is not worthless, but the sibling commit test is the only one whose expected
value (`+ FootSphereCenterLift`) can only come from a canonical placement.
Adding one `Assert.Equal(0u, …)`-style discriminator (or asserting the hook
ran, as the routing-seam tests do) would close it.
### A5 (MINOR) — the landing block's new comment states something false
**File:** `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:2266-2272`.
> *"A landing packet always classifies Interpolate (the teleport/cell-less arm
> dispatches earlier and returns before this block can be reached — D5)."*
The teleport half is true; "always classifies Interpolate" is not. The landing
block is reached for a wire-grounded, body-out-of-contact packet with ANY
non-teleport, non-airborne-no-op classification — including `SetPositionSimple`
(>= 96 m), `null` (login window), `RejectedAuthority`, and `RejectedData`. The
hard-coded `RuntimeRemoteAcceptedPositionArm.NearInterpolate` at `:2273` still
produces the correct arm count for all of those (every one of them is an
"arms" row), so this is a comment defect, not a behaviour defect — but process
rule 6 is explicit, and this is exactly the shape ("a comment asserting
behaviour the code no longer has") that six consecutive slices have shipped.
### A6 (MINOR) — new per-packet allocations on the remote hot path
**File:** `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:2148-2153`,
`:2370-2374`, `:2609-2613`, and `:1080-1088`.
`runTeleportHook: () => RunRemoteTeleportHook(update.Guid, entity.Id, () => IsCurrentPositionOwner(entity))`
captures `update`, `entity` and `this`, so a display-class + delegate pair is
allocated for **every** remote accepted Position, not only for teleports (the
pre-slice tree allocated the inner lambda only when `remoteHardTeleport` was
true). Separately, `teleportStatus.ToString()` at `:1087` is evaluated at the
call site, so the probe's own `ProbeRemoteTeleportEnabled` self-guard does not
prevent the string allocation. Given Slice I's "0 B/resolve" discipline this
is worth a `Func<bool>` cached per controller, or hoisting the probe behind
`if (PhysicsDiagnostics.ProbeRemoteTeleportEnabled)` at the call site.
### A7 (MINOR) — D2's "unifies the player and NPC arms" is not delivered, and AP-137 now claims it was
**Files:** `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:2186-2210`;
`docs/architecture/retail-divergence-register.md` (AP-137 row, the "**D2 — the
wire-airborne leftover shape**" sentence).
The retail return-0 shape is implemented only inside the `IsPlayerGuid` block.
On the NPC arm a wire-airborne packet with a null/`Rejected*` classification
still falls through the synth-velocity block into
`ApplyRemoteContactRouting`, which either hard-snaps the body (`AirborneSnap`,
if the body is also out of contact) or **enqueues/places** it
(`UnroutedCatchUp``ApplyInterpolate`, if the body is in contact) and then
syncs the render entity and publishes the collision shadow. That is not
"AP-135's two bookkeeping writes only, no body/queue/render write, no leash
arm, return". The register row's claim that this "unifies player and NPC
remotes on one behaviour" is therefore factually wrong as shipped, which is a
register-rule-1 problem in its own right. (This is NOT a regression against
`3e002993` — the NPC arm behaved this way before — so the fix can legitimately
be "scope the row to the player arm" rather than "implement the NPC half".)
### A8 (MINOR) — dead local
`src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs:1727`:
`bool wasCellless = hadCanonical && beforeCell == 0u;` has no remaining reader
(only the comment at `:1759` mentions it). No compiler warning fires because
the initializer is not a constant. Delete it or the comment will outlive the
variable's meaning.
### A9 (MINOR) — proof obligation 1 is not stated anywhere in the tree
The contract requires the `ParkCollisionResidents`-overlap-throw
unreachability argument (with 4b-1's B2 caveat about
`HasOldPrefixPlacementDebt` being a stall rather than a throw) to be stated
"in the contract-conformance section of the implementation commit". The work
is uncommitted, so I cannot verify it; nothing in the diff carries the
statement. The underlying property does hold by reading — the teleport arm
adds packets to the same one-operation-per-key machinery and opens no new
operation shape — but the statement is still owed.
### A10 (MINOR, partly unverified) — the visibility-edge protection `_activePlacementOwners` provided has no replacement
I independently traced the deletion the implementer reported (item 1 of the
task). **Their conclusion is right, their stated reasoning is not.**
`_activePlacementOwners` was NOT write-never at `3e002993`: the writer chain
was `LiveEntityNetworkUpdateController` (`remotePlacementRequired`
`_remoteTeleportController.BeginPlacement`) → `RemoteTeleportController.BeginPlacement`
`RemoteTeleportPlacementPresentation.Begin`
`LiveEntityPresentationController.BeginAuthoritativePlacement`
(`RemoteShadowPlacementSynchronizer.cs:48` at HEAD). It becomes write-never
*because this slice deletes that chain*, which is exactly what the contract's
deletion-inventory row said. `git grep` over `src` + `tests` at HEAD confirms
no other production writer. So deleting the set and its four `IsPlacementActive`
gates changes nothing for any surviving path, and the four gates degenerating
to "always act" is correct.
What is not replaced is the *protection*: during the old multi-frame pending
placement, an intervening Hidden/UnHide edge was prevented from restoring a
shadow at an unresolved pose. The new teleport is synchronous inside
`OnPosition`, which mostly removes the window — but the canonical placement
publishes its receipt synchronously and the projection sink can raise a
visibility edge from inside it. I could not construct a reachable
Hidden/UnHide-during-placement path in the new design and am flagging this as
**unverified**, not as a defect.
---
## Judgment on the three reported items
**1. `_activePlacementOwners` deletion — sound, reasoning wrong.** See A10.
The set had a real production writer at HEAD; it is the slice's own deletions
that kill it. The four consumers' degeneration is behaviour-neutral for every
surviving path. Not a defect. The commit message should say "its only writer
is deleted by this commit", not "the predicate was permanently false".
**2. The two parallel `OnPosition` copies — a genuine structural hazard, and
it has already produced a defect.** This is not a stylistic complaint: A2 is
precisely the two copies disagreeing. The player copy's teleport block returns
at `:2185` before its synth-velocity at `:2432`; the NPC copy has no teleport
block at all and falls through its synth-velocity at `:2526` on the way to
routing. A1 is the second instance — the player copy cannot produce
`AirborneSnap` (its landing block returns first) while the NPC copy can, so
the shared `ToConstraintArm` was written against the player copy's reality and
is wrong for the NPC one. The implementer's own report that "a sabotage of the
wrong copy left both tests green" is the same signal. The duplication should
be collapsed, or at minimum every arm tail should be one shared helper that
both branches call; shipping a third slice against these two copies without
that is how the next one of these lands.
**3. Declining D7 (consolidating the post-placement currency guard into a
Runtime seam) — acceptable, and not the source of either MAJOR.** The guard is
now duplicated at three App call sites (`:2155-2161`, `:2393-2399`,
`:2619-2626`) instead of two. I checked all three: the predicate is
character-identical (`!IsCurrentPositionOwner(entity) || !ReferenceEquals(positionRecord.RemoteMotionRuntime, rmState)`),
and all three sit BEFORE their arming call, preserving the R5 invariant. The
widened arm predicate (`FarSnapPlacement or TeleportPlacement`) is applied
consistently at the two that need it; the teleport-specific site is
unconditional, which is strictly stronger. So the duplication is not itself a
correctness hazard today. It is, however, the same duplication-of-invariant
pattern as item 2, and a fourth copy (route 5's projectile arm) is next in the
queue — consolidating it is now overdue rather than optional.
---
## Gate status observed
- `dotnet build AcDream.slnx -c Release`**succeeded, 0 warnings, 0 errors**.
- `dotnet test tests/AcDream.Runtime.Tests -c Release --no-build`
**1,125 passed / 0 skipped / 0 failed**.
- `dotnet test tests/AcDream.App.Tests -c Release --no-build`
**4,078 passed / 3 skipped / 0 failed**.
- Complete-solution Release suite: **not run by this reviewer** (the contract's
own gate; the new figure still has to be measured and recorded, not
inherited from 11,027).
- Two-client connected teleport gate: **not run**, and note the finding above
that neither MAJOR is observable through it as specified — A1 and A2 both
need a *creature* teleport (`@teleto` a drudge/mosswart into view), not a
second player character.