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

299 lines
17 KiB
Markdown

# 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`](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`](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 `null``if (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 `return`s, 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 `return`s (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-1454`
`EndExpiredObjectCollisions(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
`TryPublishRemote``SyncRemoteShadowToBody` (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.Equal`s 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 implementations**
`git 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 Release`**succeeded, 0 warnings, 0 errors**.
- `tests/AcDream.Runtime.Tests -c Release --no-build`**1,125 / 0 / 0**.
- `tests/AcDream.App.Tests -c Release --no-build`**4,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`.