Skip to content

How the Stage renders a model

The Stage draws the model it was started with. It does not generate screens for it and it does not own a shell of its own: it renders the scene the model describes through the default blueprint (@cratis/scene.blueprint.default), the same application shell, chrome, themes and components a Scene application gets.

Part of the modelRendered as
screenOne entry in the blueprint menu, addressed by #/<ScreenName>.
screen templateThe template’s arrangement flow positions the screen’s slots, evaluated for the current size class.
layoutThe blueprint’s shell for that layout name (AppShell, FullPage). A layout the blueprint does not provide is drawn in AppShell.
ui profileDecides which packages a bare component name resolves against. core, Cratis.Components (React Aria) and the blueprint are always admitted. React Aria is the zero-configuration default; packages PrimeReact explicitly opts in to PrimeReact. Its root template, shell options and appearance decide the shell the application starts in (see Root template, options and appearance).
Command and queryComponents wired to the routes the running application exposes.

With packages PrimeReact, the frontend adds the PrimeReact packages and icons, its provider, theme CSS, styling plugin and build-time PrimeUI license. Without that declaration, the scaffold emits none of them and the live Stage does not load the provider, license or theme. Bare names shared with React Aria resolve to PrimeReact only when it is listed; blueprint names still take precedence.

A bare component name in a template (for example title or inputText) is resolved against the profile’s packages before it is rendered, so a template written against the blueprint’s vocabulary draws real components rather than placeholders. A name no package declares renders a placeholder that names what is missing.

A simple action … execute <Command> translates to core:action. Guarded when … execute alternatives translate to Scene 4.83.0’s core:guarded-action, carrying typed conditions, subject references and an optional fallback. Stage binds the nearest data declaration’s query, route and screen-key arguments to the component’s item; a collection supplies its selected row instead. Scene selects and executes the branch through Stage’s registered Arc commands, for both live and generated applications.

  • Only the first matching alternative executes, with its with values read from the item.
  • A missing field never matches. A present null matches only == null.
  • When no alternative holds, otherwise hidden renders nothing; otherwise execute <Command> offers that command.
  • No fallback is invented. Without an item or selected row, the action is hidden, even if it has a fallback command.
  • The action follows changes to the item and prevents duplicate execution while its command is pending.

Guarded on click, on double click, on select and on change interactions translate to one Scene interaction binding containing ordered alternatives and optional otherwise actions. Scene selects the first matching branch for the row the interaction happened on, or otherwise when none matches. Without a row it runs nothing. A double click does not first select or navigate through the single-click handler.

An on change on a component that presents items translates to a Scene interaction binding like any other, guarded or not. What the component does with it is the Stage frontend runtime’s: a component that lets the user change an item reports the change, and the runtime runs the bindings for the item that changed.

The default blueprint’s kanbanBoard is the first such component. When the document declares an on change for it, moving a card, by dragging or from its “Move to” menu, runs the bindings with:

  • item, the card’s row from the query the board reads, with the property named by cardColumn already holding the column the card was dropped in. with talkId from item.talkId reads the row’s own field, and a guard such as when item.status == "accepted" is evaluated against the changed row, exactly as for a table row.
  • value, the column the card was dropped in, and previous, the column it came from.

Everything else is the ordinary interaction engine: the command is executed through the Stage’s command route, an on success or on failure continuation runs as written, and a notification appears where every notification does. Once the bindings have run, the queries are read again. A move the command refused is therefore shown as the data says it is rather than as the user left it, and a move that succeeded shows the figures the command’s events changed. Data that reaches the board without the user moving a card runs nothing. A board whose document declares no on change is the blueprint’s board unchanged.

Scene evaluates comparisons of item.<field> with literals using ==, !=, >, >=, <, <=, contains and startswith, including nested and and or and enumeration membership. References use subject, not render-time path bindings. Unqualified paths are not implicitly prefixed with item.; they remain unsupported. An unevaluable condition refuses the whole selection rather than taking a different branch or the fallback.

For guarded actions and item-change interactions, contains requires both a string field and a string literal. An array or any other non-string operand is unevaluable, even inside an or whose other condition is true; neither the command nor the fallback runs. An explicit null field can match == null, but an absent field or nested path never matches that comparison. Stage keeps missing values distinct from null.

Unsupported source or Scene JSON stays refused with STAGE-SCENE-ACTION-001 or STAGE-SCENE-INTERACTION-001, including inside sections and template slots. The Host records the capability refusal in /stage/status and returns HTTP 501 rather than executing an unconditional action. A runtime action without a data declaration or whose subject query fails shows STAGE-SCENE-ACTION-001; an unsupported predicate that reaches Scene shows its disabled action and SCENE-GUARD-001. Neither executes a command. Legacy $guard envelopes are not emitted and remain refused.

Importers can retain the supported parts of a model without treating an unsupported projection or arrangement as an exception. ScreenplayEventModelVisitor.Convert(ApplicationSyntax) returns an EventModelConversion with Model and Diagnostics. The additive ProjectionConverter, ReadModelConverter, SliceConverter and ArrangementConverter overloads accept a ConversionDiagnostics collector. Each diagnostic carries Code, Severity, Construct, Path, Location and Message; ToDiagnostic() converts it to a Screenplay diagnostic. All preserves reporting order, and HasErrors distinguishes errors from warnings.

An unsupported projection is omitted as a whole, not partially translated. Its read model remains, with Projection = null and UnconvertedProjectionName naming the omission. Other slices, commands, events and specifications remain available. Additional projections receive a warning because a Stage read-model definition carries only the first projection. Existing converter overloads keep their throwing behavior; ScreenplayEventModelVisitor.Visit stops at the first unsupported projection with the original InvalidEventModel from SliceConverter, preserving its message. Other legacy engine admission checks still apply to both visitor entry points.

ScreenplaySceneVisitor.Convert records arrangement conversion diagnostics in SceneApplication.ConversionDiagnostics. The existing Visit entry point uses the original throwing path, stopping at the first invalid arrangement with UnknownArrangementNode, UnknownArrangementContainerKind, UnknownSizeClassName or FormatException as appropriate. Unsupported nodes and containers lose their subtree; invalid sizes omit the affected placement, override or variant. Supported siblings remain. These diagnostics are not Scene JSON members, so keep them alongside the translated Scene when importing. The Cratis renderer refuses Error conversion diagnostics before creating a profile; warnings alone do not refuse it.

CodeSeverityConstruct
STAGE-PROJECTION-001ErrorProjection sequence
STAGE-PROJECTION-002ErrorProjection variant
STAGE-PROJECTION-003ErrorUnsupported projection block
STAGE-PROJECTION-004ErrorUnsupported projection mapping, including its property
STAGE-PROJECTION-005ErrorUnsupported projection expression
STAGE-PROJECTION-006ErrorUnsupported projection key
STAGE-PROJECTION-007ErrorUnsupported automap mode
STAGE-PROJECTION-008WarningAdditional projection not converted
STAGE-SCENE-ARRANGEMENT-001ErrorUnsupported arrangement node
STAGE-SCENE-ARRANGEMENT-002ErrorUnsupported container kind
STAGE-SCENE-ARRANGEMENT-003Render findingUnsupported Scene arrangement type
STAGE-SCENE-ARRANGEMENT-004ErrorInvalid placement size or unknown size class

ArrangementSelector.TrySelect returns false for an unsupported arrangement type. RenderPlanner reports ArrangementUnsupported (value 20) with STAGE-SCENE-ARRANGEMENT-003 instead of throwing. A known freeform arrangement missing the target size-class variant remains a separate missing-variant finding. Package catalog failures continue to use the existing render findings.

Generated scoped projections map supported $eventContext values directly from Chronicle metadata, including from, every/all, joins, children, and unconditional nested targets:

Screenplay valueTarget typeGenerated mapping
$eventContext.occurredDateTime.ToEventContextProperty(c => c.Occurred)
$eventContext.correlationIdUuid.ToEventContextProperty(c => c.CorrelationId)
$eventContext.sequenceNumberInt.ToEventContextProperty(c => c.SequenceNumber)
$eventContext.eventSourceIdString.ToEventSourceId()

Optional leaves and unconstrained concepts over these primitives are supported. Required leaves remain required; Stage does not replace metadata with payload fields or defaults. Mappings through optional intermediate objects still degrade only where omission is safe. Unsupported paths such as $eventContext.causedBy.name are not emitted as arbitrary C# members. Key, child-identity, join-backfill, query-filter, and command-decision guards still apply; strict planning rejects unsafe or degraded output. Modeled specifications asserting occurrence, correlation, or sequence values without bound event metadata are not rendered. This renderer support does not change Screenplay’s reference executor.

An explicit passive directive on a Screenplay read model renders Chronicle’s [Passive] attribute. A fluent projection also receives builder.Passive() immediately after builder.NoAutoMap(). Model-bound projections and reducers honor the attribute. The EventModel runtime registers a passive projection as inactive with sink None; active projections keep their InMemory sink.

readmodel Current
passive
id String key
value String

A passive model is rebuilt on demand from the requested event source’s events instead of being maintained in a sink. Commands can read it; Screenplay refuses queries and screen data that observe it with PLAY0859. Without the directive, the model stays active, even if only commands read it. Unannotated models retain their existing profile and output bytes.

Passivity is not part of portable ESM or its revision. Source-backed planning carries the explicit semantic identities in the hashed cratis-realization:passive-read-models profile input. Programmatic callers can use CratisRendering.WithRealization(profile, compilation) or WithPassiveReadModels(profile, compilation) from the same compilation. A portable-ESM-only caller without this metadata retains active behavior. Malformed, duplicate, noncanonical or unknown identities fail profile admission.

STAGE-ESM-061 refuses passive projections with payload, constant, composite or context root keys, joins, or child/nested scopes. Chronicle’s immediate projection replays only the requested key’s event-source stream: other keys risk CHR0046, and cross-stream scopes can return incomplete state. Reducer-backed passive models remain supported.

Passive reads are fresher, not safer: they do not capture a read position or provide optimistic-concurrency protection. Passivity does not enable protected reads or change their policy.

Artifact planning can opt a confirmed new application into self-reducing read models. Existing callers remain classic; this does not change the running Stage’s semantic execution or automatically change CLI and Studio output. See the profile and persisted configuration contract.

In self mode, the read-model record in the existing slice file contains one public static Reduce overload per observed event and a non-public static typed-context helper for each verified pure transition. Its queries remain unchanged. Both StateView and Automation slices use this layout; no IReducerFor<T> companion is emitted. Observer source and stream filters are attributes on the model. Tenant translation, occurrence time, checked sequence conversion, immutable inputs, deletion, and exact event-source identity checks retain the classic semantics. The scaffold takes the exact Chronicle and Arc pins for this generated surface from Stage’s Directory.Packages.props at build time.

Self-mode admission checks the same record and query signatures used for rendering, so model properties and query names participate in pure-body binding. Rendering and analysis share the entire file’s imports and visible declarations, including sibling reducers, projections, and Automation commands. Observable query signatures are represented in closed analysis without allowing query or reactive-service calls from pure transitions. Reduce as a model, property, or query name, the replay-hook query names BeginReplay, EndReplay, BeginReplayPartition, EndReplayPartition, and collisions with generated generated transition helper names (<Event>Transition, qualified by the reducer name when two reducers share an event) refuse with STAGE-ESM-019. Rename the conflicting member; Stage never silently switches a self model back to classic. Existing purity and typed-context refusals still apply.

For the Chronicle runtime contract, see self-reducing read models and choosing a read-model style. Existing applications migrate one read model at a time: the self-reducing model keeps the classic reducer’s observer identity and the plan requires removing the old file. See migrating a classic read model.

Stage tables and guarded actions resolve the modeled query name to a registered Arc query proxy. A route only disambiguates proxies sharing that name; an unregistered query is not fetched directly. Generated applications register their generated proxies. The live Stage host registers native Arc query proxies from its discovered routes before rendering.

Snapshot queries use Arc’s perform with the configured origin, API base path, microservice and HTTP identity headers. Route parameters are substituted by Arc; remaining arguments go in the query string for GET queries. Queries wait until their required argument bindings are ready. Observable queries use Arc’s configured subscription transport (including the native hub for generated proxies with their fully qualified query names). Only the live Stage host configures its Arc provider with queryDirectMode, preserving its direct per-route transport because its proxies have routes rather than fully qualified backend query identities. Queries unsubscribe when their view or arguments change. The SSE EventSource relies on the browser’s cookie policy, not the HTTP headers callback. If no first ready subscription result arrives within 15 seconds, Stage unsubscribes and performs one read through Arc to surface a rejection rather than leave the view loading indefinitely. After the first ready result, Arc owns reconnection.

The generated-application build harness packs the runtime after running npm ci in Source/Frontend, resetting that checkout’s node_modules. Do not run frontend checks concurrently with that harness in the same checkout.

For legacy syntax rendering, an inline C# command handler keeps its authored body and receives CommandContext context. Its return type comes from the body, not from declared produces clauses:

Authored bodyGenerated return type
Any yield return or yield breakIEnumerable<object>
No return <expression> (no returns, or only return;)void
Every return <expression> constructs the same explicit type with new T(...), and its rightmost name is non-genericT
Anything else, including a generic constructed type, returned variable, array, mixed types or target-typed new(...)IEnumerable<object>

A body mixing return; with return <expression>; is rejected with MixedReturns under STAGE-CRATIS-INLINE-001: return a value or nothing on every path. Bare returns alone still render as void. Returns and yields inside lambdas, anonymous methods and local functions do not affect the signature or this check. Explicit type names are compared after whitespace normalization; qualified names in the authored body, such as global::Cratis.Chronicle.EventSequences.EventForEventSourceId, are preserved. A constructed type whose rightmost name is generic, such as List<object> or System.Collections.Generic.List<object>, keeps IEnumerable<object> so collections retain the collection-of-events signature. Returning a list variable also keeps IEnumerable<object>. A single constructed type is preserved even when it is hand-written: unless it names a model-declared event or EventForEventSourceId (qualified or unqualified), rendering emits a non-fatal warning that the inferred type is unverified. If that type is a collection not implementing IEnumerable<object>, its events are not appended; return the events as IEnumerable<object> or a single event. This inference does not add async handler support or change file-backed handlers.

For example, assuming the slice declares an OrderPlaced event with no properties, this handler body:

return new OrderPlaced();

renders as this method inside the generated command record:

public OrderPlaced Handle(CommandContext context)
{
return new OrderPlaced();
}

Admission uses the same inferred signature. It still refuses non-C# bodies, directives, malformed bodies and bodies that depend on the generated context parameter with STAGE-CRATIS-INLINE-001. Inference falls back to IEnumerable<object> for malformed bodies; admission rejects them rather than rendering invalid code. Semantic implementation admission is unchanged.

An authored inline C# body that Stage splices into generated code (handler, reaction, policy, query performer, reducer, validation and generation bodies) is re-indented to the block it sits in. Stage strips the common leading whitespace of the body, then applies the block indent, four spaces per level. Line count, line order, every line-break byte (LF, CRLF or CR) and all characters after the leading whitespace stay as authored. Lines that start inside a multi-line raw, verbatim or interpolated string literal are never touched, so a literal keeps its exact value.

The presentation is verified when the body is issued: the token sequence and the containment result must match the authored body. If either differs, Stage splices the authored text verbatim instead of refusing the body. Compiler diagnostics from the generated code are mapped back to the authored body, so reported lines and columns refer to your source.

Bindings for inline reaction bodies and handlers

Section titled “Bindings for inline reaction bodies and handlers”

A reaction body declares a local for a triggering event property only when the body references it by simple name, so an unused property no longer produces an unused-variable warning. Each local is var Name = @event.Name;. A body that only reads another object’s member of the same name, such as other.Title, does not count as a reference. A command handler whose generated Handle method does nothing else with the result returns the handler call directly instead of storing it in a pass-through local. When Handle does wrap the result, the handler result is held in one local per production, named after the event in camelCase with a numeric suffix on a clash. Several productions are deconstructed in declared order, for example var (orderPlaced, orderEchoed) = HandlePlaceOrder(...).

The shared policy runtime identifies concept base classes with open-generic typeof(ConceptAs<>) and typeof(EventSourceId<>) comparisons, not CLR type-name strings. Claim matching and fail-closed behavior are unchanged.

A policy whose complete condition is require anonymous opts its command or query into anonymous access. Both unauthenticated and authenticated callers are allowed. Stage renders [AllowAnonymous] from Cratis.Arc.Authorization, emits no [Authorize] on that artifact, and reports the information diagnostic STAGE-ESM-060 for the command or query.

Anonymous access must be the entire referenced policy. Stage refuses not anonymous, anonymous mixed with and/or, or combined authorization references with STAGE-ESM-015; it does not guess which policy to override. Unknown security members remain fail-closed with STAGE-ESM-016. The semantic Host and specification executor admit the same anonymous shape. Denied expectations for anonymous operations do not pass: an anonymous policy cannot justify then denied or a denied query. Unannotated behavior and all non-anonymous policy emission are unchanged.

Query arguments retain their concept types so Arc validates the by key and supplied filter values before invoking the generated method. An out-of-rule value returns HTTP 400 with validation results; an in-rule key that matches nothing still returns null or an empty collection. Validation also reaches concept members inside composite arguments and collection elements. Authorization runs before validation. TypeScript proxy argument types are unchanged. Nullable keys and optional concept filters with not empty rules receive STAGE-ESM-010: Arc skips null concept values, while the reference validates nullable concept keys and rejects explicitly null filters. An omitted optional filter remains distinct from a supplied null.

A query a performer answers is an opaque read: the executable model leaves it out of the slice’s queries and carries its shape - the read model, cardinality, delivery, by key and filters, and who may call it (SemanticPerformedQueryShape.Authorization, Screenplay 4.236) - on the QueryPerformer implementation requirement. Stage renders it as the query that shape states: the read model record, the query method with its route, parameters (filters in declaration order, optional ones defaulted) and the [Authorize] policy the model names, and a read model such a query returns needs no projection or reducer. When a verified inline csharp or file body is attached, Stage embeds it in a compile-checked helper. The body returns a read-model instance (or null) for a single query, or IEnumerable<ReadModel> for a collection. readModels is the current read-model collection; caller arguments are available both as their camelCase parameter names and as PascalCase properties on context.Arguments. The application-local context also supplies Tenant (string), Identity (ClaimsPrincipal), and ReceivedAt (DateTimeOffset captured by Arc at query receipt). It deliberately has no command causation, CausedBy, or event Occurred member. Snapshot queries load instances once. The body is synchronous C#, not a pure reducer sandbox. Observable/live performers have no body-owned stream and render as explicit gaps rather than applying a body to read-model emissions. A body in another language now refuses planning with STAGE-IMPL-LANGUAGE; it never becomes a gap or stub, even in Stub mode. A missing attachment, changed or hash-invalid body, or body compile error retains its existing diagnostics and gap handling. Both planning and direct shape rendering produce the same compilable method boundary, throwing NotImplementedException with Query '<name>' (requirement <id>) is not runnable: <reason>. Planning reports one STAGE-IMPL-006 Warning per such query (“performer rendered as a gap”), preserving the original reason, compiler diagnostic and body source location when available. The generated artifacts remain available.

Each gap has ArtifactImplementationGapOrigin.Unverified, an additive public contract member with minor-version intent, the compiler requirement identity, and the generated stub’s signature, body locator and unrealized typed-context metadata. STAGE-NOT-RUNNABLE.md lists every gap in the plan. Any non-empty ImplementationGaps makes Outcome Partial; Success still means no Error diagnostics, so a partial gap plan can be successful without being fully runnable. Verified synchronous bodies remain unchanged, with Outcome Complete and no marker when no gaps exist. STAGE-ESM-019, STAGE-ESM-020 and STAGE-IMPL-005 keep their Error meanings for cases that still refuse; they are not reused as warnings. The semantic runtime continues to list the read model but not the opaque query. A policy implemented by code, a composite by key, an unresolved argument type, an observable single result or a required argument after an optional one refuse the read model with STAGE-ESM-007, naming the query and why. Explicit scoped to clauses remain refused by Screenplay (PLAY0268); Stage does not silently drop them.

ESM v18 adds query-only then error specifications. For empty fixtures, Stage generates an Arc QueryScenario call that asserts validation failure and the expected message, when supplied. These empty-world specifications also support then no result, but do not relax application admission: every read model still needs a projection or reducer (STAGE-ESM-007), unless a performer answers a query for it. The projection-less QueryValidation/v18 corpus runs in the reference and Stage executors but is refused as a rendered application; generated conformance specifications use a projected view. Query rejection specifications with given events or read-model state are refused with STAGE-ESM-011; the generated rejection scenario supports only empty fixtures. Sparse composite query arguments also receive STAGE-ESM-011: supply every member, including optional members, so generated literals do not invent values that change validation.

Stage renders the application while omitting supported specification gaps by default. When every Error is STAGE-ESM-011, STAGE-ESM-024, or STAGE-ESM-051 and names a specification in the model, Stage changes those diagnostics to warnings and skips only those specification files. Each warning names the specification, the original diagnostic code and the reason it cannot render. No opt-in is needed, including when CratisPlanOptions.Refusals is ArtifactRefusalMode.Refuse.

Outside the event-member case below, Stage never omits commands, events, policies, or other application contracts. Any other error still refuses the entire plan, including evolved events (STAGE-ESM-026), implementation-envelope errors, and STAGE-ESM-024 attached to a reaction, trigger, capture, or slice. Specification admission guards, including the constraint-target guard, remain unchanged; skipping a specification does not prove that it passes.

Event-triggered native reaction specifications can run the rendered reactor through Chronicle’s reactor scenario helper, for both inline C# and file bodies. The generated tests await the handler and assert returned event counts, types, payloads and destinations on the explicit triggering source; then no events asserts that no side effects were returned. This is not a kernel settlement test. The supported deterministic subset constructs modeled events from input properties, literals and local decisions. Context reads, calls, deeper reaction cascades, append-time constraints and native bodies in given history still omit the specification with STAGE-ESM-011. Undeterminable or mixed event/command returns retain STAGE-ESM-037. Startup and schedule reaction bodies remain outside this specification support.

A plan with only skipped specifications has Success == true and Outcome == Complete.

Specifications for operations with admitted opaque C# policy bodies execute the predicate through Arc’s command/query pipeline. Their authored then blocks are the assertion oracle; the reference evaluator still reports Unsupported when it reaches an opaque condition and never interprets its C#. Caller identity comes from the scenario’s caller fixture; the subject comes from the command’s or query’s argument values. An opaque context.Occurred read requires a command scenario with an authored given clock; query scenarios and commands without that clock are omitted with STAGE-ESM-011 rather than using an uncontrolled authorization receipt time. Other unsupported scenario shapes remain omitted.

Inspect Refusals on ArtifactRenderPlan or CratisPlanResult for the omissions. Each refusal lists its semantic Artifact, Kind (specification), and sorted diagnostic Codes; entries are sorted by artifact identity. No emitted artifact’s Sources contains an omitted identity, and no placeholder specification is emitted.

DiagnosticSeverityMeaning
STAGE-PLAN-007InformationRendered around N refused specifications; inspect their warnings.

Publishers still perform the ordinary publication and ownership checks. This behavior does not change Host or specification-executor admission, and does not add a CLI flag. The existing refusal-mode profile input remains accepted and hashed with the profile. The output digest adds a refusals-v1 segment only when Refusals is nonempty.

In audited ESM versions only, an event-owned unsupported property member (STAGE-ESM-012) can produce a Degraded plan: Stage omits the whole event and owners proven to depend on it, while independent, supported slices still render. It emits no stub or artifact sourced from an omitted owner. This does not permit rendering around arbitrary command, query, reaction, or shared-type errors.

The dependency proof tolerates unknown variants or unresolved identities only in the root event’s own property-type subtree at supported traversal locations. Unclassified gaps, ambiguous ownership, an authored Scene composition, unrelated errors, migration removals affecting an omitted read model, or failure while rendering the remaining owners retain the whole-render refusal. Strict mode also refuses the whole render, with no artifacts. In newer, unaudited ESM versions, undefined enum values retain the security refusal (STAGE-ESM-016); the range warning (STAGE-DEGRADE-004) does not permit partial rendering. Supported models keep their existing output.

The root warning STAGE-DEGRADE-001 preserves the original STAGE-ESM-012 message. Dependent warnings use STAGE-DEGRADE-005 and identify their root event or events. Upgrade Stage to support the member, or change it to a supported type, to remove the gap. See Render plans for the omission metadata.

Set CratisPlanOptions.Strict for source planning, or CratisRenderingOptions.Strict when creating a direct rendering profile, to reject validated degradation at acceptance time. The setting is recorded as the hashed stage:strict profile input, version 1, with exact UTF-8 bytes true or false and no trailing newline. An absent input means false; default profile creation omits it to preserve legacy profile bytes. The shared Cratis.Stage.Contracts.Rendering.StrictAcceptanceInput creates and reads this input.

Unverified query performers retain their generated throwing bodies and implementation-gap metadata. Default planning preserves the existing STAGE-IMPL-006 diagnostics and does not add public degradation warnings or acceptance metadata. Default plans with unclassified implementation gaps retain their legacy partial outcome and omit the unavailable inventory and completeness reports rather than claim those owners are rendered. Only strict planning records STAGE-DEGRADE-002 warning evidence in ArtifactRenderPlan.Degradations and promotes that recorded warning to an Error, so Success becomes false while Outcome remains Degraded and CanPublishArtifacts remains true. Genuine generation or integrity Errors still prevent safe publication. Strict acceptance is not a request to discard safe artifacts or reports; planning itself never publishes files.

An unknown validation rule kind or undefined severity on a command records one STAGE-DEGRADE-002 warning and degradation evidence for that command. Its inventory uses DenyEnclosingOperation: an always-failing Error-severity validator rejects every invocation before argument resolution, the handler, or any append. Specifications invoking the denied command are skipped with STAGE-ESM-011. Strict acceptance promotes the degradation warning to an Error. Concept validation rules and fixable authoring errors in known rules still refuse.

WithAcceptance(plan.Degradations, strict: false) restores the original warning evidence. Release-baseline warnings and source diagnostics retain that evidence across plan reconstruction. WithInventory accepts a validated construct inventory; its locations carry the exact typed SemanticAddress as semanticLocator and a real portable relative modelPath, or explicit null when no source filename is available. Semantic locators are not filenames and must not be flattened into fabricated paths.

The current provider counts selected identity-bearing commands, produced events, read models, queries, projections, reactions, captures, specifications, concepts and composite types. It does not claim a complete inventory of every public construct kind: reducers and constraints lack independent identities in the current semantic model, and policies, guards, validations and screens are not yet independently inventoried. It never invents identities for those owners. The shared contract supports their kinds for providers with explicit semantic ownership. A refused construct requires refusal reasons and no degradation entries; independent refused and degraded constructs may coexist in one inventory, with refusal taking precedence. When supplied with graph-proven omission evidence and typed causes, the inventory represents omitted roots and dependents as Degraded with GraphProvenOmittedLeaf gaps, retaining the original warning evidence. Only refusals containing 001 or 005 with matching degradation evidence for every refusal code receive this omission shape; other degradations retain their throwing-body placeholders. Root gaps have no cause references; 005 dependency gaps reference the original root owners and codes, deduplicated and sorted by owner and code. Skipped specifications without degradation evidence remain Refused with their original omission warnings. This inventory support does not activate the planner’s graph-omission analysis.

The shared STAGE-DEGRADE-001 through 005 codes describe unknown members, unsupported constructs, inline mismatches, a newer ESM inside an explicitly declared supported range, and dependency degradation. Their availability is not a claim that the Cratis provider emits all five today.

The Cratis renderer declares the range >=32.0 <33 and audits exact ESM v1.0–v34.0 and v36.0 language/semantic pairs. This does not admit v35 or arbitrary minor versions. ESM v34 then invokes specification expectations outside the admitted single-hop recording subset remain refused per construct with STAGE-ESM-016. A language/semantic pair that is not audited, matches in major and minor, and lies inside that range (for example 32.1) renders as Degraded with plan-level STAGE-DEGRADE-004 evidence in ArtifactRenderPlan.PlanEvidence, in the default and the strict mode alike (strict promotes the warning to an error, as for any degradation, but the plan still publishes). Members that this Stage version has not audited on selected owners add STAGE-DEGRADE-001 evidence. A member that carries security meaning (policies, authorization, claims and similar masks) is never rendered around: the render is refused with STAGE-ESM-016 naming the member. A future major (33.1 or any later version, except the audited exact v34.0 and v36.0 pairs), an out-of-range pair, a malformed pair and an unknown enumeration value are refused with STAGE-ESM-016; the runtime and specification executor still support only the exact audited pairs.

Plan-level evidence names no selected construct, so it is excluded from the inventory, the completeness score and the construct gaps. It does change the plan digest and the outcome.

When a completeness report is written for such a plan, it is the additive stage-render-completeness/v2 shape: support carries the stage-render-audited-support/v2 version, the AuditedOrNewerWithinDeclaredRange policy, the typed declaredRange (minimum pair and exclusive major) and the effective policy and versions, and a planEvidence array lists the evidence. Reports without a declared range keep the exact v1 bytes and the AuditedOnly policy.

Release-baseline warnings in artifact plans

Section titled “Release-baseline warnings in artifact plans”

An admitted release baseline freezes stored event revisions, including the resolved concepts and types they use. The Cratis artifact planner reports changed and missing admitted revisions as warnings when its caller supplies ArtifactRenderRequest.ReleaseBaseline. It also warns when that baseline is unreadable or the check is unavailable or stale. Warnings do not refuse rendering or change generated artifacts; ordinary blocking diagnostics still refuse.

Source-based PlanFrom reads the model root’s .screenplay/release-baseline.json without writing it. In-memory callers can use StageReleaseBaseline.Check with an authoritative workspace or compilation and Screenplay’s baseline read. The assessment covers the whole application, even for a scoped plan. Bare-ESM callers that omit it remain unchecked; the live Host does not enforce these artifact-plan warnings. See Planning admitted release revisions for status and diagnostic codes.

A release step explicitly admits the baseline; rendering never does. Run screenplay baseline check <root> in CI to gate edits. Its resolved-schema equality is not Chronicle’s schema compatibility relation. The destination check remains separate: baseline warnings do not bypass CheckPublication or STAGE-EVENT-002.

The live semantic sandbox treats translation integrations differently from generated applications: STAGE-ESM-032 public events and event projections, STAGE-ESM-033 foreign events and event-source captures, and STAGE-ESM-024 Translate slices and captures appear as renderedAround admission entries. Public events are published to the application’s outbox; inbound translation runs on seeded foreign facts and on what reaches the application’s inbox, and never polls external sources. These entries do not block unrelated local execution. Non-event reactions and other unsupported behavior still refuse admission. This does not relax renderer or legacy-engine admission; see semantic sandbox execution. Generated applications can instead render the supported inbox capture and outbox publication subset; see Translation rendering for its mappings, added-state approximation and refusals.

Systems, operation declarations and phases, inline produces operation, given operation … fails, then operation and then compensated are syntax-only authoring constructs. The legacy renderer, Host and structural specification runner refuse them with STAGE-ESM-057, naming the construct and its source location, rather than dropping it or appending an event instead. The semantic path refuses the same constructs with Screenplay’s PLAY0268; operations are not admitted by any supported executable model (ESM) version yet (Cratis/Stage#176, Cratis/Screenplay#301).

A toolbar item or screen action can name its subject (Screenplay decision 0110, Cratis/Stage#855):

  • for item acts on the selected row. The action stays visible and disabled with “Select a row first” until exactly one row of the screen’s own tables is selected, even when a table is empty. with <input> from item.<path> binds that input from the same row, and the form does not ask for it.
  • for screen never waits for a selection and never fills anything from a row. It behaves like the same action on a screen without tables.
  • An omitted subject keeps the existing row inference and its output unchanged.
  • Subjects from Screenplay’s effective syntax (ScreenActionSubjects.Resolve) render the same way.

An input of an action that names a subject, bound from anything other than item.<path> on a for item action, refuses with STAGE-ESM-059 before any output is written. The diagnostic names the action, the input, its binding and its source location.

Stage carries the subject as the Stage-local scene properties actionSubject and subjectInputs until Scene ships a typed ActionSubject.

The semantic Cratis renderer maps whole-number primitives and concepts to long for ESM v8 and later, and to int for ESM v1–v7. Older models with authored whole-number literals outside the int range fail admission with STAGE-ESM-031 rather than generating uncompilable C#.

Stage uses Screenplay 5.3.1 and admits exact ESM v1.0–v34.0 and v36.0 language/semantic pairs, including v11’s corrected Double-mode literal lowering, v15’s generation strategies with atomic freshness scopes, and v20’s rules on generated concepts. V21’s generate pattern and generate random render Fundamentals factories and execute with reference-compatible fixture shapes; dated patterns take their UTC date from the generation context, and random values preserve the full inclusive Int64 range. The specification executor runs pinned String generate rule values without invoking their bodies and still checks concept rules; missing pins remain Unsupported(IdentityAllocation). The Host refuses commands that generate rule values with STAGE-ESM-028, naming the command, property, concept and rule, by design because the interpreter never runs implementation bodies; unused rule concepts do not block the Host or executor. The semantic renderer emits verified C# rule bodies through IGeneratableInContext<T>.New(GenerationContext context), compile-checked against embedded BCL and pinned Fundamentals metadata references without the pure-body walk. Fundamentals is an embedded build-only reference, not a renderer runtime or package dependency. Missing attachments fail STAGE-ESM-020, body SHA-256 mismatches against ContentHash fail STAGE-ESM-056, body compile errors and warnings fail STAGE-ESM-019 at the attachment, and shadowing fails STAGE-ESM-022. Any concept rule requires a pin for rule generation; concepts with no validation rules may generate unpinned in rendered specifications. The legacy syntax engine still refuses generate rule with STAGE-ESM-028. validate checksum luhn, mod11 and mod97 render and execute. The legacy syntax renderer still refuses checksum and unknown validation rule kinds with STAGE-ESM-042. Generated values with any unproven rule require pins in rendered specifications; fully proven rules permit unpinned generation. Failing pins retain STAGE-ESM-011, and unpinned specifications with unproven rules retain STAGE-ESM-038. The generated backend uses Stage’s central Arc pin, which rejects generated values failing concept rules of any severity. Runtime generation defects produce exceptions rather than input validation failures. Applications using only implicit UUID generation remain unguarded even at later schema versions. See Generated values for factory shapes, fixtures and timing limits. Mixed version pairs, unaudited minor versions outside the declared >=32.0 <33 render range, v35, and versions after v36 are refused with STAGE-ESM-016. Exact v33 renders key <property> is event source captures, checking the key against the delivered event source and reading only that source’s inbox history (see Capture state); exact v34 models retain per-construct admission for then invokes expectations, and the specification executor runs v34 models, running event-source-keyed captures over inbox events (see A specification runs its own slice only). V32 and v33 preserve declared capture event-source identities for required scalar text, UUID and whole-number keys, including non-enumerated concepts over them. Inconsistent or non-convertible identity types fail with STAGE-ESM-052, never an implicit text fallback; see Typed capture keys. The semantic Host and specification executor run protected command reads, read-backed requirements and produced-event mappings. The Cratis renderer renders command reads protected, command reads, read-backed requirements and read-path mappings as Arc read protection; see Protected reads rendering. It still refuses the shapes it cannot preserve with STAGE-ESM-044, each naming its reason: a read path in a produces when condition or destination, a read-path mapping to an optional or differently typed event property, and reads combined with generated-value freshness scopes. Non-Error requirements on protected commands, including reaction-invoked commands, remain refused before execution with STAGE-ESM-045 until Screenplay defines their severity semantics. See the semantic Host for how it commits decisions against outside appends. The legacy renderer, Host and structural specification runner also refuse the preamble with STAGE-ESM-044; without it, reads still fail binding with PLAY0271 and the repair diagnostic PLAY0786. ESM v24’s contains, not contains, starts with and ends with rules execute on the Host and specification executor and render as ordinal UTF-16 string comparisons, without regex or a Screenplay dependency in generated apps. Absence passes; an empty operand satisfies contains/starts-with/ends-with and fails not-contains for every present value. In admitted v22+ models, the semantic renderer supports reaction batches atomic when each generated event handler has at most one direct-production trigger: its returned event collection appends as one batch, and each selected invocation refusal branch returns its own event collection. Multiple same-event production triggers in one reaction retain STAGE-ESM-043, naming the unsupported shape. Screenplay rejects routes on direct and refusal-branch productions before Stage admission; Stage retains a defensive STAGE-ESM-043 guard for routed direct productions. Atomic stream closing has no Screenplay ESM representation yet and is not rendered (Screenplay#770). Other reaction admission limits still apply, including STAGE-ESM-037 for mixed events and invocations. The legacy syntax renderer still refuses the atomic declaration. The semantic Host and specification executor execute each batch all-or-none. See Atomic batch rendering and the executor’s Atomic reaction batches. The renderer refuses described command handlers without a body with STAGE-IMPL-002, naming the command and RequirementId. A resolved handler body in another language instead receives STAGE-IMPL-LANGUAGE. No stubs are emitted for either case, even in Stub mode.

STAGE-IMPL-LANGUAGE means a member has implementation bodies but none in csharp for the Cratis target. It is an Error in both Refuse and Stub modes, with strict mode on or off: no foreign-language fallback or stub is emitted. This applies to command handlers, query performers, reaction effects, reducer transitions, value generation, code validations, rule predicates and policy predicates. The diagnostic names the member, requirement and authored language. Add a csharp body to render the member with the Cratis target. Missing or malformed attachments retain their existing diagnostics.

Several same-event production triggers in one atomic reaction stay refused with STAGE-ESM-043 until Chronicle can return ordered per-trigger append batches from one reactor handler (Chronicle#5248). A reactor appends what it returns as one batch, so rendering the triggers into one handler would merge their batch boundaries.

The semantic Host and specification executor admit models containing described command handlers. Authorization, input validation, declarative reads and requirements run before the body; denied and invalid requests keep their normal outcomes. Only a reached handler body reports Unsupported(ImplementationBody), including a reaction-invoked handler. Its declarative produces clauses are not executed and no handler facts are appended. An unsupported body cannot select an invocation refusal branch. The Host lists the handler command as unsupported without blocking the live model. Executor details use STAGE-IMPL-002:; live Host details retain Screenplay’s message without that prefix. Both name the command and RequirementId.

Generated application specifications execute verified synchronous C# command handlers through the command scenario and assert the authored then outcomes, not the declarative produces mappings. These specifications no longer receive STAGE-ESM-011 merely because they reach a C# handler. Existing body admission still applies: only the immutable Command snapshot is available; ambient context reads remain refused with STAGE-ESM-015. Specifications reaching unsupported handler kinds retain STAGE-ESM-011. This does not enable handler-body execution in the semantic Host or reference specification executor.

The stage-spec-run/1 report schema is unchanged, but its capability names now include ImplementationBody. An older reader rejects that unknown enum name; update report readers before consuming these results.

The semantic Host and specification executor execute admitted C# named Boolean rules on command input properties and their concepts through a separate, killable body worker. The worker compiles the renderer’s own pure-rule wrappers, preserving validation messages and rejecting a command before appending facts when a rule is unsatisfied. Timeouts (STAGE-IMPL-007), memory breaches (STAGE-IMPL-008) and worker failures (STAGE-IMPL-009) are unsupported execution, not failed validation. See Body worker configuration for limits and process isolation.

Code-validation blocks, handlers, reducers, policies, reactions and query performers remain unsupported. Query-argument concept bodies also remain refused. With the worker disabled, the original STAGE-IMPL-003 validation-body refusal is retained, naming its owner and RequirementId. Unrelated commands, queries and specifications remain admitted. Command input types and query parameter types reach concept rules transitively through composite types, collections and optional values. A reducer is reached by events introduced by a specification or command, including reaction cascades, or by an assertion or query on its read model. The reach check conservatively includes possible reaction effects without evaluating runtime guards. Reducer refusals carry the Projection capability; validation refusals carry Command. This does not change live-world history rebuilding or the renderer’s implementation-body admission.

Generated specifications with given events and then readmodel assertions execute the rendered pure reducer body through Chronicle’s ReadModelScenario, for both classic and self-reducing read models. Events for the asserted instance are replayed in order, including when append facts; the authored then values are the oracle, not values computed by Stage. Reducers that read context.Occurred, context.SequenceNumber or context.Tenant remain refused with STAGE-ESM-011 because the scenario cannot supply those values deterministically. An asserted instance must be reached by a handled event. Reducer absence assertions remain unsupported. This does not change the semantic Host or specification executor’s implementation-body refusals.

Pure reducer bodies may use Stage’s generated primitive-to-concept implicit conversion: for example, Reason reason = context.Key; when Reason is a String concept, or a Guid value passed to a Uuid identifier concept. The primitive must match the modeled type, and the operator must belong to the exact generated Common concept symbol. Positional and named constructor arguments and typed locals use the same admission rule. Null-literal and null-default operands converted to concepts remain refused. Concept-to-primitive conversion still requires .Value; projection-source concept-to-concept operators, record equality, inherited operators and arbitrary user-defined operators are not admitted. Production analysis mirrors only the allowed operator, and Debug parity specs compare its full bound identity against the real rendered compilation for classic and self-reducer profiles.

The renderer admits verified pure C# Boolean named rule bodies and code-validation iterators on commands and concepts, using generated app-local contexts and Arc validators without a Screenplay runtime dependency. The context supplies Artifact, Value, Property and IsWholeArtifact; command shapes exclude server-generated properties. ValueAs<T>() admits the declared realization, a primitive concept’s underlying value, checked whole-number conversions, and IEnumerable<T> with compatible primitive elements. Whole-number element conversions use a lazy checked projection, not a copied collection. ArtifactAs<T>() requires the artifact’s exact generated type. Explicit casts on context.Value follow the same scalar restrictions; unrelated casts and accessor targets are refused with STAGE-ESM-022.

Rules admit Contains(string)/Contains(char), Length, IsNullOrWhiteSpace, char.IsDigit, and string matching with Ordinal or OrdinalIgnoreCase. Bare StartsWith(string) and EndsWith(string) are validation-only: generated validation bodies pin and restore both current cultures to the invariant culture. Integer Sum is validation-only. Bodies execute in a checked context; overflow or another body exception fails validation rather than escaping the command.

context.Occurred reads the Arc operation’s receipt time, without a validator constructor dependency or wall-clock fallback. Generated specifications for these rules require given clock; without it the specification is omitted with STAGE-ESM-011. Rule-dependent specifications otherwise use their then blocks as the oracle and execute the rendered validators, including named-rule rejection assertions. Tenant and CausedBy remain refused with STAGE-ESM-015. Authored messages and severities are preserved, and every failed rule blocks the command regardless of severity. Missing bodies fail STAGE-ESM-020, envelope or compile errors STAGE-ESM-019, descriptor mismatches STAGE-ESM-021, and forbidden pure-body symbols STAGE-ESM-022. Optional validated concepts with body rules remain refused. Iterator bodies emit IEnumerable<string> methods wired through .Custom(...), adding an error for each yielded message in order. Only iterator validation roles admit yield return and interpolation; each interpolation is emitted through FormattableString.Invariant, including number and DateTimeOffset formatting, so messages do not depend on the host culture. Holes must use audited scalar formatting types and pure expressions; arbitrary object formatting and all other forbidden constructs remain refused. Boolean rules, reducers and policies still refuse interpolation and iterator syntax.

V26’s Date/DateTime comparisons in command requirements, produces when and validation rules execute in the semantic Host and specification executor, including through concepts. DateTime compares UTC ticks and requires an explicit input offset or Z after authorization; Date compares calendar days. then expectations remain ordinal.

The Cratis renderer renders these comparisons: CompareTo over DateOnly values or DateTimeOffset values built from the literal’s UTC ticks, in requirements, produces when and validation rules (absence passes). Each compared DateTime command property carries [property: ExplicitOffset] (from Cratis.Arc.Commands), so Arc refuses input without an offset or Z after authorization with the reference message. The rules render as server-side .Must predicates, so no client-side date comparator is extracted. Reactions do not admit date comparisons. Three-way conformance covers DateComparisonsCorpus in all four source forms.

Divergences and refusals (STAGE-ESM-046, naming the property):

  • A compared DateTime property typed through a concept binds as a plain DateTimeOffset, because Arc’s marker supports only DateTime and DateTimeOffset; it converts to the concept where an event or rule uses it. A concept with compliance, an optional concept-typed compared DateTime (an absent DateTimeOffset has no conversion back to an absent concept), or a property whose name is not camel case (Arc names the member in camel case), is refused. Pending Arc ask: a marker for concept-wrapped dates (Cratis/Arc#3350).
  • For a command with an authorization policy, an unauthorized caller sending an unbindable date body gets Validation, not 403. Stage renders native named Arc policies, and Arc’s resource-free evaluation covers only ASP.NET requirements. See command input binding and Cratis/Arc#3341.
  • Rendered then expectations compare DateTime values as instants, while the reference compares spelling; the binder already requires expected event values to match the produced spelling.
  • A comparison whose operand comes from a protected command read is not rendered for dates and stays refused.
  • The legacy engines still refuse these comparisons with STAGE-ESM-046.

V28 admits Uuid == and != in command require conditions and produces when guards. The semantic Host and specification executor compare Uuids ordinally in canonical lowercase hyphenated spelling, including through concepts and protected read paths, and reject a non-canonical input before the requirement runs. The Cratis renderer renders property and literal comparisons: concept properties compare by .Value and a literal compared with a Uuid operand becomes Guid.Parse("…"), so the comparison is a Guid comparison rather than a Guid-to-text one. A comparison whose operand is a protected read path, such as require PlantAccessGrant.memberId == requestedBy, renders too: the renderer resolves the read operand’s type through the read scope, so it compares as a Guid like any other operand. A read path in produces when keeps STAGE-ESM-044 (see Protected reads rendering). The legacy engines refuse every Uuid comparison with STAGE-ESM-049, naming the command and property, because they drop a right-hand property and do not check canonical spelling. Three-way conformance over the uuid-comparisons/v28 vector proves that the reference, the executor and the rendered application agree, and when_comparing_uuid_read_path_equality proves the same for the granted and denied read-path cases.

ESM v29 (Screenplay decision 0098) adds three constructs the semantic engines handle as follows:

  • decimals <= N on a Decimal concept or command property is checked by the executor and the Host with the reference rule (exact decimal arithmetic, trailing zeros ignored) and rendered as a generated validator, so a third decimal on a Money value is refused where it enters.
  • add/subtract <property> by <a> * <b> in a projection is run by the executor and the Host (Play) with exact decimal arithmetic, then converted to the type of the total like any operand. The renderer lowers it, because Chronicle’s projection engine adds one event property: the event record carries the product as a computed, serialized member (public decimal UnitPriceTimesQuantity { get; init; } = (decimal)UnitPrice.Value * (decimal)Quantity.Value;, whole numbers multiply checked) and the projection adds or subtracts that member. It is admitted in a from block when both factors are required whole or decimal properties of the event and the total holds the same kind of number (a decimal total for a decimal product, a whole total when both factors are whole); anything else (a join or every block, an optional factor, a literal factor, a total of another number type, a member name the event already has) is refused with STAGE-ESM-050, naming the projection and the reason. Events must be appended through the generated event types for the member to be present. Converting such a projection for Chronicle (ProjectionConverter) still reports STAGE-PROJECTION-004 at the mapping instead of converting the sum of one factor, and the specification runner’s per-run Chronicle projection does not run it.
  • A concept mapped to another concept over the same primitive needs no special handling: both are the same primitive at run time and in generated code.

An application toolbar button declared in a layout’s or module’s navigation topbar region (decision 0098) is carried as Scene’s NavigationButton (Cratis/Scene#215): its slice as the qualified id the structure knows, its emphasis and icon, and its authorize requirement as the policy expression it was written as. The region draws it after the items and opens the slice’s task dialog over the current page; a module’s buttons follow the items’ inherit, extend and override rules. The converter no longer reports STAGE-SCENE-NAV-001.

ESM v30 adds two more: a command whose produces ... for <property> names a required scalar property other than its identifier appends to several event sources (run and rendered like any command), and given generated <Command>.<property> = <value> pins the generated value of a command a reaction invokes. The executor feeds each invocation the next value pinned for its command and property in authored order, and the generated reaction specification carries the pinned value as the generated fixture of the invoked command. An invocation with nothing pinned stays Unsupported.

V27 admits read models with several key parts. The semantic Host runs them end to end: parts projection keys and removals, list queries with equality filters, single-instance queries by parts and the read-model browser, which shows each instance key as an object keyed by part name. The specification executor refuses a projection into such a read model as Unsupported(Projection), naming ESM v27 composite read-model keys, because its per-run scenario seeds and looks up instances by one event-source key. The Cratis renderer renders such a read model with a synthetic Id key record (<ReadModel>Key) holding the parts, alongside the declared properties, and a scoped projection that sets the parts through Chronicle’s UsingCompositeKey, for from blocks and remove with alike. List and filtered queries work as for any collection. Shapes the renderer cannot express are refused with STAGE-ESM-048, naming the reason: reducers and flat projections, joins, children, nested objects, every, optional, collection, date-time or non-primitive key parts, decimal key parts (until Chronicle keys 1.0 and 1.00 as one instance, Chronicle#5427; lifted by Stage#847), a property named Id, from or removal keys that are not a complete parts key, and protected live queries. STAGE-ESM-047 names each single-instance query by parts, because Chronicle cannot look up one read-model instance by a composite key until Chronicle#4265. Generated specifications that project into such a read model are not yet admitted (STAGE-ESM-011). The legacy engines refuse a read model with several key marks with STAGE-ESM-048.

For example, authored 9007199254740991, -9007199254740991 and 1234567890123456 render as 9007199254740991L, -9007199254740991L and 1234567890123456L; the fraction 0.10000000000000002 renders as 0.10000000000000002m. Command inputs, event payloads, validation operands, projection constants and generated specification expectations preserve those retained ESM values.

A retained non-route number whose legacy lowering would lose digits selects language/semantic v11 and schema 11. Already-lossless route literals and route-feeding inputs alone keep their earlier version. Reading a v1–v10 model does not change its numbers. Source still uses Double mode with whole numbers limited to ±9007199254740991; v11 neither admits numbers exact nor promises arbitrary-precision arithmetic. Generated fractions use C# decimal, not the full binary expansion of a Double value.

Moving an existing model from v7 to v8 widens stored event schemas from int32 to int64. Chronicle refuses that change within the same generation; plan a new event generation before upgrading (Stage #204). TypeScript properties remain number.

Root projection joins can correlate a required scalar event property with a read-model property of the same type. For example, a variant can retain the latest title for an issue even when the retitle event’s source is a different issue:

projection WorkItem
from IssueRetitled key issueId
title = title
variant Filed
enters on IssueFiled

Here Filed is a read model with an id String key and title String; IssueRetitled supplies issueId String and title String. Stage renders the root join with join.On(model => model.Id) and join.UsingKey(evt => evt.IssueId). The specification executor uses the same key, rather than the event source id. Omitting the key, or using $eventSourceId, retains event-source correlation. Literal, optional, mismatched, composite, event-context and child or nested join keys remain unsupported.

The Chronicle server must be 19.112.4 or later. Chronicle still backfills joins by event source id rather than by the explicit key (Chronicle#5439). Stage refuses a specification if a differently keyed join fact is followed by any root from event, including a later update to the event-source instance rather than the keyed instance. A subsequent command is conservatively refused too. This check applies across the given history and the action; it does not prevent the issue at runtime in a rendered application. Until Chronicle resolves the issue, append the root facts before differently keyed join events. Backfill with a key that equals the event source id remains admitted.

An application render writes GlobalUsings.cs at the backend root with global using directives for Cratis.Arc.MongoDB, Cratis.Chronicle.Keys, MongoDB.Driver and System.Reactive.Subjects. Generated files then leave out per-file usings for those namespaces. Where a name exists in two of them, such as Tag, it is qualified only as far as needed to resolve the collision. A scoped render keeps the per-file usings.

GlobalUsings.cs is owned by Stage, like Program.cs, and is regenerated on every application render, so edits to it are overwritten. Put application-specific global usings in a separate file, for example Usings.cs.

A rendered application hosts an Orleans silo through Cratis.Choreography, so jobs, event-sourced state machines and workflows have grains to run on. Program.cs calls AddCratisChoreography with the local configuration (localhost clustering and in-memory grain storage) and the workflow engine turned on, then registers AddEventSourcedStateMachines() and AddEventSourcedWorkflows(). Directory.Packages.props pins Cratis.Choreography and Cratis.Choreography.Chronicle to the versions Stage itself uses, and the project references them.

Choreography 2.x is wire-incompatible with 1.x silos: upgrade the entire cluster together; do not run a mixed cluster.

The local configuration starts with the application and needs no extra service, but jobs and workflow runs live in the application’s memory and do not survive a restart. Configure durable storage in Program.cs when runs must survive restarts.

Stage appends _ only when a command input property’s generated name collides with its command type, a record member, Handle, or GetEventSourceId. For example, command Text with input text renders property Text_, JSON field text_, and proxy field text_. Validation uses the escaped member too. Events and read-model properties retain their names. Hand-written implementation bodies must use the emitted command members.

A query colliding with its read-model type, EqualityContract, Clone, or a read-model property also receives _. Its proxy export follows the emitted method name; an Arc [Path] preserves the original HTTP route using the scaffold’s route settings.

A multi-field command response renders as record <Command>Response. When a response field, a sibling declaration, or a namespace already uses that name, the record and its proxy type are emitted as <Command>Response_; the response fields keep their names on the wire. For example, command Move beside command MoveResponse returns MoveResponse_.

STAGE-ESM-050 reports each escape as information and does not block rendering. Non-colliding members retain their existing output.

STAGE-ESM-012 still refuses duplicate normalized command properties, command types named Handle or GetEventSourceId, unsafe event/composite/response/concept members, unsafe read-model properties, identical query overloads, and colliding query parameters (including the injected readModels argument). Chronicle property-path collisions remain STAGE-ESM-017. These refusals identify the declaration rather than renaming stored data.

An observable ReadModel[] query renders as ISubject<IEnumerable<ReadModel>>, returning Chronicle’s readModels.Observe<ReadModel>() directly. A by key and equality filters become the expression passed to Observe. Chronicle compiles it once and evaluates it client-side after releasing the read models; it does not narrow server reads. Results are eventually consistent with the materialized projection or reducer, not command-side decision state. The first emission is the current collection, including an empty collection, followed by changes.

A plain GET can return HTTP 202 until the subject emits. Use ?waitForFirstResult=true to wait for the first result (HTTP 200), or Accept: text/event-stream to receive SSE data: frames as the collection changes. Generated src/stage.ts routes include waitForFirstResult=true, so list screens do not mistake a pending result for an empty list. The runtime frontend subscribes to those routes over SSE, so list screens update when another client’s command changes the data. The Host streams live (observable) queries; snapshot queries still fetch one result and refresh after commands.

Without observable, a list query renders as Task<IEnumerable<ReadModel>> using GetInstances and in-memory filtering. Snapshot lookup arguments retain their concept types. Live-query companion specifications append fixtures to EventStoreForTesting and await an emission from the generated query, executing its actual filter expression. Protected observable collection-success specifications use Arc’s QueryScenario with its default Chronicle read models. Scoped projections remain refused with STAGE-ESM-011; routed replay requires a readable generated Key, Subject, or Id on every pinned row. Protected snapshot collection success remains refused because Arc 22.87’s GetInstances does not read scenario-seeded rows. Both allowed and denied protected queries run through Arc’s authorization pipeline with the fixture caller.

A declarative query InvoiceById => observable InvoiceSummary optional with a by argument matching the read model’s sole identifier renders as ISubject<InvoiceSummary?>. This keyed optional shape is an exception to the observable single/optional refusal in the query-shape matrix above. Stage filters Chronicle’s observed collection by that identifier and maps each emission to its first matching instance, or null when none matches. Later updates and transitions back to no match remain observable. The adapter subscribes only when its consumer subscribes; disposing the consumer’s subscription releases the underlying observation. Key arguments retain their concept types.

Non-identifier keys, filtered optional lookups, and required single-result observable queries remain refused with STAGE-ESM-010. Screenplay rejects an unkeyed optional lookup before Stage sees it, because an unkeyed query must return a collection.

This support does not admit observable performer bodies. An observable collection performer remains an unverified STAGE-IMPL-006 gap with a non-runnable stub and STAGE-NOT-RUNNABLE.md. An observable optional performer is refused, because only a collection can be observed through a performer body. Live optional lookup companion specifications use Chronicle’s real observation and await the first emission rather than substituting a snapshot read.

ESM v14 equality filters render on snapshot and observable collection queries, with or without a by argument. Parameters follow the key in declaration order; all supplied filters and the key combine with AND. Optional filters render as T? name = null. Observable expressions use name == null || predicates because expression trees cannot contain is patterns; snapshot predicates retain name is null ||. Concept-typed filters run their concept validation rules through Arc, just like concept-typed query keys. An omitted optional filter is not validated.

A required filter renders as a non-nullable parameter. Arc rejects missing value-type and concept arguments with MissingArgumentForQuery. Arc treats plain reference parameters as optional, so Stage explicitly guards required text filters with that same validation failure before reading any rows. The Host instead returns the reference Contract rejection. Neither path silently widens a query with a missing required filter.

An omitted optional filter does not narrow the result. The reference refuses an explicitly supplied null; generated HTTP queries cannot distinguish explicit null from omission and skip it. This is a known live difference. The semantic Host preserves supplied null values and lets the reference reject them.

Snapshot collection queries are admitted. A filter that cannot preserve scalar comparison or parameter declaration order also fails 010, naming the query and filter. This includes DateTime filters: the reference compares retained text, whereas CLR equality compares instants and would merge distinct offset representations. Generated parameter collisions fail STAGE-ESM-012. Filtered companion specifications pass named filter values and assert row counts as well as expected rows. The independent specification executor and Host specification admission support scalar equality-filtered assertions over projected and seeded read-model state. Supplied filters combine with by using AND; omitted optional filters do not narrow. Results compare by read-model identity and key without regard to order, including row counts. Projection shapes the per-run scenario cannot preserve still receive typed refusals. The executor supports complete scalar per-key seeds and folds later facts over them. Generated specifications with keyed absence assertions also admit complete scalar seeds for independent projections. A seed replaces that key’s Given history; subsequent command or append facts update or remove it. Root removals render RemovedWith keyed by the authored event property when one is supplied, without removing other seeded keys. Presence at a different key can be asserted alongside absence, including without an action. Partial or null seeds, seeded decision reads, reducers, dependent scopes and seeded query combinations remain STAGE-ESM-011; the existing query-only lookup of one complete seeded record is unchanged. Models without collection queries keep their existing generated bytes.

ESM v13 renders personal-data concepts with [PII] and operational secrets with [Encrypted] plus [NotAudited]. Personal classification takes precedence when both are present. The markings travel with the ConceptAs type and protect different things: [Encrypted] and [PII] protect event payloads, while [NotAudited] and [PII] keep command inputs out of Arc’s command causation chain. [NotAudited] does not protect event payloads or logs. Identity uses and enum concepts refuse with STAGE-ESM-040 before emission. See Concept compliance rendering for scope mapping, registration caveats and the difference between generated applications and Play.

Chronicle stores enum members as integers, with an integer event schema and x-enumNames metadata. For numbered enums, ESM v12 carries each member’s int32 identity and Stage preserves it in C#:

public enum InvoiceStatus
{
Draft = -1,
Issued = 4,
Cancelled = 90,
}

Negative and sparse numbers are valid; -0 becomes 0. Reordering numbered members does not change their stored identities. Never renumber a published member or reuse its number. Specifications and conditions still compare member names. The world rebuilder maps a stored integer through the declared numbers, accepts declared model names, and refuses unknown numbers, fractions and undeclared names.

Unnumbered enums keep implicit declaration-order numbers: the first member is 0, the next is 1, and so on. For those enums, only append members; never reorder or remove existing members. Reordering changes which member a stored number identifies, and Chronicle’s schema compatibility check does not detect that remapping. Pins equal to declaration positions preserve the earlier ESM version and generated bytes. Numbering every member with its existing identity avoids future reorder hazards.

The Host’s event store is for the Host only. The Host registers its own event schemas, and they differ from the schemas a generated application registers: enums are strings rather than integers, and identifiers, dates, numbers and optional properties use different formats. Do not point the Host and a generated application at the same event store. Chronicle refuses the second registration of a changed schema with EventTypeSchemaChanged.

matches uses Screenplay’s portable match dialect: case-sensitive UTF-16 matching without culture-dependent character classes. \d and \w are ASCII; \s includes Unicode spaces and the byte-order mark (U+FEFF), but not next-line (U+0085). Dot excludes LF, CR, line separator (U+2028) and paragraph separator (U+2029). An unescaped $ outside a character class means end of input, not the position before a final newline: ^[A-Z]{3}$ accepts ABC but rejects ABC\n. Escaped dollars and dollars inside character classes remain literal. Unanchored patterns search the input rather than requiring a whole-input match.

The semantic executor and Host delegate match evaluation to Screenplay. Generated C# validators expand portable character sets and use strict anchors with .NET’s non-backtracking engine; they do not turn timeouts into validation failures. If .NET cannot compile an otherwise portable pattern (for example, an empty group with a counted repeat larger than .NET’s integer bound), rendering refuses it with STAGE-ESM-005, naming the construct. The renderer also retains .NET’s non-backtracking automaton-size limit rather than changing process-wide settings.

The topbar, sidebar, breadcrumb, footer and settings panel are the blueprint’s own. The Stage fills them: the breadcrumb shows where the current screen is, the topbar carries the blueprint’s theme switcher (unless option showThemeSwitcher false or a pinned colour scheme removes it) and, when the model declares more than one locale, a locale picker, and the menu is one of two things.

A model that declares navigation topbar and navigation sidebar (in the layout, and per module with inherits, extends or overrides) gets the shell’s own regions: the blueprint’s topbar and sidebar draw the effective navigation of the module the user is in (Scene’s effectiveNavigation; the Stage keeps no navigation tree of its own). Going to another module swaps the sidebar, the item of the page is marked (aria-current) in both regions, an item renders into the slot it names, and an item that targets a module opens the module’s first feature. The Stage tells Scene which screen each feature opened (screenFor: the screen declared in the feature) and which module a screen belongs to (moduleOf: the first of its path).

Where the user is is the address: #/OrderList restores the module, the feature and the slots after a reload or from a shared link. App takes an isVisible(item) predicate that hides an item - hidden, never disabled, so nothing about it leaks - at every level of a region; a host that knows what the user may see plugs it in.

The scene a running Stage serves carries the model’s structure (modules, features and slices) and commandSchemas (the JSON schema of every modeled command) only when a toolbar button names a slice; an application without one is byte-identical to what it was before slice buttons existed.

A toolbar button that names a State Change slice opens the slice as a task dialog over the page through Scene’s SliceDialogView. Its fields are what the command’s schema declares, read the way the native command form reads it (a property the form cannot present, such as a collection, is left out rather than shown as a field that cannot be filled). Submitting posts the command to the route the Stage registered for it, through the Stage’s slice invoker, exactly as an action or a form posts it: the application’s own validation results come back to the dialog against the fields they name, a rejected command keeps the dialog open, and a successful one closes it and refreshes what the screen shows.

Creation intent (x-creates) classifies a command as creating a row in some listed read model, not necessarily on the current screen; STAGE-INTENT-001 reports only guarded or ambiguous creation candidates, not ordinary updates.

A ui profile can name the application’s shell, configure it and set its look:

ui profile Web
root template blueprint Cratis.Blueprint.Default AppShell version "^4.5"
option navigationMode slim
option showFooter false
option showThemeSwitcher false
appearance
color scheme dark
accent "#4f46e5"
new screens use template MasterDetail
  • The shell is chosen by Scene’s resolveEffectiveShell (in .NET ShellResolver, through RootShell.Resolve), so the Stage uses Scene’s shell selection order: root, then the legacy layout, then the default blueprint’s layout, then the bare shell. Nothing in the Stage re-implements the order.
  • option navigationMode (static, overlay, slim, slim-plus, compact, horizontal, reveal, drawer), option menuTheme, option showFooter and option showThemeSwitcher (both flags, default true) are applied to the shell. The theme switcher is also left out when color scheme is light or dark, because the scheme is then pinned and there is nothing to switch, and when the blueprint offers only one theme. option showFooter false leaves out the footer, whose default text is the application’s name; an option or value the shell does not understand is reported, never silently dropped.
  • color scheme (light, dark, system) selects the dark or light theme (a system scheme follows the operating system), and accent becomes the theme’s primary colour.
  • A root template the application cannot start with - unknown, blueprint not installed, version pin not satisfied, no content outlet - is a build-time finding when planning (cratis render, the render plan) and, in a running Stage, an explicit start-up message that names the cause. The Stage never falls back to a different shell, and never shows a blank page.
  • The runtime scene carries root, appearance and defaults and, as always, no sample data (see below).

With no extra authoring the Stage reads like a product, not a debug view:

  • Navigation and breadcrumb. A screen is listed by an explicit navigation label when there is one; otherwise by the text of the first core:title at the top of the screen’s body (a title inside a section names that section, not the screen); otherwise by its name humanized. The breadcrumb’s last item uses the same text.
  • Humanized names. PascalCase and camelCase are split into words and written in sentence case, and a run of capitals stays as written: RegisterOpportunity is “Register opportunity”, RegisterSKU is “Register SKU”. A screen name also loses a trailing Screen, Page or Dialog: PipelineScreen is “Pipeline”.
  • Actions. An action’s label is its authored label, else the humanized command name. A command dialog is titled with that label and is submitted with the themed Button, labelled the same way (there is no “Execute” prefix) and, inside a dialog, a Cancel button beside it. Adjacent actions sit on one row; the first is primary and the next ones secondary, unless an action authors its emphasis (primary, secondary or quiet), which always wins.
  • Table values. A boolean reads Yes or No, and an ISO date or date-time reads in the locale of the user. Any other value, including text that only looks like a date, is shown as it is.
Section titled “Navigation items that render into a shell slot”

A navigation item authored with slot <name> renders its target into that outlet. When the outlet is one the shell declares - side in layout AppShell ... side, not a core:outlet some screen draws - the Stage hosts it: the page stays in the primary outlet, the target is drawn in a closable pane beside it, both items are marked active (aria-current), Close empties the pane, the pane stays when another page is opened and is restored from the address after a reload or a deep link. A shell without that slot opens the target in the primary outlet, as an item with no slot does. The Scene preview host and the Stage share Scene’s ApplicationHost, and navigationParity.spec.tsx clicks the same items in the Stage and in the preview host and requires the same pages, panes and marked items.

Sample data exists to preview with at design time. The runtime document and the scene a running Stage serves carry no sample data: no packs, no blueprint packs, and no selection in a profile (SceneApplication.ForRuntime). The browser scenarios assert it on the live Stage and on the generated application.

One composition for the Stage and the preview

Section titled “One composition for the Stage and the preview”

The Stage composes and hosts a screen through Scene’s composeApplicationScreen and ApplicationHost from @cratis/scene.blueprint.default. The Stage decides only what is its own: the chrome around the shell, its command form, its status line, its data sources, and the data and command scope it provides around the screen and the dialogs. The template wrapper and the flow arrangement view the Stage used to keep are Scene’s.

A model that declares no navigation regions keeps what it always had. When a feature declares contribute to Navigation entries, the menu is the application hierarchy the model wrote instead of a flat list of screen names: a group for the module and for each feature, nested as deep as the model nests features, with each entry under the feature that contributed it and ordered by its order. An entry links to the screen it opens; the current screen is marked, and the breadcrumb shows the groups leading to it. A feature’s entry reaches the runtime on every screen of that feature and is shown once. A model with no navigation entries keeps the flat list of screens.

A navigation entry carries everything it was authored with. Its id, icon, presentation and group reach the runtime. The id is the entry’s stable identity, so there is one entry per id. Entries that share a group are listed under a section heading of that name within their feature, and the breadcrumb leaves the section out. A destination outlet|dialog|external composes with its navigate to into one Scene destination: the entry opens in the named outlet, opens the named dialog, or leaves the application. The presentation hint decides what the menu shows - icon, text or both (the default) - and an icon-only entry keeps its label as its accessible name. An entry that opens somewhere a plain link does not reach is executed by the Scene navigation host, the same as a toolbar item.

A module’s own contribute to reaches every screen beneath the module, recorded against the module alone, and a contribute to written inside a screen records the module and features the screen sits in. Every screen carries that place as path: the modules and features, outermost first, it was declared in. A screen no entry lists is shown under its own module and feature - beside the entries that do list screens there - instead of a single “Other screens” group, and the breadcrumb leads to it through that place. Only a screen declared outside any module has no place to be under and goes under “Other screens”.

Section titled “Navigation host, outlets, dialogs and toolbars”

The Stage does not keep a navigation state of its own. Where the user is - the screen in the primary region, the screens in nested outlets, the dialog on top - and the address (#/Screen?parameter=value) that reaches it belong to the Scene navigation host (useSceneHistoryNavigation over createHashSceneHistory), the same way a generated or embedded host runs them. Destinations resolve through Scene: a core:outlet renders the screen placed in it inside the Stage’s data scope, a dialog destination opens the authored dialog template and closing it goes back, and an address that stands for an outlet destination restores the whole composition (the list with its details beside it), not just the details.

A toolbar renders as core:toolbar with a core:toolbarItem for each item. An action item announces its command with its parameters read from the screen; a navigate or dialog item carries the destination it opens (navigate to the screen, or the dialog template) and executes it through the host. An item’s presentation is honoured: mode is icon, text or both, and every other hint (placement, density, …) is exposed to themes as a data-scene-presentation-<name> attribute.

Themes are the blueprint’s (Scene Default Light and Scene Default Dark). The active theme’s colour scheme is mirrored onto the document root so PrimeReact overlays, which are portalled to body, follow it.

A ui profile that activates a package the target does not render, selects a layout, theme or root template that does not exist, or names packages that cannot be used together would publish an application with a different shell or unstyled components. CratisRendering.ResolveProfileInputs(scene, approvedPackages) resolves every profile with the Scene engine’s own RenderPlanner and returns one CratisProfileInputFinding for each input the target cannot honor, so a renderer consumer refuses before it writes or generates anything. Consumers must call this check before publication; the API alone does not enforce a publisher’s refusal policy.

CodeA profile input that
STAGE-PROFILE-001activates a package the target does not render and no approved package set supplies
STAGE-PROFILE-002selects a layout neither the application nor an active package declares
STAGE-PROFILE-003names packages that cannot be used together (incompatible theme, missing or conflicting dependency, dependency cycle)
STAGE-PROFILE-004selects a theme the application does not declare
STAGE-PROFILE-005names a root template that cannot be the shell (missing, blueprint not installed, version pin unsatisfied, no content outlet)
STAGE-PROFILE-006references a blueprint, version or template the approved blueprint does not provide

Forms, guarded actions, interactions and slice task dialogs execute registered Arc Command proxies. In rendered applications, src/bindings.ts registers the generated proxies. Commands honor the enclosing ArcContext’s microservice, API base path, origin and httpHeadersCallback, including identity headers. Only properties declared by the proxy are submitted; responses retain the proxy’s response type. Arc checks required properties in the browser before making the call, enforcing the same required-property rule as the server. Validation failures, authorization denials and exceptions remain failed outcomes and do not refresh the screen or expose a success response.

The live Stage registers equivalent route-backed proxies before its first render. Both live engines expose commandSchemas alongside commands and queries at /stage/routes: a map from command name to its JSON schema string, including commands used only by interactions or guarded alternatives. In the semantic engine, generated properties are server-owned and are not proxy inputs. commandResponseKinds describes the response from the modeled return metadata so proxies preserve strings, numbers, booleans and records. Commands without a declared return retain the host’s object echo. Commands register under their full type identity and route, with simple-name aliases only for unambiguous names. Missing schema metadata produces a console warning and leaves that command unregistered; other commands and the live UI remain available. If /stage/routes cannot be loaded, Stage warns and renders the scene without registered commands. This metadata does not add schemas to the scene document of an application without toolbar slice references. Query reads still use the Stage query transport.

A model with no screen still renders. The Stage synthesizes one screen per slice that carries a command or a read model and places it in the same shell. That is a fallback for a model that has not designed its screens yet; as soon as the model declares its own screens they are what is drawn.

CratisPlanOptions.ServiceLayout (and CratisRepositoryPlanOptions for the repository root) renders one application as a service of a repository that holds several. It defaults to false, and then every plan is byte-identical to the plan without the option: no path changes, no input joins the profile, and the digest is unchanged.

With ServiceLayout = true the profile carries the hashed input cratis-render:service-layout, version 1, with exact UTF-8 bytes service. Every artifact moves, byte for byte, into Source/{ProjectName}/, including scaffold, source plans, scoped plans, implementation-gap stubs and required removals. The files the repository owns are left out: .editorconfig, Directory.Build.props, Directory.Build.targets, Directory.Packages.props, {ProjectName}.slnx and docker-compose.yml. Files with those names in a sub-folder are application files and stay. Pass the same options, together with the same Profile, to every planning call. Planning never moves an existing repository; the caller decides to switch layouts.

CratisRendering.PlanRepository plans those repository files: the shared build files, a solution that lists every service and Source/Composition, docker-compose.yml, and an Aspire application host that starts Chronicle and every service. Composition is reserved for the host.

DiagnosticSeverityMeaning
STAGE-PLAN-028ErrorServiceLayout is combined with RepositoryRoot, which would give one application the repository-root files it must leave to the repository plan.
STAGE-PLAN-029ErrorPlanRepository options are invalid: no services, a name that is not a dot-separated C# identifier, Composition, a service whose Aspire resource name is chronicle (any casing), which the application host reserves for Chronicle, a duplicate service ignoring case, or two services that map to the same Aspire resource name.

Both codes are raised only for invalid option combinations, never for model constructs.