fix(review): close the C5b architecture-review findings (D2/D3/D4, L1-L5, S1)

Follow-up to C5b (735f0a72). The retail-conformance review passed, so no
production behaviour line moved: the flag truth table and the
refreshPosition:false withhold are untouched. This is blast radius, test
discrimination and documentation fidelity - plus two findings I could not
confirm and am rebutting rather than complying with.

D3 - THE PUBLISH-CONSERVATION TEST DID NOT DISCRIMINATE. The reviewer was
right and it was the worst finding here: proof obligation 3's test passed
identically with C5b reverted. Its only delta assertion FILTERED
(Assert.Single(deltas, Rebucketed && parentGuid)), so the pre-C5b stream
[Rebucketed] and the post-C5b stream [Updated, Rebucketed] both satisfied
it, and childSpatialBefore+1 held in both worlds because whichever site did
not move the cell propagated idempotently. It now asserts the complete
ordered parent stream plus each element's CellId and Position.ObjCellId.
Sabotage: restoring refreshPosition:acceptedPosition turns it red (it was
green before), together with the withhold test and the new L5 test.

That cardinality change was itself unfiled and is now AP-147: a
cell-changing accepted Position publishes TWO entity deltas where it
published one, and the intermediate Updated pairs the OLD CellId with the
NEW wire Position - a torn pair that did not exist pre-C5b, since both
halves used to move inside one publish. No production consumer reads a
delta's paired fields, but a recorder/plugin/bot event log would capture
it. The row states why suppressing the Updated is not available at that
layer (the merge cannot know whether its caller reaches W2).

D4 - THE PROJECTILE DOC COMMENT WAS FALSE AND ITS RETAIL ARGUMENT WAS
INVERTED. SyncPresentationFromResolvedBody claimed record.FullCellId is
"the WIRE cell ... stamped by the merge's RefreshDerivedState/SetFullCell,
before classification ever runs" and argued from retail's store_position
@0x00515CE2 that the destination cell is the right one. C5b falsified the
premise; the missile arm also returns before W2, so nothing stamps the wire
cell for a projectile at all. Rewritten. The honest conclusion, which the
old text would have called wrong: on a stored outcome presentation now
pairs the DESTINATION world position with the SOURCE cell. That is not a
choice this method can make differently - StoreAcceptedDestinationPose
writes only Position/Orientation, so record.FullCellId and
body.CellPosition.ObjCellId now hold the same source cell and reading
either yields the same value. The divergence is AP-138 item (1)'s
store-writes-pose-but-not-cell residual, retiring via #309, not a field
choice here. Projecting the wire cell instead would invent a residency the
placement declined - the AP-1 shape C5b closed.

L3/L4/L5 - PINNING GAPS, ALL THREE CONFIRMED AND CLOSED.
L3: the matrix's oracle passed HasAnimations as a literal, so the merge's
old.MotionTableId ?? old.Physics?.MotionTableId and
RuntimeAcceptedPositionRouteRequests.Build's canonical-snapshot twin were
textually identical and pinned by nothing. The oracle is now BUILT by the
production constructor.
L4: every fixture set both MotionTableId halves to the same value, so
deleting either operand of the ?? was undetectable while the production
comment said the mixed case is the real-world one. Six mixed rows added,
including the explicit-zero row (a present-but-zero top half is not null,
so ?? never reaches the physics half).
L5: the retained Rebucketed ternary had zero coverage through
TryApplyPosition - every restoreCancelledPark test called Forget directly.
Now driven through the real merge, with the wire cell deliberately the
SOURCE while the park's committed body cell is the DESTINATION, so the
restored residency can only have come from the rollback.
Sabotage (each red, each restored): merge ?? -> top half only, 1 red;
-> physics half only, 2 red; Build's ?? -> physics half only, 2 red;
ternary -> constant Updated, exactly the L5 test red.

L1/L2 - THE MISSING TEST IS ADDED; THE DEFECT IS NOT THERE. The reviewer
was right that C5b's "no fixture covers pickup at that layer" was
inaccurate - LiveEntityNetworkOnPositionCollapseMatrixTests drives the real
OnPosition at ~26 sites - and the end-to-end test is added: withdraw ->
accepted Position -> IsSpatiallyProjected && FullCellId == wireCell, both
guid classes.

But ChildUnparentDisposition.Pending is NOT a live defect, because it is
production-unreachable. The sole production _withdrawProjection binding
(LivePresentationComposition.cs:599) is
LiveEntityProjectionWithdrawalController.WithdrawExact, whose only Pending
mint is inside its catch block and therefore always carries a non-null
Failure - and AdvanceUnparentTransition rethrows at
EquippedChildRenderController.cs:1307 BEFORE the return Pending at :1309.
The named drop scenario does not reach it anyway (BeginDetachedRemoval has
already emptied the capture list) and would be correct if it did: a
previously-equipped child is LegacyImmediate, so the FullCellId != 0u gate
at DatLiveEntityProjectionMaterializer.cs:767 is never consulted and
re-projection uses the wire cell at LiveEntityRuntime.cs:824.

Measured while building that test, and NOT what C5b assumed: W2 and W3 are
REDUNDANT on the remote tail. Sabotaging W2 alone - adopting the committed
cell instead of the wire cell, OR skipping the rebucket outright - leaves
the whole file green, because W3's RemoteMotion.CellId write reads through
to canonical FullCellId via CommitCanonicalCell, whose CellCommitted
recovery re-installs the bucket. Only removing BOTH goes red, and then the
new test is the only red in the file. So it is named for what it pins, and
AD-60 is amended with the measurement: neither channel is individually
load-bearing, so a future retirement of one is caught by nothing else.

D2 - REBUTTED, WITH THE REAL GAP FILED INSTEAD. The reviewer's hypothesis
was that TryApplyInitialCreateCompletionPresentation's staleness guard lost
its ability to detect an intervening steady-state Position when C5b stopped
the merge stamping the wire cell, and asked for a PositionAuthorityVersion
term. I do not think that is right and did not add it.

The receipt's facts are the canonical BODY's pose and cell at publish
(PublishExecutorCompletion builds both from the record). Exactly two owners
can move them: a Runtime SetPosition commit/withdrawal, every one of which
calls AdvancePlacementCommit - the only caller family is
RuntimeSetPositionState - and a rebucket, which moves FullCellId. Both are
already covered by the two existing terms. An accepted steady-state
Position is neither, and C5b did not make it one: the merge refreshes the
snapshot and advances PositionAuthorityVersion but never wrote the body,
and the App generic tail writes the RENDER entity. The wire-cell half stays
covered because W2/W3 commit it in the same call; the paths that return
before them leave the record at the last committed cell, which IS the
receipt's own cell - correctly not a supersession.

Adding the term would decline receipts whose facts are still true, on the
entity's FIRST world-visible moment: the pose write and
RebucketLiveEntityPresentationOnly would be skipped while TryPublishPlace
still publishes, so a packet returning before the render write would leave
the sidecar visible at its materialized pose in a wrong bucket. That is the
handoff's own "removed the invariant failure while leaving the bug" shape.

There IS one supersession neither term covers, and it predates C5b:
RuntimeRemotePlacementDriveController.StoreAcceptedDestinationPose writes
body.Position/Orientation on the far-snap Refused/Contention arm with no
placement commit and no cell move. Filed as #323 with the FIFO-blocking
argument for why a receipt can still be pending when it lands, an explicit
"not established as reachable", and an explicit "do not fix it with
PositionAuthorityVersion". The guard's comment now carries the whole
argument instead of one sentence.

S1 - DANGLING POINTER CLOSED. InboundPhysicsStateController.cs:610 still
said the two-callers-one-rule debt was "tracked for the eventual cutover
unification ... See docs/ISSUES.md", which pointed at nothing after C5b
closed #275 without a successor. Filed #322, cited from both the comment
and #275's closure, including why widening TryApplyPosition's signature to
take a route would be the wrong unification.

AP-138 amended: C5b staled its round-3 measurement that "both
accepted-Position callers commit the accepted wire cell to
record.FullCellId before submitting". Route 2 submits from
TryExecuteAcceptedLocalPosition ahead of W2, so on a first submit
PlacementTouchesPrefix's CurrentCellId arm now names the SOURCE landblock,
not the destination. Confined to which prefix the quiescence pre-flight
matches, which that row already established is not the correctness
mechanism.

GATES. Release build 0 errors. Complete suite 11,134 passed / 4 skipped /
0 failed, from the 11,125 / 4 baseline at ed806997: net +9, all new tests,
no test deleted or weakened, no new skip. Runtime.Tests 1195 -> 1202 (+6
mixed-motion-table rows, +1 park-rollback fact); App.Tests 4132 -> 4134
(+2 guid rows). None of #302/#308/#321 appeared. Not connected-gated -
nothing here changes runtime behaviour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-05 22:08:46 +02:00
parent ed8069976c
commit 23aa62f292
9 changed files with 637 additions and 62 deletions

View file

@ -80,6 +80,97 @@ public sealed class LiveEntityNetworkOnPositionCollapseMatrixTests
private static bool IsPlayer(uint guid) => guid == PlayerGuid;
// ── Scenario 0: C5b's untested load-bearing claim ───────────────────
/// <summary>
/// C5b (#275) review, findings L1/L2. C5b made the steady-state merge stop
/// stamping the wire cell and justified it with a claim it never tested:
/// <i>"production installs the bucket at W2 in the same call (verified: no
/// return between the recovery call and W2 is conditioned on
/// IsSpatiallyProjected or FullCellId)"</i>. The commit also asserted "no
/// fixture covers pickup at that layer" — this file drives the real
/// <see cref="LiveEntityNetworkUpdateController.OnPosition"/> at ~26 call
/// sites, and every W2 assertion the commit added hand-calls
/// <c>RebucketLiveEntity</c>/<c>CommitRebucket</c> itself, so nothing
/// drove the sequence end-to-end. This does.
///
/// <para>The scenario is the leave-world/re-entry edge the production
/// comment at <c>LiveEntityNetworkUpdateController.cs:2073-2077</c> names:
/// the record keeps <c>WorldEntity</c> as its logical/render-resource
/// owner while <c>IsSpatiallyProjected</c> is false, and "a fresh retail
/// Position is the re-entry edge; testing only for the retained object
/// reference leaves dropped inventory permanently invisible after
/// InventoryPutObjectIn3D". The materializer DECLINES (the post-C5b
/// self-projection state — <c>DatLiveEntityProjectionMaterializer.cs:767</c>
/// now sees a canonical cell the merge did not advance), so the only
/// things that can restore the bucket and the cell are the two wire-cell
/// channels AD-60 says deliberately survive C5b.</para>
///
/// <para><b>Measured at the review, and NOT what C5b's commit message
/// assumed: W2 and W3 are REDUNDANT here.</b> Sabotaging W2 alone (both
/// "adopt the committed cell instead of the wire cell" and "skip the
/// rebucket entirely") leaves this test — and every other test in this
/// file — GREEN, because the post-routing wire-cell adopt
/// (<c>TryAdoptWireCellAfterRouting</c>, W3/AP-135) writes
/// <c>RemoteMotion.CellId</c>, which reads through to canonical
/// <c>FullCellId</c> via <c>CommitCanonicalCell</c>, whose graphical
/// <c>CellCommitted</c> recovery re-installs the bucket. Only removing
/// BOTH surviving channels turns this red — and it is then the ONLY red
/// in the file. So what this test pins is AD-60's actual surviving claim
/// ("two wire-cell writers deliberately remain downstream of the merge"),
/// not W2 in isolation. Naming it after W2 would have been a third
/// contract asserting a mechanism that is not the one doing the work.
/// </para>
///
/// <para>Both guid classes per this file's own discipline.</para>
/// </summary>
[Theory]
[InlineData(PlayerGuid)]
[InlineData(CreatureGuid)]
public void WithdrawnProjection_AcceptedPositionRestoresBucketAndWireCell(
uint guid)
{
using var fixture = new Fixture(guid, decliningMaterializer: true);
Assert.True(fixture.Runtime.TryGetRecord(guid, out LiveEntityRecord record));
Assert.True(record.IsSpatiallyProjected);
Assert.Equal(SourceCell, record.FullCellId);
// The withdrawal: render bucket gone, logical record + WorldEntity
// retained. This is what makes RequiresSpatialProjectionRecovery true
// on the next accepted Position.
Assert.True(fixture.Runtime.WithdrawLiveEntityProjection(guid));
Assert.False(record.IsSpatiallyProjected);
Assert.NotNull(record.WorldEntity);
Assert.Equal(SourceCell, record.FullCellId);
const uint WireCell = SourceLandblock | 0x0002u;
fixture.Controller.OnPosition(fixture.Update(
new Vector3(13f, 15f, SpawnHeight),
WireCell,
teleportSequence: 1,
guid: guid));
// The recovery branch really ran and really declined — without this
// the assertions below could pass on an entity that never needed
// recovering at all.
Assert.True(fixture.Lifetime.Entities.TryGetActive(
guid, out RuntimeEntityRecord canonical));
// The packet really was accepted and really did reach the recovery
// branch, which really did decline — without these the cell assertions
// below could pass on a rejected packet, or on an entity that never
// needed recovering at all.
Assert.Equal(
WireCell,
canonical.Snapshot.Position!.Value.LandblockId);
Assert.True(fixture.MaterializerDeclined);
// W2 is what closes both halves.
Assert.True(record.IsSpatiallyProjected);
Assert.Equal(WireCell, record.FullCellId);
Assert.Equal(WireCell, canonical.FullCellId);
Assert.Equal(WireCell, fixture.Entity.ParentCellId);
}
// ── Scenario 1: teleport commit ─────────────────────────────────────
[Theory]
@ -966,14 +1057,27 @@ public sealed class LiveEntityNetworkOnPositionCollapseMatrixTests
private readonly uint _guid;
private readonly bool _nullClassification;
/// <summary>
/// C5b review L1/L2: true once the hydration controller's materializer
/// was actually reached and DECLINED. Only meaningful when the fixture
/// was built with <c>decliningMaterializer: true</c>.
/// </summary>
internal bool MaterializerDeclined => _decliningMaterializer?.Declined
?? false;
private readonly DecliningMaterializer? _decliningMaterializer;
internal Fixture(
uint guid,
bool nullClassification = false,
bool withAnimation = false,
bool isMissile = false)
bool isMissile = false,
bool decliningMaterializer = false)
{
_guid = guid;
_nullClassification = nullClassification;
_decliningMaterializer =
decliningMaterializer ? new DecliningMaterializer() : null;
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
engine.AddLandblock(
SourceLandblock,
@ -1221,7 +1325,8 @@ public sealed class LiveEntityNetworkOnPositionCollapseMatrixTests
Runtime,
Lifetime,
new object(),
new NoopMaterializer(),
(ILiveEntityProjectionMaterializer?)_decliningMaterializer
?? new NoopMaterializer(),
new NoopRelationships(),
new NoopReadyPublisher(),
new AlwaysKnownOrigin(),
@ -1521,6 +1626,36 @@ public sealed class LiveEntityNetworkOnPositionCollapseMatrixTests
public void ForgetUnknownOwner(uint serverGuid) { }
}
/// <summary>
/// C5b review L1/L2: models the post-C5b
/// <c>DatLiveEntityProjectionMaterializer</c> self-projection branch
/// DECLINING (<c>DatLiveEntityProjectionMaterializer.cs:767-787</c>'s
/// <c>expectedCanonical.FullCellId != 0u</c> gate not being satisfied),
/// which is the state C5b's contract §14 item (1) says production now
/// reaches. Records that it was reached so the test cannot pass
/// vacuously, and returns <see langword="false"/> — the value
/// <c>OnPosition</c>'s recovery call site deliberately ignores.
/// </summary>
private sealed class DecliningMaterializer
: ILiveEntityProjectionMaterializer
{
internal bool Declined { get; private set; }
public bool TryMaterialize(
RuntimeEntityRecord expectedCanonical,
WorldSession.EntitySpawn canonicalSpawn,
LiveProjectionPurpose purpose,
ulong expectedCreateIntegrationVersion,
AcDream.App.Rendering.LiveEntityAppearanceUpdateState?
appearanceUpdate = null)
{
Declined = true;
return false;
}
public void ResetSessionState() => Declined = false;
}
private sealed class NoopMaterializer : ILiveEntityProjectionMaterializer
{
public bool TryMaterialize(