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

421 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`**
```csharp
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:
```csharp
if (!Entities.TryCommitParent(staged, acknowledgeProjection: null, out _)
|| !relations.CommitProjection(staged, _resolveParentInstance))
```
`CommitStagedParent`/`TryCommitParent` is the *canonical* half. It runs
`InboundPhysicsStateController.TryCommitParent``ApplyParent`
(`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 `PrepareAndTryRealize``Tick()` /
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 `default` → **the 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 `TryGetActive``TryGetSnapshot` (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.