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

27 KiB
Raw Permalink Blame History

C4 route 4b-3 — retail-conformance review (2026-08-04)

Reviewer lens: does this diff do what the retail client does? Architecture, style, and layering are a separate reviewer's lane.

Subject: the uncommitted working tree on claude/acdream-physics-divergence-5aa784 at HEAD 3e002993 (git diff HEAD + four untracked files; the contract docs/research/2026-08-04-c4-route-4b-3-contract.md is itself untracked and is not part of the change under review).

Build: dotnet build AcDream.slnx -c Releasegreen, 0 warnings. Focused check: AcDream.Runtime.Tests --filter Teleport — 29/29 pass. Per process rule 3, neither is treated as evidence of conformance.


VERDICT: FAIL

Three MAJOR findings. R1 and R2 are contract requirements that were pinned and not met, and R1 additionally ships a register row asserting behaviour the code does not have — the exact defect class process rule 6 exists to stop. R3 is a newly-reachable retail divergence whose symptom is precisely what the connected gate recipe lists as its acceptance criterion ("stands with correct animation").

None of the three is hard to fix. The core of the slice — the D1 pre-merge cell, the arm ordering, the single ConstrainTo site, the hook order, the flags — is correct and verified against the decomp. The failures are at the edges the findings chain keeps warning about: the NPC copy, and an unported half nobody re-derived.


Part 1 — independent verification of the contract's retail claims

Every address in the contract's "Retail ground truth" section was re-read in docs/research/named-retail/acclient_2013_pseudo_c.txt. All five load-bearing facts confirmed.

claim verified
MoveOrTeleport @0x00516330; branch @0x00516386 if (eax_8 != 0 || this_1->cell == 0) ✓ line 284304ff. this_1 = this is assigned @0x00516334 from the incoming CPhysicsObj*, so this_1->cell is the body's own current cell, read at entry, before any placement. The whole D1 design rests on this and it is right.
arg4 is read only @0x0051638E, after the branch ✓ — the teleport branch's return 1 @0x00516438 executes without arg4 ever being touched. A teleport/cell-less packet places unconditionally: airborne wire bit, airborne body, any distance.
teleport_hook @0x005163EF runs BEFORE SetPosition @0x00516420
teleport_hook @0x00514ED0 action list and order ✓ line 283115ff, exactly: CancelMoveTo(0x3C) @0x00514EDF → UnStick @0x00514EEE → StopInterpolating @0x00514EFD → UnConstrain @0x00514F0C → ClearTarget @0x00514F1B + NotifyVoyeurOfEvent(Teleported_TargetStatus) @0x00514F28 (one target_manager != 0 guard over the pair) → report_collision_end(this, 1) @0x00514F31. Each manager guarded on non-null.
SetFlags(0x1012) @0x00516414 ✓ = Teleport(0x002) | Slide(0x010) | SendPositionEvent(0x1000). acdream's AuthoritativeTeleportFlags (RuntimeAuthoritativePositionRouteClassifier.cs:197-200) is bit-for-bit the same against PhysicsSetPosition.cs:64-76.
ONE ConstrainTo @0x00454272 via HandleReceivedPosition @0x00453FD0 ✓ line 92896ff. The remote branch (arg2 != this->player @0x0045414D) is if (MoveOrTeleport(...) != 0) @0x00454254 → ConstrainTo(arg2, &arg2->m_position, …) @0x00454272 → return. Single site, shared by all three nonzero-returning branches, anchor read live off arg2->m_position (post-move). MoveOrTeleport discards SetPosition's error and returns 1 regardless, so retail arms even when the placement failed — confirmed.
ZeroVelocity is local-player-only set_velocity(player_2, {0,0,0}, 1) @0x004541B4 sits inside the arg2 == this->player + newer_event(TELEPORT_TS) branch. The remote branch writes no velocity at all.

Contract errata (does not change any decision)

C1 (MINOR). The contract's pseudo-C excerpt presents return 0 @0x0051636D as the fallthrough of the arg4 test. It is actually the else-label of an outer sequence gate at @0x00516364 (if (-((eax_4 - eax_4)) == 0) — a Binary-Ninja- mangled POSITION_TS/update_times[4] comparison that wraps the entire body). The arg4 == 0 path does fall into the same label, so the behavioural reading ("writes nothing, returns 0") is correct; the listing just implies a flatter control flow than the binary has. Worth correcting if the contract is reused.

C2 (MINOR, out of scope but noted). HandleReceivedPosition runs unset_parent(arg2) @0x00454129 and SetPlacementFrame @0x00454142 (when !HasAnims) before MoveOrTeleport. The contract's "recorded-not-consumed" list (UnparentBeforeRouting / ApplyPlacementFrameBeforeRouting) is therefore accurate — retail really does perform both ahead of the teleport branch, and acdream still records-without-reading them. Correctly deferred, correctly stated.


Part 2 — answers to the four open questions routed to this review

Q1 — D1's evidence chain: CONFIRMED. The plumbing is necessary, not churn.

Verified end to end in source, not inferred:

  • RuntimeEntityObjectLifetime.TryApplyPosition measures the pre-merge value at src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs:1723-1727 (hadCanonical / beforeCell / wasCellless) before calling Entities.TryApplyPosition at :1728.
  • The merge onto the canonical record happens later, at :1786-1789: Entities.RefreshSnapshot(canonical, snapshot, refreshPosition: acceptedPosition).
  • RuntimeEntityDirectory.RefreshSnapshot:238record.RefreshDerivedState(refreshPosition).
  • RuntimeEntityRecord.RefreshDerivedState:230-237SetFullCell(position.LandblockId, …), and SetFullCell:244-251 writes FullCellId outright.

So after the merge canonical.FullCellId is the accepted wire cell, and the classifier — which the App calls after the merge and before the prologue rebucket (LiveEntityNetworkUpdateController.cs:1832-1840, ahead of RebucketLiveEntity at :1865) — was reading the wire cell through the route-1 Build overload. A wire cell of 0 is refused as RejectedData at RuntimeAuthoritativePositionRouteClassifier.cs:321-322, before the cell-less test at :402-404. The shipped remote cell-less predicate was unreachable. D1's characterisation is exactly right and the fix is correct: the pre-merge value is threaded on AcceptedPhysicsTimestamps.PreMergeCommittedCellId and consumed through a genuine overload (never a third copy), with null meaning "no opinion" rather than a fabricated 0.

One consequence D1 did not name — see R8 below.

Q2 — report_collision_end(this, 1)ShadowObjects.Suspend: there IS an unported half. See R2.

Q3 — the _activePlacementOwners deletion: safe. Confirmed behaviour-preserving.

IsPlacementActive had four consumers (Hidden suspend, UnHide restore, RestoreOrdinaryShadowInsideProjection, SuspendOrdinaryShadowOutsideProjection). The set's only writers were BeginAuthoritativePlacement / CompleteAuthoritativePlacement, whose only production caller was the deleted RemoteTeleportPlacementPresentation. With the writers gone the set is permanently empty, so every consumer degenerates to the false branch — which is precisely what the diff hard-codes. No Hidden/UnHide or visibility-edge restore changes for non-teleport entities. HasDeferredShadowRestore and _suspendedShadowOwners are untouched.

Q4 — RemoteTeleportSuffixIsQueuedBehindInitialAdmission: survives, correctly re-pointed.

It pins that a dormant initial-residence FIFO preserves teleport-sequence ordering (0,1,2) across continuations. The two TeleportHookRequired assertions were replaced with Assert.Null(...PreMergeCommittedCellId) on the same two continuations — a real assertion about the new field, not a tautology. But see R8: what that assertion proves is a behaviour change nobody recorded.


Part 3 — findings

R1 (MAJOR) — the D2 wire-airborne return-0 shape exists only on the player arm; the NPC arm still writes. AP-137 asserts otherwise.

Where: src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:2188-2211 (the player-remote D2 block) has no counterpart on the NPC arm. The NPC arm's only wire-airborne early return is :2516-2523, gated on IsAirborneNoOperation(earlyRemoteRoute), which (RuntimeRemoteSteadyStatePosition.cs:81-86) matches only the NoPositionOperation disposition — never null and never Rejected*.

What happens instead. An NPC remote with a null or Rejected* classification and a clear wire contact bit falls through :2525-2543, :2560, and reaches ApplyRemoteContactRouting at :2600, where:

  • body not in contact → :1145-1148 writes remote.Body.Position = worldPos; remote.Body.Orientation = rotation; and returns AirborneSnap;
  • body in contact → :1196-1203 ApplyInterpolate enqueues a waypoint, then :2642 arms the leash.

Retail: MoveOrTeleport @0x0051638E reads arg4, finds it 0, and falls to return 0 — no body write, no queue write, and (because ConstrainTo sits inside if (MoveOrTeleport(...) != 0) @0x00454254) no leash arm. Retail makes no player/NPC distinction anywhere in MoveOrTeleport. The null classification is reachable for every remote through the whole login window (ClassifyRemoteAcceptedPosition returns null until _playerController exists), so this is not a corner.

Why MAJOR rather than "pre-existing". The behaviour predates the slice, but two things make it a failure of this slice:

  1. D2 pinned the outcome — "unifies the player and NPC arms on one leftover- airborne behaviour" — and only half of it was implemented.
  2. The rewritten AP-137 row now states as fact: "This deletes the legacy player-arm fallback's entity-revert quirk … and unifies player and NPC remotes on one behaviour." That sentence is false against docs/architecture/retail-divergence-register.md line 287's own code. A register row that describes behaviour the code does not have is worse than no row: it is the thing that stops the next reader from finding the divergence.

Correct behaviour: give the NPC arm the same early return the player arm now has — AP-135's two bookkeeping writes, then return — gated on the same predicate (classification is null/Rejected* and !update.IsGrounded). Or, if the intent is to keep the NPC snap deliberately, rewrite the AP-137 sentence to say so and give the asymmetry its own row.

Test gap that let it through: contract test-plan item 10 ("D2's leftover- airborne shape: wire-airborne null-classified packet writes exactly AP-135's two fields and nothing else") was not written for either arm, and item 11 (AP-135 preservation on both airborne no-op paths) was not written either. The D4 partition test that was written (RuntimeRemoteSteadyStatePositionTests.TryArmConstraintAfterOperation_MatchesTheCompletePartition) asserts the Runtime helper's arm→bool mapping, which is correct, but the partition table's two "no arm" rows for the wire-airborne leftover case are a property of the caller returning early — and that property is only true on the player arm. The test cannot see the gap.


R2 (MAJOR) — report_collision_end(this, 1) is mismapped; the faithful port exists in-tree and is not called.

Where: src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:254ReportCollisionEnd: () => _physicsEngine.ShadowObjects.Suspend(localEntityId).

Retail @0x00514F31 → @0x00514620. report_collision_end(this, arg2) walks this->collision_table. With arg2 != 0 the staleness/still-touching tests are bypassed (@0x005146F3, @0x005146E7 both goto label_514706), so every record is deleted and each is passed to report_object_collision_end @0x00510A90, which fires weenie_obj->DoCollisionEnd(partnerId) on this object (gated on this->state & 8 = REPORT_COLLISIONS) and DoCollisionEnd(this->id) on the partner (gated on the partner's own 0x8, and skipped wholesale when the partner carries state & 0x200000). It is a force-end-all with bidirectional notification.

What acdream substitutes. ShadowObjectRegistry.Suspend (src/AcDream.Core/Physics/ShadowObjectRegistry.cs:1480-1499) removes the entity from every cell collision list while retaining its registration. That method's own doc comment says what it is: "the registry counterpart of retail CPhysicsObj::remove_shadows_from_cells" — a different retail function, which teleport_hook does not call. (Retail's shadow removal for a teleport happens later and internally, inside SetPositionInternal.)

The faithful port already exists. RuntimeCollisionReportingState (src/AcDream.Runtime/Physics/RuntimeCollisionReportingState.cs) is an explicit port of CPhysicsObj::collision_table and publishes bidirectional ObjectCollisionEnd with the same ReportCollisions gates on both sides (PublishResolvedObjectEnd:1225-1270). It has a private ForceEnd(record, key) (:1435-1450) that is exactly report_collision_end(this, 1) — reached today only from the destruction / leave-world / batch-retirement edges (:547, :881, :913, :943). The teleport hook does not reach it.

The canonical placement does not cover the gap: the SetPosition batch dispatch runs EndExpiredObjectCollisions(..., force: false, ...) (:493-500), which is retail's other call — handle_all_collisionsreport_collision_end(this, 0) @0x005147F0. Retail runs both: force-end-all in the hook, then the ordinary non-forcing pass inside the placement.

Observable delta. After a remote teleports away, any object still holding a CollisionRecord against it keeps it until the ordinary ~1 s staleness pass, and neither side receives the immediate ObjectCollisionEnd retail fires. Retail ends it on the spot, on both sides.

Added-divergence half. Conversely, suspending the shadow at hook time is something retail does not do there: between the hook and the placement — and, on any non-commit outcome, until the next SyncRemoteShadowToBody — the remote is non-collidable. The window is bounded (the placement's Register/RefreshPositionRows both clear _suspendedEntities, and the DR tick's ShouldSynchronizeShadow re-syncs on the next pose delta), so this is a frames-scale residual rather than the #184 permanent class — but it is real, and it is not what retail does at this call site.

Contract compliance: the contract's open question 2 required the reviewer to "confirm … or file the delta on the AP-137 successor row." The delta is real and the row does not mention report_collision_end, ShadowObjects.Suspend, or the notification half. No row anywhere in the register covers it.

Correct behaviour: route the hook's sixth action at a RuntimeCollisionReportingState force-end entry point (the analogue of ForceEnd), and drop or separately justify the shadow suspension. If the force-end is deliberately deferred, it needs its own register row naming @0x00514F31 and @0x00514620.


R3 (MAJOR) — a teleported NPC now plans a RunForward cycle from a teleport-distance-derived velocity.

Where: src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs:2525-2543 then :2684-2709.

Newly reachable. In the pre-slice tree the shared if (remotePlacementRequired) block (HEAD:1958) sat above the NPC path and returned on every branch, so a teleport packet never reached the NPC synth-velocity code. This slice deletes that block, and a teleport-classified NPC packet now flows straight through it.

What it does. serverVelocity is update.Velocity; when ACE omits velocity (the ordinary UpdatePosition case) :2530-2532 synthesises (worldPos rmState.LastServerPos) / elapsed — across the whole teleport distance, over one packet interval. :2536-2537 installs it. At :2704 RemoteServerControlledVelocityCycle.Apply then calls ServerControlledLocomotion.PlanFromVelocity (src/AcDream.Core/Physics/ServerControlledLocomotion.cs:40-62), which returns MotionCommand.RunForward for anything above RunThreshold, and ae.Sequencer.SetCycle(style, RunForward, …) fires. None of the guards block it: rm.Airborne is false at a walkable destination; rm.MoveTo was just invalidated by the hook's own CancelMoveTo; a standing NPC's Ready passes CanApplyVelocityCycle. SpeedMod is clamped, the motion is not.

The cycle persists until the next packet or the 0.6 s ServerControlledVelocityStaleSeconds pass in RuntimeRemotePhysicsUpdater.cs:198-208.

Retail. The teleport branch writes no velocity at all (contract invariant 6, verified above), and teleport_hook's first action cancels the moveto. A retail observer sees the creature appear at the destination and stand. acdream will show it sprinting in place.

Impact is animation-onlyServerVelocity drives cycle selection, not translation (RuntimeRemotePhysicsUpdater.cs:188-193) — so this is not a body-motion defect. But "stands with correct animation" is a literal line in the contract's own two-client gate recipe, so the gate is expected to catch it if it is run against an NPC. It will not be caught against a player remote: RemoteServerControlledVelocityCycle.Apply:27-49 returns early for player GUIDs.

Correct behaviour: the teleport arm must not leave a teleport-derived ServerVelocity installed. The narrow fix is to skip the synth (and the cycle apply) for a teleport-classified packet, which is also what invariant 6's "the teleport arm must not add a velocity write" was reaching for; the register row for the AP-80 velocity-cycle adaptation should name the exclusion.


R4 (MINOR) — stale comment: the prologue rebucket no longer feeds the classifier's CommittedCellId.

LiveEntityNetworkUpdateController.cs:1848-1853 still reads: "it is the only site that moves an ordinary moving remote's draw bucket, commits its canonical FullCellId (which feeds back as the classifier's own CommittedCellId and as the ConstraintDistance cell key)…". After D1 the remote classifier's CommittedCellId is the pre-merge value threaded on AcceptedPhysicsTimestamps, never the rebucket's commit. The comment sits four lines below the classification call the slice rewrote and directly contradicts the fix; a reader who believes it would "simplify" D1 away. Correct it in the same commit (process rule 6). The ConstraintDistance half is still true.

R5 (MINOR) — the architecture docs still describe the deleted classes as live.

docs/architecture/acdream-architecture.md:314, 316, 469-471 and docs/architecture/code-structure.md:265, 267, 420-423 document RemoteTeleportController / RemoteTeleportPlacement as the live remote placement owners. CLAUDE.md: "When the architecture doc and reality diverge, update one or the other — never leave them out of sync." The deletion inventory covered every .cs wiring site but not these two docs.

R6 (MINOR) — the [remote-teleport] probe can report hookRan=True for a hook that did nothing.

RunRemoteTeleportHook (LiveEntityNetworkUpdateController.cs:234-256) re-resolves the RemoteMotion and EntityPhysicsHost by GUID, rather than using the rmState the arm already holds and hands to ApplyAcceptedRemoteTeleport. Every action is remote?. / host?., so on a null lookup all six silently no-op and RemoteTeleportHook.Execute still returns true — and PhysicsDiagnostics.LogRemoteTeleport prints hookRan=True. Since the probe exists specifically to satisfy process rule 5 ("a clean-looking live session is not a passed gate"), a probe that cannot distinguish "ran" from "no-opped" undercuts the gate it was added for. Passing rmState through (it is in scope at all three call sites) removes both the mismatch risk and the false-positive.

R7 (MINOR) — hookRan is consumed only by the probe. Correct, but say so.

ApplyRemoteContactRouting:183 captures the hook's currency result and the routing proceeds regardless. That is retail-faithful — retail has no currency concept and runs the hook before knowing the placement outcome — but the code reads as if a false were being dropped on the floor. One line of comment naming @0x005163EF's unconditional ordering would close it.

R8 (MINOR) — D1's null-widening has a second, undocumented producer.

The AP-137 rewrite names exactly one new null-producing reason: "the merge observed no PRIOR canonical record for this entity." There is a second: the dormant initial-residence enqueue path. TryApplyPosition returns at RuntimeEntityObjectLifetime.cs:1705-1721 (EnqueueDormant) before the :1767-1770 PreMergeCommittedCellId write, so those timestamps carry null — which the diff's own changed assertion in RuntimeInitialCreateResidenceStateTests (Assert.Null(...PreMergeCommittedCellId)) now pins. A TELEPORT_TS-advancing packet on that path previously classified SetPosition (via TeleportAdvanced, which is unaffected by the cell input) and now classifies nullUnroutedCatchUp. I could not determine from static reading whether the App's OnPosition reaches ClassifyRemoteAcceptedPosition for an enqueued packet — flagging as unverified rather than guessing. Either way the AP-137 row should name the producer the test now pins.

R9 (MINOR) — the D2 block's "AP-135's two writes" are the packet's second copy.

LiveEntityNetworkUpdateController.cs:2206-2209 writes LastServerPos / LastServerPosTime, but :2106-2107 already wrote both unconditionally for this packet (and rmState.CellId at :2076), with a second DateTime.UtcNow read producing a slightly later timestamp. Harmless, but the block's comment reads as though these are the AP-135 writes being preserved, when the preservation actually happened 100 lines earlier. Contract invariant 8 is satisfied on both arms (player: :2076 + :2106-2107; NPC: :2519-2521) — this is a clarity issue, not a correctness one.


Part 4 — what the diff gets right (verified, not assumed)

Recorded so a fix round does not disturb it:

  • D1 plumbing. Pre-merge cell measured by the same TryApplyPosition call that merged the packet, never re-read; null propagated honestly through both TryBuild and Build; route-1 semantics untouched (the old overload still passes canonical.FullCellId, RuntimeAcceptedPositionRouteRequests.cs:38-54). The discriminating test pair (CellLessRecord_… / CompanionTest_NonzeroPreMergeCellWithNoTeleportAdvance_…) genuinely distinguishes the fix from the shipped dead predicate.
  • D3 hook. RemoteTeleportHook.Execute preserves retail's six actions in retail's order with a currency re-check between each; WeenieError.ITeleported is 0x3C; EntityPhysicsHost.NotifyTeleported is the correct one-to-one for the ClearTarget + NotifyVoyeurOfEvent pair under retail's single guard.
  • D4 single arming site. The legacy pre-operation call is gone; the post-operation site is the only one, matching @0x00454272. Dropping the ConstrainAfterRouting route-flag check is behaviour-preserving — Interpolate and SetPositionSimple both carry ConstrainPhase.AfterPositionOperation (RuntimeAuthoritativePositionRouteClassifier.cs:473), and NoPositionOperation's None is already excluded by the arm mapping. Keying on the routing outcome rather than the raw classification is the right call: it is the only input that separates a grounded UnroutedCatchUp (arms, retail's nonzero analogue) from AirborneSnap (does not).
  • D5 ordering. The teleport dispatch is the first thing ApplyRemoteContactRouting does, ahead of the !remote.Body.InContact carve-out; on the player arm it precedes both the wire-airborne block and the landing block; on the NPC arm the sticky gate is widened (!snapSuppressedByStick || isTeleportRoute) rather than duplicated. All three match "decided before arg4". UnStick being the hook's second action is the right justification for the sticky widening.
  • D6/D7. TryApplyGenericRemoteRenderPose gate unchanged and now stated; TryAdoptWireCellAfterRouting suppression widened to the teleport arm; the re-entrancy guard covers both placement arms and sits before the arming on both, preserving the R5 invariant.
  • Invariant 1. ApplyAcceptedRemoteTeleport reuses the far arm's StoresAcceptedDestination partition and StoreAcceptedDestinationPose unchanged — the partition was not re-litigated.
  • Invariant 2. Both teleport tails sync the render entity from the resolved body and publish the shadow (:2171-2184 player, :2719-2732 NPC). The placement writes the resolved cell back through RuntimeSetPositionState.cs:5013 (remote.CellId = result.CellId), so entity.ParentCellId really is the resolved cell.
  • Invariant 4. The classifier's teleport branch carries StopInterpolating: false deliberately; the queue clear comes from the hook's Interp.Clear(), matching @0x00514EFD.
  • Invariant 10. No new production caller for TickLostCellDeadlines / TryDequeueExpiredLostCell; ArmLostFamilyDeadlines keeps its single pre-existing ParkDeferred call.
  • Register bookkeeping. AP-137 rewritten in place rather than deleted (correct — the contract overrides the handoff here); AD-42's dead citation removed; AP-136 and AP-138 updated to the two surviving non-Position rebucket writers; AP-138's Risk column gains the teleport arm; AP-135 untouched. All correct except the false sentence named in R1.

Part 5 — gate evidence not present

Stated, not held against the verdict:

  • The complete Release suite figure the contract's Gates section requires ("measure and record the new figure; do not inherit 11,027") is not recorded — the change is uncommitted and there is no commit message. I ran the build and a focused filter only.
  • The two-client connected teleport gate is user-run and cannot be evidenced by a reviewer. Note that per R3 an NPC target is the discriminating case: a player-remote teleport will not exercise the velocity-cycle path at all.
  • Contract test-plan items 10 (D2 shape) and 11 (AP-135 preservation on both airborne no-op paths) have no corresponding test in the diff. Item 10's absence is what left R1 invisible.

  1. R1 — give the NPC arm the same D2 return-0 shape, and make the AP-137 sentence true (or split the asymmetry into its own row). Add contract test 10 for both arms.
  2. R2 — route the hook's sixth action at the collision-reporting force-end, or file the delta on AP-137 with the @0x00514F31/@0x00514620 citations.
  3. R3 — exclude a teleport-classified packet from the NPC synth-velocity install and the cycle apply; note the exclusion on the AP-80 row.
  4. R4/R5 — the two stale-documentation fixes, same commit.
  5. R6-R9 — clarity and probe-honesty, at the implementer's discretion.