acdream/docs/research/2026-08-05-issue-319-architecture-review.md
Erik 392c1e22c1 fix(physics): bind a parented child to the parent's live incarnation (#319)
A player-parented child never received a canonical cell. Its FullCellId stayed
0 for its whole attached lifetime, so it could not follow the player across a
boundary. Scope was wider than the local player: every REMOTE player's
equipment too.

ROOT CAUSE. EquippedChildRenderController hardcoded ParentInstanceSequence: 0
for a parented CreateObject. Correct for creatures and statics, which really
are sequence 0; wrong for players, whose ObjectInstance is Character.TotalLogins
(ACE Player_Networking.cs:37). The relation filed under (playerGuid, 0) while
the record carried TotalLogins, so both route-7 write sites — D1's attach
re-cell and D2's propagation lookup — keyed on an incarnation that never
matched. TryCommitParent did not validate the sequence, so the attach
succeeded and printed normally. Silent.

A ROUTE 7 REGRESSION (cd3129e9) that un-masked a latent bug: the TickChild call
route 7 deleted was keyed on the child guid alone and was structurally immune
to a wrong parent key.

THE FIX IS TO STOP TREATING PLAYERS DIFFERENTLY, not to special-case them.
Retail's attach path is guid-only end to end — PhysicsDesc::get_parent_id
@0x00558a18 -> CObjectMaint::GetObjectA @0x00558a2d -> set_parent @0x00558a3e,
with SetChildren @0x00509370 hash-walking by guid — and neither set_parent
overload (@0x00515A90, @0x00515B50) nor enter_cell @0x00510ED0 contains any
player test or instance-sequence read. Our player/non-player split was purely
an artifact of keying relations by (guid, incarnation) against a wire message
that carries no parent incarnation. Late-binding to whoever currently holds the
guid is retail's own semantics. Fixed at BOTH producers: OnSpawn and
OnCreateParentAccepted, the second carrying the byte-identical defect and not
named in the contract's scope line.

THE INVARIANT IS EQUALITY, NOT FRESHNESS. The contract rejected both framings I
offered: every one of the 45 FullCellId liveness predicates excludes a
committed child on a NON-cell clause first, so the child inherits only the
parent record's existing staleness, which is already present today with no
symptom. The key fix alone restores child-equals-parent for every parent class.

TWO SITES GATED, inert only because the cell was zero and would have woken
wrongly: the hydration candidate loop (a nonzero-cell child would take the
legacy RebucketLiveEntity -> CommitRebucket, a second canonical writer — route
7's exact defect class) and RestoreShadow (would install a broadphase row for
the weapon, the #184 shape, contradicting route 7's P4). Retail anchor:
update_object's parent != 0 early-out @0x00515D40 — children are never
independently re-placed.

THREE MAJORS WERE FIXED BY DELETION. The first pass added a deferral queue for
an unaddressable parent, carrying a missing child-freshness gate (A2), a
sentinel-0 collision with the generation filters (A3), and unbounded
accumulation (A5). Both reviewers then proved the deferred branch unreachable
for BOTH producers — RegisterEntityCore defers the entire CreateObject one
layer above, reading the same ?? chain, and CreateParentUpdate is produced only
inside AcceptCreateCore, after that gate passes. The machinery was deleted
rather than repaired, and the diff SHRANK to 76 added / 13 removed from 91/24
while gaining the A1 fix. Retail confirmed the deletion does not diverge:
acdream's real port of retail's per-guid replay (QueueBlobForObject) is a
different, untouched layer, and the deleted queue was a third redundant one
downstream of it.

THE GUARD MUST NOT TEAR WHAT IT PROTECTS. The first pass threw
InvalidOperationException AFTER the canonical half had committed, so the one
time it fired it left the child parented with no committed relation and a
staged one blocking Resolve — a torn transaction, the exact outcome the
contract pinned against. Now a pure CanCommitIncarnation precondition checked
BEFORE the commit at both sites, with a logged refusal instead of a throw.
Route 3's N3 principle (do not make a transient fatal on a host that must
survive 30 sessions x 2 hours) reinforces it, but the tearing argument stands
alone.

TEST QUALITY, the recurring lesson in its most refined form. The A1 test
initially passed sabotage FOR THE WRONG REASON: a mismatched ChildPositionSequence
meant TryCommitParent's own gate refused in either ordering, so the three
assertions carrying A1's meaning passed both ways and only an incidental
staging assertion failed. It failed on stranding, not tearing. Corrected, the
sabotage now names line 925 — Assert.Null(snapshot.ParentGuid), with the
parent's guid in it — proving the canonical mutation happened before the catch.
"Fails under sabotage" is necessary, not sufficient; WHICH assertion fails is
the real question.

The dual parent-class matrix (player 0x5… incarnation > 1 vs creature 0x8…
incarnation 0, identical outcomes, sabotage-verified in both directions) is the
structural fix for how this survived a full dual review and two connected
sessions: every prior test and both captured gate logs used sequence-0 parents.

Register: AP-142 clause (f); AP-132 amended to distinguish the two producers;
new row AP-146 for the local player's coarse canonical cell (retail writes it
per tick at SetPositionInternal @0x00515330 — which, per the retail review, ALSO
walks this->children writing each child's objcell_id @0x005153AE-@0x005153D8,
so retail's per-tick child propagation lives in the same function). That
divergence had no row at all, a standing rule-1 violation now corrected.
Follow-up #320 filed for making the player's cell track ordinary movement —
deliberately excluded here: it touches the landblock-preserve contract, the
Rebucketed cadence, route-2/4b-3 classification inputs AP-136/AP-138 spent four
review rounds pinning, and the portal-space frozen-source-cell race.

Two dual review rounds; 6 architecture MAJORs and 2 retail MAJORs closed.
Diagnostic refusals are latched per child guid and the latch clears on
Clear()/RemoveChild, so a recycled guid's next incarnation still logs rather
than being silently suppressed.

Complete Release suite MEASURED at 11,112 passed / 4 skipped / 0 failed
(baseline 11,090 at 52175aa1, +22). Neither known flake fired.

STILL OWED: the connected gate, with the CORRECTED positive criterion — assert
the equipped child's FullCellId EQUALS the parent's after a crossing (a zero is
a failure, not a silence), run with BOTH a player and a creature parent, plus
the new step carrying an armed creature across a landblock unload/reload.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 11:56:31 +02:00

23 KiB
Raw Permalink Blame History

Issue #319 — independent architecture / adversarial review (2026-08-05)

Verdict: FAIL.

Reviewed: the uncommitted working tree at branch claude/acdream-physics-divergence-5aa784, HEAD af828a8a (git diff HEAD plus the untracked contract doc). Production delta measured independently: 91 non-comment lines added, 24 removed across five files — the implementer's claim 3 is exact.

Verification performed for this review (not inherited):

  • dotnet build -c Release — green (exit 0).
  • AcDream.Runtime.Tests filtered to ParentAttachmentStateTests + RuntimeEntityChildCellPropagationTests — 32/32 pass.
  • AcDream.App.Tests filtered to EquippedChildProjectionWithdrawalTests + LiveEntityHydrationControllerTests + LiveEntityPresentationControllerTests
    • LiveEntityRuntimeTests — 206/206 pass.

The key fix (F1's staged half) is correct, well-anchored, and its tests are genuinely sabotage-sensitive. The bookkeeping (AP-142 clause (f), AP-132 clarification, new AP-146, issue #320, route-7 §7 supersession note) is the most thorough in the campaign so far. The FAIL rests on two things: the commit-time tripwire is wired at the wrong point in the transaction so that when it fires it tears the commit it was built to protect (A1), and the deferred late-bind branch — the larger and more novel half of F1 — is production-unreachable, carries a freshness hole its ParentEvent sibling does not, and its one test reaches it only by bypassing the production seam under an incorrect stated rationale (A2, A3, A6).


A1 — MAJOR — the tripwire throws after the canonical parent commit has already landed; when it fires it produces a torn transaction, which is the one outcome F1 pinned against

src/AcDream.App/Rendering/EquippedChildRenderController.cs:955-963

if (candidateKind is ParentProjectionCandidateKind.Staged)
{
    if (!_liveEntities.CommitStagedParent(relation, out _)      // canonical commit
        || !Relations.CommitProjection(relation, ResolveLiveParentInstance))  // throws here
    {
        return default;
    }

src/AcDream.Runtime/Session/RuntimeLiveEntitySessionController.cs:408-412 — identical ordering:

if (!Entities.TryCommitParent(staged, acknowledgeProjection: null, out _)
    || !relations.CommitProjection(staged, _resolveParentInstance))

CommitStagedParent/TryCommitParent is the canonical half. It runs InboundPhysicsStateController.TryCommitParentApplyParent (InboundPhysicsStateController.cs:1347-1373), which nulls the child's snapshot Position, writes ParentGuid/ParentLocation/PlacementId, and stamps POSITION_TS. Only then does CommitProjection (ParentAttachmentState.cs:651-661) evaluate the tripwire and throw.

Failure scenario. Any input that reaches the tripwire with a mismatch: the child's canonical snapshot has already been rewritten as parented (its world Position destroyed, POSITION_TS advanced), _lastAcceptedByChild has not been written, and the relation is still sitting in _stagedByChild. The exception then unwinds through PrepareAndTryRealizeTick() / the inbound sink. Net state: a child that the canonical layer believes is parented, with no committed relation for D1/D2 to find, and a staged relation that Resolve's early return (ParentAttachmentState.cs:457-458) will now block forever for that child.

That is strictly worse than the silent mis-keyed commit #319 shipped. The contract's F1 pinned outcome was "never a silent success under a mismatched key"; the XML doc on CommitProjection promises it "refuses loudly rather than silently filing the relation under a key D1/D2 can never find again." Neither is what the code does — it half-files, then throws.

On reachability, stated honestly. I traced every path that can advance a parent's live InstanceSequence while a staged relation naming that parent survives, and found none reachable today:

  • Both producers set the sequence from the live snapshot at stage time (EquippedChildRenderController.cs:845-857; ParentAttachmentState.Resolve :473-498).
  • The only incarnation bump is a NewGeneration CreateObject, and RuntimeEntityObjectLifetime.cs:1004-1010 calls ParentAttachments.EndGeneration(...)RemoveParentReferences(_stagedByChild, guid) (ParentAttachmentState.cs:825), purging every staged relation naming that parent.
  • Delete removes both the gate and the snapshot (InboundPhysicsStateController.cs:101-102) and is immediately followed by DeleteGeneration (RuntimeEntityObjectLifetime.cs:2088-2090), which purges the same way — so after a delete the producers defer rather than stage a stale key, and the next create is InitialGeneration (no EndGeneration needed).

So the mismatch is unreachable — but only as an emergent property of a three-file invariant chain that nothing records, and one link is order-fragile: AcceptCreate publishes the new incarnation into _snapshots before RegisterEntityCore reaches EndGeneration, with RemoveActive and WithdrawCommittedChildrenToCellless (which publishes deltas to synchronous App observers) executing inside that window.

This is exactly route 3's N3 shape (docs/research/2026-08-04-c4-route-3-retail-review-round2.md:346-362): "Refusing to fake success is right; converting a possibly-transient condition into a process-killing throw on the host that must survive K4's 30-session / two-hour endurance profile is the wrong end of that trade." The tripwire's own comment asserts a diagnosis it cannot establish — "a mismatch here can only be a producer regression" — when a parent replacement whose purge ordering ever changed would produce the same mismatch. On the headless site the throw lands in OnParentUpdated, an inbound sink; HeadlessSessionHost.cs:52/64 will quarantine it, i.e. one bot session of 30 dies rather than the process — still a lost endurance row, and the torn canonical state above is what it dies holding.

Fix direction (cheap, no redesign). Move the incarnation check above CommitStagedParent/TryCommitParent at both sites and make it a refusal, not a throw: log loudly (the codebase's existing refusal idiom) and return false, so the staged relation is rejected (RejectProjection) rather than left to block Resolve. That satisfies F1's pinned outcome exactly — no silent success under a mismatched key — without a fatal, and without tearing the transaction. If the project wants a hard assertion, it belongs in a Debug.Assert/test-only seam, not on the live packet path.


A2 — MAJOR — a deferred late-bind relation skips accept, so it carries no child-freshness gate at all, and is retained across the child's own generation boundary

src/AcDream.Runtime/Entities/ParentAttachmentState.cs:506-524 (the if (!lateBind) wrapper around the accept(update) call) and :412-426 (DeferCreateObjectRelation enqueues with WaitOwner = Parent).

For every ParentEvent-sourced relation, accept is TryApplyParent/TryAcceptParentForProjection → the child's PhysicsTimestampGate.TryAcceptPositionChannelEvent (InboundPhysicsStateController.cs:258-273). That call does two things: it gates the relation on the child's POSITION_TS, and it advances the child's gate to update.ChildPositionSequence — which is precisely what makes the subsequent TryCommitParent gate (InboundPhysicsStateController.cs:299-312: gate.PositionTimestamp != positionSequence || child.PositionSequence != positionSequence) satisfiable.

A late-bind relation skips both. Its only remaining gate is that same TryCommitParent equality — against a ChildPositionSequence captured when the relation was enqueued, possibly many packets earlier.

Compounding it: DeleteGeneration and EndGeneration both run FilterChildCandidates(guid, relation => relation.WaitOwner is ParentAttachmentWaitOwner.Parent) (:819-821, :844-846) — i.e. they retain relations whose WaitOwner is Parent. DeferCreateObjectRelation sets WaitOwner = Parent at enqueue, and Resolve's re-queue would set it anyway, so a deferred late-bind relation survives its own child's delete or replacement.

Failure scenario. Child C's CreateObject/CreateParentUpdate defers a late-bind relation naming parent P (P unaddressable). C is then replaced by a new generation (EndGeneration(C, n+1) — an ordinary re-observe shape) or deleted and its GUID recycled. The relation survives. P later becomes addressable → RetryWaitingDescendants(P)Resolve(C, …) → the late-bind branch adopts P's current incarnation and stages, with no accept gate to reject the stale ChildPositionSequence. Two outcomes, both bad:

  • TryCommitParent's POSITION_TS equality fails (the new child generation has a different stamp) → CommitStagedParent returns false → PrepareAndTryRealize returns defaultthe relation is stranded in _stagedByChild permanently. Resolve's early return (:457-458) then blocks every subsequent legitimate parent relation for that child for the rest of the session, and CopyPendingProjectionChildrenTo retries it every frame forever.
  • Or the stamps happen to coincide and the attach commits — binding a dead wire event's relation onto a new child incarnation and whatever object now holds P's GUID. This is AP-132's documented GUID-reuse hazard, applied on the one producer that now has no gate at all.

Note the pre-fix behaviour self-healed: AcceptCreateObjectRelation (:391-394) is an unconditional _stagedByChild[child] = relation assignment, so a newer relation always superseded a stuck one. Routing through the queue removes that self-healing property.

Reachability caveat, stated honestly: see A6 — the deferred branch appears to be production-unreachable today, so this is latent, not live. It is still a MAJOR defect in newly added code, because the code's stated purpose is to be the fail-safe.

Fix direction. Either (a) subject late-bind relations to the same child POSITION_TS gate (call accept and let it advance the stamp — the retail argument for skipping it is about the parent incarnation, not the child timestamp), or (b) if the branch stays as a pure fail-safe, do not enqueue it with WaitOwner = Parent and add an explicit child-incarnation field so the generation filters can drop it with its child.


A3 — MEDIUM — the deferred relation carries a placeholder ParentInstanceSequence = 0 that the generation filters read as a wire-named incarnation

src/AcDream.App/Rendering/EquippedChildRenderController.cs:838-844 builds the relation with the literal ParentInstanceSequence: 0 and hands that exact value to DeferCreateObjectRelation when the parent is unknown. The LateBindParentInstance flag records "this 0 is meaningless" — but only Resolve reads the flag.

EndGeneration (ParentAttachmentState.cs:828-833) and DeleteGeneration (:853-857) both filter the unresolved queue with predicates that compare relation.ParentInstanceSequence against a real generation via PhysicsTimestampGate.IsNewer. For a late-bind relation with the placeholder 0 and a player parent (TotalLogins ≥ 1, never 0):

  • EndGeneration(P, 8): 0 == 8 false; IsNewer(8, 0) false → dropped.
  • DeleteGeneration(P, 7): IsNewer(7, 0) false → dropped.

Failure scenario. A late-bind relation whose semantic is "attach to whatever currently holds this GUID" is judged as "names generation 0, which is older than the replacement" and silently discarded at exactly the moment the replacement it should bind to arrives. Player-class only — #319's own failure signature, reintroduced in a narrower window.

Not reachable today only because a NewGeneration disposition requires a pre-existing snapshot, and a pre-existing snapshot means the producer would have taken the staged branch rather than deferring. That coupling is undocumented and is not a property either file states.

Fix direction. Make FilterParentCandidates's callers retain LateBindParentInstance relations unconditionally (they name no generation to compare), or carry a sentinel the filters can recognise.


A4 — MEDIUM — the RestoreShadow gate is not behaviour-preserving at HEAD; contract §3.2's premise is wrong for creature-parented children

src/AcDream.App/World/LiveEntityPresentationController.cs:229

The contract argues (§3.2) that RestoreShadow "no-ops today on record.FullCellId == 0", so the new HasCommittedParent clause is inert at HEAD and only matters post-fix. That premise holds only for player-parented children. Route 7's D1 already re-cells creature-parented children to a nonzero cell — that is exactly the class route 7 shipped and gated. So today an NPC's wielded weapon that has a shadow registration and crosses a Hidden→Visible edge reaches ShadowPositionSynchronizer.Sync and gets its broadphase row refreshed; with this gate it no longer does.

The change is in the right direction — route 7's P4 record (RuntimeEntityDirectory.cs:451-465) says a committed child never owns a broadphase row, and ShadowObjectRegistry.UpdatePosition (ShadowObjectRegistry.cs:696-697) no-ops for an unregistered entity, so the live population is bounded by "children that already carry a registration." But this is a live behaviour change on a shipped path, justified in the contract by a premise that does not hold, and route 7's connected gate never exercised it.

Failure scenario if the premise is wrong in the other direction: a formerly-world object that is picked up and equipped keeps a suspended registration in _shadows that RestoreShadow will now never restore, while _suspendedShadowOwners keeps its key (:203 is only reached when restored is true). Forget/Clear (:111-127) converge it at teardown, so this is bounded — but it is a state the contract did not analyse because it assumed the population was empty.

Fix direction. Not a code change necessarily — but the creature-parented half must be named in the connected gate (§7 Half B) as a thing to watch, and the contract's §3.2 "correct today" claim corrected. The test CommittedChild_HiddenThenVisible_NeverInstallsShadowRow does assert against the real ShadowObjectRegistry (the fixture pre-registers the entity at LiveEntityPresentationControllerTests.cs:700-710), so the AP-145/#318 rule is honoured — good.


A5 — MEDIUM — unbounded accumulation of deferred late-bind relations (the un-written §6 test 9 would have caught this)

_unresolvedByChild is removed for a child only by RemoveChild/RemoveObject (ParentAttachmentState.cs:782, :878) or by Clear() (:890), and per parent guid by RemoveObject(parentGuid) (:788-798) and the two generation filters. A late-bind relation naming a parent GUID that never becomes addressable and is never itself deleted (an equip whose holder stays outside visibility) is removed by none of them and is retained across the child's own delete by the WaitOwner is Parent rule (A2).

Each such relation is a permanent +1 on PendingRelationCount and a permanent per-parent-spawn scan cost in ChildrenWaitingForParent (:715-719, a queue.Any(lambda) per child). Over a two-hour, 30-session endurance run this accumulates monotonically. Clear() converges it at reset, so a teardown-only ledger assertion would pass — which is exactly why the missing §6 test 9 is not the whole answer here.

Fix direction. Bound the unresolved queue per child (the existing Enqueue has the same shape, so this is a pre-existing class the new producer widens), or age deferred late-bind relations out.


A6 — MEDIUM — the deferred branch is production-unreachable for both producers, and its only test reaches it by bypassing the production seam under an incorrect stated rationale

Implementer claim 1 is verified for the raw CreateObject path: RuntimeEntityObjectLifetime.cs:797-812 defers the whole CreateObject when beginInitialResidence && parentGuid != 0u && !Entities.TryGetActive(parentGuid, out _), and TryGetActiveTryGetSnapshot (the snapshot is written by AcceptCreate before AddActive). So OnSpawn never sees an unaddressable parent.

But the same gate covers CreateParentUpdate. The test's own justification (EquippedChildProjectionWithdrawalTests.cs, the doc comment on OnCreateParentAccepted_ParentNotYetKnown_DefersThenLateBindsOnArrival) says CreateParentUpdate's producer "has no equivalent parent-addressability precondition." That is wrong: CreateParentUpdate is manufactured by BuildSameGenerationEvents inside the very RegisterEntityCore call whose line-797 gate reads incoming.ParentGuid ?? incoming.Physics?.Parent?.Guid — a same-generation CreateObject carrying a Parent hits the identical deferral. The test reaches the deferred branch only because it calls fixture.Live.TryApplyCreateParent(...) and fixture.Controller.OnCreateParentAccepted(...) directly, below the routing layer that would have prevented it.

So DeferCreateObjectRelation, the LateBindParentInstance field, and Resolve's late-bind branch — roughly half of F1's added lines and all of its novel state — are dead in production, and they carry A2's freshness hole and A3's placeholder-sequence hazard. A fail-safe that resurrects relations across generation boundaries with no timestamp gate is not a safer state than the assertion it replaces.

Fix direction. Either establish a real production path (and then test it through the production seam), or reduce the branch to something with no independent failure modes — e.g. refuse the relation outright with a loud log when the parent is unaddressable, which is what the layer above already guarantees cannot happen.


A7 — LOW — the hydration gate is correctly scoped

src/AcDream.App/World/LiveEntityHydrationController.cs:561-562 — verified behaviour-preserving at HEAD and necessary post-fix, on stronger grounds than the contract gave:

  • LiveEntityRecord.ProjectionCellId (LiveEntityRuntime.cs:387-389) is WorldEntity is not null ? FullCellId : Snapshot.Position?.LandblockId ?? FullCellId. A committed child always has a null snapshot Position (ApplyParent nulls it, InboundPhysicsStateController.cs:1358/1366), so all three sources of projectionCellId collapse to FullCellId — 0 today, the parent's cell post-fix. The gate is therefore exactly a no-op at HEAD for both parent classes and exactly required after.
  • The continue skips all three downstream branches (CreateSupersessionRecovery, RebucketLiveEntity, SpatialRecovery), which is correct: none of them was reachable for this population before, so nothing legitimate is newly dropped.
  • The predicate matches the precedent it cites (RuntimeSetPositionState.IsAffectedCollisionResident), and stale committed entries for a replaced child are cleared by RemoveCommittedChild inside both generation paths.

No finding beyond noting the gate is GUID-keyed while the loop iterates canonical records; that is safe today only because _lastAcceptedByChild is purged on every child generation change.


A8 — LOW — AcceptCreateObjectRelation remains a public, unguarded producer

ParentAttachmentState.cs:391-394 still accepts any ParentInstanceSequence and is called raw from twelve test sites. The tripwire is the only thing standing between a future caller and #319 verbatim — and per A1 that tripwire is optional (resolveParentInstance defaults to null) and fires after the canonical commit. Consider making the parameter required, or making the late-bound wrapper the only public entry.


Implementer claims — adjudicated

claim verdict
1. OnSpawn's deferred branch is structurally unreachable in production VERIFIED (RuntimeEntityObjectLifetime.cs:797-812). But it is unreachable for the CreateParentUpdate producer too, which the diff and its test both deny — see A6. Not acceptable as an untested fail-safe in its current shape.
2. Sabotage: restoring the literal 0 fails the player row via the tripwire before the value assertion CONCERN REFUTED. With the tripwire removed and the literal 0 restored, CommitProjection returns true and files (parentGuid, 0), so Assert.Equal(parentIncarnation, committedInstance) fails on its own; the D1 assertion (childCanonical.FullCellId == parent.Canonical.FullCellId) and the D2 assertion also fail independently, because CommitAcceptedParentCellless's parent.Incarnation == parentInstanceSequence gate and ChildrenAttachedToParent(guid, Incarnation) both miss. The matrix proves what it claims.
3. 91 added / 24 removed, net +67 vs a 3580 estimate VERIFIED exactly. The overshoot is entirely the tripwire plumbing plus the second commit site — i.e. the two things A1 says should be restructured.

§6 gaps — do they block?

  • Test 5 (residence non-event)does not block. Contract §3.3's premise is verified at RuntimeEntityObjectLifetime.cs:2751-2752 (if (canonical.FullCellId != 0u) Entities.SetFullCell(canonical, 0u, 0u) immediately before InitialCreateResidences.Begin), so Begin's FullCellId != 0 refusal is genuinely unreachable and no production code on that path changed.
  • Test 6 (unwield classification population)does not block. No production code on the classifier path changed, and the contract already recorded (af828a8a) that the short-circuit OR at RuntimeAuthoritativePositionRouteClassifier.cs:391 labels the drop teleport-ts either way, so the observable outcome is unchanged.
  • Test 9 (ledger convergence)BLOCKS. It is the one missing test whose subject is exactly where the new state lives, and A2/A5 are precisely ledger-convergence defects: a late-bind relation that outlives its child's generation, blocks Resolve for that child forever, and accumulates in _unresolvedByChild with no bound. A convergence test written against the new deferred population (child deleted with a deferred relation pending; parent guid never arriving; teardown and mid-session counts) would have surfaced both.

Route 7 invariants

Re-checked against the diff and found intact: removal propagates ZERO (untouched; a new dual-parent-class withdrawal test was added); RebucketLiveEntityPresentationOnly is not made a canonical writer (the D4 demotion stands, LiveEntityRuntime.cs:1033-1060 untouched); no per-tick cross-cell rebuild is introduced; no new SetFullCell call site exists; the ParentEvent path's AP-132 incarnation gating is preserved verbatim in Resolve's else if arm.

What a PASS would need

  1. A1 — move the incarnation check above the canonical commit at both sites and make it a logged refusal returning false (rejecting the staged relation), not a throw.
  2. A2/A3/A6 — either give the deferred branch a real production path and a test that reaches it through production routing, or reduce it to a loud refusal with no independent state. If it stays, close the missing child freshness gate and the placeholder-sequence filter hazard.
  3. Test 9 — a ledger-convergence test over the new deferred population, asserting both mid-session boundedness and teardown convergence.
  4. A4 — correct the contract's §3.2 premise and name the creature-parented shadow behaviour change in the connected gate's Half B watch list.

Nothing here requires redesign; items 1 and 3 are small, item 2 is a scoping decision.