feat(runtime): public initial-Create completion surface for hosts

Cutover slice C3-1 (the C3 flip's Runtime prerequisite, landed separately
after the flip itself was halted with structural findings — see the plan's
C3a/b/c decomposition). Hosts can now read the executor-completion facts
they must bind at cutover through one public, generation-gated channel
accessor: RuntimePlacementProjectionChannel.TryGetInitialCreateCompletion
returns RuntimeInitialCreatePlacementCompletion — the teleport-hook phase,
resident cell, replay outcomes, and per-Position route facts (disposition,
constrain phase, hook phase, stop-interpolation/zero-velocity/preserve-
heading/send-position flags) via public 1:1 mirror enums of the internal
classifier vocabulary. The projection is built once at completion, cached
in the same reaped entry as the internal receipt (identical acknowledge/
discard/clear lifecycle, ledger-covered), and read allocation-free.
Mirror maps enumerate every value explicitly with throwing catch-alls,
guarded by a sabotage-verified arity/round-trip reflection test. Doc
comments pin the two consumption rules: unparent/placement-frame are
already applied to the canonical snapshot (hosts must not re-apply), and
array order — not Sequence — is the authoritative Position-fact ordering.

Reviewed: architecture PASS + retail-conformance PASS (mirrors verified
member-for-member against the retail phase semantics; the route-fact
selection confirmed to cover exactly the host-bindable deferrals).
Runtime 932/932; complete Release solution 10,727 passed / 4 skips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-02 08:17:12 +02:00
parent a32aba35d1
commit fe02c4f56d
4 changed files with 588 additions and 9 deletions

View file

@ -174,7 +174,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
Placements = new RuntimePlacementProjectionChannel(
Events,
Physics.SetPosition);
Physics.SetPosition,
InitialCreateExecution);
}
internal RuntimeEntityObjectLifetime(
@ -226,7 +227,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
Placements = new RuntimePlacementProjectionChannel(
Events,
Physics.SetPosition);
Physics.SetPosition,
InitialCreateExecution);
}
internal RuntimeEntityObjectLifetime(
@ -278,7 +280,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
Placements = new RuntimePlacementProjectionChannel(
Events,
Physics.SetPosition);
Physics.SetPosition,
InitialCreateExecution);
}
public RuntimeEntityDirectory Entities { get; }

View file

@ -194,6 +194,99 @@ internal readonly record struct RuntimeInitialCreateExecutionReceipt(
ImmutableArray<RuntimeInitialCreateExecutedAction> Trace,
int ReplayedDeferredChildCount);
/// <summary>
/// C3-1: public projection of <see cref="RuntimeTeleportHookPhase"/>. A
/// separate public enum (rather than widening the internal one's
/// accessibility) keeps the classifier/executor's internal vocabulary free to
/// evolve without becoming a host-facing contract; values map 1:1 today.
/// </summary>
public enum RuntimeInitialCreateTeleportHookPhase : byte
{
None,
BeforePositionOperation,
AfterPositionOperation,
AfterEnterWorld,
}
/// <summary>C3-1: public projection of <see cref="RuntimeAuthoritativePositionDisposition"/>.</summary>
public enum RuntimeInitialCreatePositionDisposition : byte
{
RejectedAuthority,
RejectedData,
AwaitFreshPosition,
NoPositionOperation,
Interpolate,
SetPosition,
SetPositionSimple,
}
/// <summary>C3-1: public projection of <see cref="RuntimePositionConstrainPhase"/>.</summary>
public enum RuntimeInitialCreatePositionConstrainPhase : byte
{
None,
BeforePositionOperation,
AfterPositionOperation,
}
/// <summary>
/// C3-1: one Position continuation's route facts from the executor's trace,
/// projected to a public shape so a host can bind constrain/interpolation
/// presentation (retail's <c>ConstrainTo</c> placement, stop-interpolate,
/// zero-velocity, preserve-heading, send-position-immediately) without
/// reaching into internal Runtime route-classifier types.
/// Review addendum (2026-08-02): the route's <c>UnparentBeforeRouting</c>
/// ("unset_parent") and <c>ApplyPlacementFrameBeforeRouting</c>
/// ("SetPlacementFrame") facts are deliberately NOT projected here - the
/// executor's own merge (<c>ApplyAcceptedPositionSnapshot</c>'s
/// <c>clearParent</c>/<c>installPlacementFrame</c> parameters) already
/// applies both directly to the canonical snapshot, synchronously, before
/// the trace entry carrying this fact is even built. A host reading this
/// record must NOT re-apply either one - the facts this type DOES carry
/// (<see cref="ConstrainPhase"/>, <see cref="StopInterpolating"/>,
/// <see cref="ZeroVelocity"/>, <see cref="PreserveHeading"/>,
/// <see cref="SendPositionImmediately"/>, <see cref="HookPhase"/>) are
/// exactly the bindings still DEFERRED to the host at presentation time;
/// everything already-applied is intentionally excluded.
/// </summary>
public readonly record struct RuntimeInitialCreatePositionRouteFact(
ulong Sequence,
RuntimeInitialCreatePositionDisposition Disposition,
RuntimeInitialCreateTeleportHookPhase HookPhase,
RuntimeInitialCreatePositionConstrainPhase ConstrainPhase,
bool StopInterpolating,
bool ZeroVelocity,
bool PreserveHeading,
bool SendPositionImmediately);
/// <summary>
/// C3-1: the public host consumption shape for one executor drain's
/// completion, reached via
/// <see cref="RuntimePlacementProjectionChannel.TryGetInitialCreateCompletion"/>
/// using the correlated <see cref="RuntimePlacementProjectionKind.ExecutorCompleted"/>
/// receipt's own Entity/Sequence identity. Built exactly once, at completion
/// time, and cached alongside the internal receipt it projects (see
/// <see cref="RuntimeInitialCreateContinuationExecutor.ProjectCompletion"/>) -
/// a host retrying <c>TryGetInitialCreateCompletion</c> across multiple polls
/// never triggers a second allocation.
/// Review addendum (2026-08-02): <see cref="PositionRouteFacts"/>'s ARRAY
/// ORDER is the authoritative ordering, not <see cref="RuntimeInitialCreatePositionRouteFact.Sequence"/>
/// alone - Position facts drained from the SAME same-incarnation envelope
/// (<see cref="RuntimeInitialCreateExecutedAction.Stage"/> distinguishes
/// them internally, a field this projection does not carry) share one
/// continuation <c>Sequence</c>, so two entries can legitimately have equal
/// <c>Sequence</c> values. <see cref="RuntimeInitialCreateContinuationExecutor.ProjectCompletion"/>
/// walks the executor's trace strictly in construction order (itself the
/// exact FIFO drain order) and appends without reordering or deduplicating,
/// so array index - never a sort or group-by on <c>Sequence</c> - is the
/// only reliable way to recover drain order from this array.
/// </summary>
public readonly record struct RuntimeInitialCreatePlacementCompletion(
RuntimeEntityKey Entity,
uint FullCellId,
RuntimeInitialCreateTeleportHookPhase TeleportHookPhase,
ImmutableArray<RuntimeInitialCreatePositionRouteFact> PositionRouteFacts,
int ReplayedDeferredChildCount);
/// <summary>
/// Applies one entity's completed initial-Create residence: adopts the
/// initial placement exactly once, emits the AfterEnterWorld teleport-hook
@ -285,10 +378,16 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
/// reentrancy guard), so only the most recent completion for a key is
/// ever meaningful; the exact-sequence check in
/// <see cref="TryGetCompletionReceipt"/> rejects a stale lookup against a
/// superseded completion under a reused key.
/// superseded completion under a reused key. C3-1: the tuple's third
/// slot is the SAME receipt already projected once to the public
/// <see cref="RuntimeInitialCreatePlacementCompletion"/> shape (see
/// <see cref="ProjectCompletion"/>) - stored here, not recomputed per
/// read, so a host polling <c>TryGetInitialCreateCompletion</c> across
/// retries never allocates a second time for the same completion.
/// </summary>
private readonly Dictionary<RuntimeEntityKey,
(ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt)>
(ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt,
RuntimeInitialCreatePlacementCompletion Public)>
_completionReceipts = [];
private Func<RuntimeGenerationToken>? _generation;
private Func<bool>? _usePositionFromServer;
@ -438,7 +537,8 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
{
if (_completionReceipts.TryGetValue(
token.Entity,
out (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt) entry)
out (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt,
RuntimeInitialCreatePlacementCompletion Public) entry)
&& entry.Sequence == token.Sequence)
{
receipt = entry.Receipt;
@ -448,6 +548,185 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
return false;
}
/// <summary>
/// C3-1: the public host consumption surface for
/// <see cref="TryGetCompletionReceipt"/> - same exact-sequence
/// correlation rule, but returns the cached public projection instead of
/// the internal receipt/trace. Reached via
/// <see cref="RuntimePlacementProjectionChannel.TryGetInitialCreateCompletion"/>.
/// </summary>
internal bool TryGetCompletion(
in RuntimePlacementProjectionToken token,
out RuntimeInitialCreatePlacementCompletion completion)
{
if (_completionReceipts.TryGetValue(
token.Entity,
out (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt,
RuntimeInitialCreatePlacementCompletion Public) entry)
&& entry.Sequence == token.Sequence)
{
completion = entry.Public;
return true;
}
completion = default;
return false;
}
/// <summary>
/// Review fix (2026-08-02, architecture pass): every arm is now listed
/// explicitly and the catch-all throws instead of silently folding an
/// unmapped future internal value into <c>None</c>. A value this method
/// cannot map must never reach a host disguised as "nothing to bind" -
/// that would silently drop presentation behavior (e.g. a real
/// teleport-hook phase host code never runs). See
/// <see cref="RuntimeInitialCreateContinuationExecutorTests"/>'s
/// reflection-based completeness test, which walks every declared
/// <see cref="RuntimeTeleportHookPhase"/> value through this exact
/// method and fails if a new internal value is ever added without a
/// matching arm here.
/// </summary>
private static RuntimeInitialCreateTeleportHookPhase MapHookPhase(
RuntimeTeleportHookPhase phase) => phase switch
{
RuntimeTeleportHookPhase.None =>
RuntimeInitialCreateTeleportHookPhase.None,
RuntimeTeleportHookPhase.BeforePositionOperation =>
RuntimeInitialCreateTeleportHookPhase.BeforePositionOperation,
RuntimeTeleportHookPhase.AfterPositionOperation =>
RuntimeInitialCreateTeleportHookPhase.AfterPositionOperation,
RuntimeTeleportHookPhase.AfterEnterWorld =>
RuntimeInitialCreateTeleportHookPhase.AfterEnterWorld,
_ => throw new ArgumentOutOfRangeException(
nameof(phase),
phase,
$"Unmapped {nameof(RuntimeTeleportHookPhase)} value - add an explicit arm to {nameof(MapHookPhase)} and to the public {nameof(RuntimeInitialCreateTeleportHookPhase)} projection."),
};
/// <summary>
/// Review fix (2026-08-02): see <see cref="MapHookPhase"/>'s remarks -
/// same explicit-arms-plus-throwing-catch-all discipline.
/// </summary>
private static RuntimeInitialCreatePositionDisposition MapDisposition(
RuntimeAuthoritativePositionDisposition disposition) => disposition switch
{
RuntimeAuthoritativePositionDisposition.RejectedAuthority =>
RuntimeInitialCreatePositionDisposition.RejectedAuthority,
RuntimeAuthoritativePositionDisposition.RejectedData =>
RuntimeInitialCreatePositionDisposition.RejectedData,
RuntimeAuthoritativePositionDisposition.AwaitFreshPosition =>
RuntimeInitialCreatePositionDisposition.AwaitFreshPosition,
RuntimeAuthoritativePositionDisposition.NoPositionOperation =>
RuntimeInitialCreatePositionDisposition.NoPositionOperation,
RuntimeAuthoritativePositionDisposition.Interpolate =>
RuntimeInitialCreatePositionDisposition.Interpolate,
RuntimeAuthoritativePositionDisposition.SetPosition =>
RuntimeInitialCreatePositionDisposition.SetPosition,
RuntimeAuthoritativePositionDisposition.SetPositionSimple =>
RuntimeInitialCreatePositionDisposition.SetPositionSimple,
_ => throw new ArgumentOutOfRangeException(
nameof(disposition),
disposition,
$"Unmapped {nameof(RuntimeAuthoritativePositionDisposition)} value - add an explicit arm to {nameof(MapDisposition)} and to the public {nameof(RuntimeInitialCreatePositionDisposition)} projection."),
};
/// <summary>
/// Review fix (2026-08-02): see <see cref="MapHookPhase"/>'s remarks -
/// same explicit-arms-plus-throwing-catch-all discipline.
/// </summary>
private static RuntimeInitialCreatePositionConstrainPhase MapConstrainPhase(
RuntimePositionConstrainPhase phase) => phase switch
{
RuntimePositionConstrainPhase.None =>
RuntimeInitialCreatePositionConstrainPhase.None,
RuntimePositionConstrainPhase.BeforePositionOperation =>
RuntimeInitialCreatePositionConstrainPhase.BeforePositionOperation,
RuntimePositionConstrainPhase.AfterPositionOperation =>
RuntimeInitialCreatePositionConstrainPhase.AfterPositionOperation,
_ => throw new ArgumentOutOfRangeException(
nameof(phase),
phase,
$"Unmapped {nameof(RuntimePositionConstrainPhase)} value - add an explicit arm to {nameof(MapConstrainPhase)} and to the public {nameof(RuntimeInitialCreatePositionConstrainPhase)} projection."),
};
/// <summary>
/// C3-1: projects an internal <see cref="RuntimeInitialCreateExecutionReceipt"/>
/// to the public <see cref="RuntimeInitialCreatePlacementCompletion"/>
/// shape exactly once, at completion time (see the
/// <c>_completionReceipts</c> assignment in <see cref="ExecuteCore"/>).
/// Only Position-kind trace entries carry route facts meaningful for
/// constrain/interpolation binding; every other action kind
/// (InitialAdoption, TeleportHookRequest, replay, envelope stages, ...)
/// is intentionally excluded from <see cref="RuntimeInitialCreatePlacementCompletion.PositionRouteFacts"/> -
/// widening this to every trace entry would require making the whole
/// internal action-kind vocabulary public, which the pinned contract
/// explicitly prefers to avoid.
/// </summary>
private static RuntimeInitialCreatePlacementCompletion ProjectCompletion(
in RuntimeInitialCreateExecutionReceipt receipt)
{
ImmutableArray<RuntimeInitialCreateExecutedAction> trace = receipt.Trace;
int positionCount = 0;
for (int i = 0; i < trace.Length; i++)
{
if (trace[i].Kind == RuntimeInitialCreateExecutedActionKind.Position)
positionCount++;
}
ImmutableArray<RuntimeInitialCreatePositionRouteFact> positionFacts;
if (positionCount == 0)
{
positionFacts = ImmutableArray<RuntimeInitialCreatePositionRouteFact>.Empty;
}
else
{
var builder = ImmutableArray.CreateBuilder<RuntimeInitialCreatePositionRouteFact>(
positionCount);
for (int i = 0; i < trace.Length; i++)
{
RuntimeInitialCreateExecutedAction action = trace[i];
if (action.Kind != RuntimeInitialCreateExecutedActionKind.Position)
continue;
// Review fix (2026-08-02): PositionDisposition is nullable
// on RuntimeInitialCreateExecutedAction because it is only
// meaningful for Position/hook-request entries in general -
// but BuildPositionTrace is the SOLE constructor of
// Kind.Position entries (grep-confirmed, 5 call sites, all
// through BuildPositionTrace) and it always passes
// route.Disposition, a non-nullable enum, into this slot.
// Null is therefore NOT a legitimate state for a Position-
// kind entry specifically - a silent `?? NoPositionOperation`
// fallback here would have hidden a real bug (a future
// Position-trace producer that forgot to set it) behind a
// plausible-looking default. Fail loudly instead.
if (action.PositionDisposition is not { } disposition)
{
throw new InvalidOperationException(
"A Position-kind executor trace entry must always " +
"carry a non-null PositionDisposition - " +
"BuildPositionTrace (the sole producer of Kind.Position " +
"entries) always supplies route.Disposition.");
}
builder.Add(new RuntimeInitialCreatePositionRouteFact(
action.Sequence,
MapDisposition(disposition),
MapHookPhase(action.HookPhase),
MapConstrainPhase(action.ConstrainPhase),
action.StopInterpolating,
action.ZeroVelocity,
action.PreserveHeading,
action.SendPositionImmediately));
}
positionFacts = builder.MoveToImmutable();
}
return new RuntimeInitialCreatePlacementCompletion(
receipt.Entity,
receipt.FullCellId,
MapHookPhase(receipt.TeleportHookPhase),
positionFacts,
receipt.ReplayedDeferredChildCount);
}
/// <summary>
/// F2: reaps exactly one completion-receipt correlation entry, bound as
/// <see cref="RuntimeSetPositionState.BindExecutorCompletionAcknowledgement"/>'s
@ -461,7 +740,8 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
{
if (_completionReceipts.TryGetValue(
key,
out (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt) entry)
out (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt,
RuntimeInitialCreatePlacementCompletion Public) entry)
&& entry.Sequence == sequence)
{
_completionReceipts.Remove(key);
@ -787,6 +1067,13 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
progress.Trace.ToImmutable(),
progress.ReplayedDeferredChildCount);
receipt = completedReceipt;
// C3-1: project to the public host-consumption shape
// exactly once here, alongside the internal receipt -
// never recomputed per host read/retry (see
// ProjectCompletion's and _completionReceipts's own doc
// comments).
RuntimeInitialCreatePlacementCompletion publicCompletion =
ProjectCompletion(completedReceipt);
_progress.Remove(key);
// C0-1: bridge the executor's own completion onto the
// SAME ordered placement receipt stream every
@ -806,7 +1093,7 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
canonical,
beforePublish: token =>
_completionReceipts[key] =
(token.Sequence, completedReceipt));
(token.Sequence, completedReceipt, publicCompletion));
return RuntimeInitialCreateExecutionStatus.Completed;
}
case RuntimeInitialCreateResidenceExecutorReleaseStatus.Revised:

View file

@ -12,16 +12,20 @@ public sealed class RuntimePlacementProjectionChannel
{
private readonly RuntimeEntityObjectEventStream _events;
private readonly RuntimeSetPositionState _setPosition;
private readonly RuntimeInitialCreateContinuationExecutor _initialCreateExecution;
private Func<RuntimeGenerationToken> _generation = static () => default;
private bool _generationBound;
internal RuntimePlacementProjectionChannel(
RuntimeEntityObjectEventStream events,
RuntimeSetPositionState setPosition)
RuntimeSetPositionState setPosition,
RuntimeInitialCreateContinuationExecutor initialCreateExecution)
{
_events = events ?? throw new ArgumentNullException(nameof(events));
_setPosition = setPosition
?? throw new ArgumentNullException(nameof(setPosition));
_initialCreateExecution = initialCreateExecution
?? throw new ArgumentNullException(nameof(initialCreateExecution));
}
/// <summary>
@ -81,6 +85,32 @@ public sealed class RuntimePlacementProjectionChannel
public int PendingCount => _setPosition.PendingProjectionCount;
/// <summary>
/// C3-1: the public host consumption shape for an initial-Create
/// continuation-executor drain's completion. Reached with the exact
/// <see cref="RuntimePlacementProjectionToken"/> carried by a
/// <see cref="RuntimePlacementProjectionKind.ExecutorCompleted"/> receipt
/// observed through <see cref="Subscribe"/> - the same correlation
/// identity (Entity/Sequence) every other placement Kind uses. Exposes
/// exactly the facts a cutover host needs to bind presentation off an
/// initial placement (the teleport-hook phase, the drained Position
/// continuations' route facts for constrain/interpolation binding, and
/// the replayed-deferred-child count) without widening any internal
/// Runtime type's accessibility. Returns false for a generation
/// mismatch or a stale/superseded/unknown token, mirroring every other
/// generation-gated method on this channel.
/// </summary>
public bool TryGetInitialCreateCompletion(
RuntimeGenerationToken expectedGeneration,
in RuntimePlacementProjectionToken token,
out RuntimeInitialCreatePlacementCompletion completion)
{
if (IsCurrent(expectedGeneration))
return _initialCreateExecution.TryGetCompletion(token, out completion);
completion = default;
return false;
}
private bool IsCurrent(RuntimeGenerationToken expectedGeneration) =>
_generationBound
&& expectedGeneration.Value != 0UL