Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions Documentation/architecture/layering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
title: Layering and seams
description: The six layers a modeled application passes through, the rule that keeps them apart, and the invariants CI enforces so they stay that way.
---

A Screenplay document describes an application. Something has to turn that description into pixels a user can
click. Scene is the middle of that journey, and the whole design rests on one rule about who is allowed to know
what.

## The layers

```text
Screenplay (.play) The model. Declarative, portable, human-authored.
│ Knows nothing about React, URLs, HTTP or CSS.
▼ compile
Screenplay semantic model The compiler's answer: syntax → semantics.
│
▼ convert
Scene model The platform-neutral UI model. Serializable — the wire contract.
Cratis.Scene.Model Still knows nothing about React or URLs.
≡ @cratis/scene.model
│
▼ serve
Scene engine Pure functions over the model: resolve, evaluate, aggregate, bind.
@cratis/scene.engine Platform-neutral. No DOM. No framework.
│
▼
Scene renderer Turns engine output into platform widgets.
@cratis/scene.react Owns URLs, the DOM, effects and transports.
│
▼
Packages and blueprints core → components → primereact/tailwind → blueprints.
Blueprints supply the shell chrome and the template set.
```

## The seam rule

**Each layer may only know about the layer directly above it.** Four consequences do all the work:

| Rule | Why |
| --- | --- |
| The Scene **model** contains no URL, CSS class, React type or HTTP verb. | It is the wire contract, and a future MAUI or Compose renderer has to be able to honour it. |
| The Scene **engine** imports no framework and touches no `window`. | Its behaviour must be assertable without a DOM. That is what makes the data lifecycle testable rather than observable-by-screenshot. |
| The **renderer** is the only place a URL is constructed. | The model says `navigate to <Screen>`; deciding that means `/invoicing/invoices` is a platform decision. |
| Only **Stage** knows what an Arc endpoint is. | The engine asks a transport for a query's result; it never knows a route existed. |

The existing `NavBar` is the pattern in miniature: it takes a `renderRoute` callback rather than building a path,
because at that layer the path is not yet knowable. Every new surface follows it.

### What "platform-neutral" buys

It is tempting to read the seam rule as ceremony — the application is a web application, so why should the
engine pretend otherwise? Two reasons, both practical:

- **Testability.** A pure engine lets "entering this screen runs that query once, with these parameters, and does
not re-run on an unrelated change" be a unit test. Behind a DOM, the same assertion needs a browser and becomes
flaky.
- **Honesty about the model.** If the engine may reach for `window`, the model slowly stops being a model. The
first `location.href` is the point where the document stops describing an application and starts describing a
web page.

## The invariants

These are checked, not aspirational.

| # | Invariant | How it is checked |
| --- | --- | --- |
| I1 | The Scene model serializes to canonical, stable JSON. | `Stage` — `for_CanonicalSceneJson/when_serializing_the_same_scene_twice`. |
| I2 | The C# model and the TS mirror agree. | `scene-model-shape.json` plus both sides' shape specs. |
| I3 | The engine and model have no framework or DOM dependency. | ESLint: restricted imports and globals over `engine/**` and `model/**`. |
| I4 | No URL is constructed outside a renderer package. | ESLint: no route-template literals in `engine/**` or `model/**`. |
| I5 | Every reference either resolves or produces a diagnostic. | Compiler diagnostics plus the frontend finding surface. |
| I6 | A construct that reaches the model reaches the renderer, or is reported. | `Stage` — `for_UnrenderedConstructs`. |
| I7 | Enum members are never silently renumbered. | The append-only rule in Screenplay's `ast-compatibility.md`. |

### I3 and I4 in practice

Both live in `eslint.config.mjs` as a configuration block scoped to `Source/JavaScript/engine/**` and
`Source/JavaScript/model/**`. A violation fails the lint run with an explanation rather than a rule name:

```text
error I3: 'document' is a host environment API. The engine and the model must run without a DOM — take it
as an injected seam (a transport, a dispatcher, a store) supplied by the renderer.
error I4: no URL is constructed outside a renderer package. The model and the engine deal in screen
references and parameter maps; a renderer turns those into a path.
```

The same code in `react/**` lints clean. That asymmetry *is* the seam.

When the engine genuinely needs an effect, the answer is an injected seam, not an exception: a transport for
data, a dispatcher for actions, a store for state. The engine sequences; the renderer performs.

## Where decisions are recorded

The layering is the consequence of a set of recorded decisions rather than a matter of taste. The architecture
decision records covering it: interaction location, trigger vocabulary, blueprint as a selection, the URL
scheme, screen state ownership, the frontend failure posture, and multi-file project semantics.

## See also

- [Glossary](../glossary.md) — one meaning per word, shared with Screenplay.
- [Blueprints](../blueprints/index.md) — what a blueprint bundles and how a profile selects one.
73 changes: 73 additions & 0 deletions Documentation/glossary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
title: Glossary
description: The vocabulary Scene shares with Screenplay — layouts, slots, templates, elements, behaviors, actions and routes — with one meaning per word.
---

Scene renders what Screenplay declares, so the two share a vocabulary. Where a word already means something in
Screenplay, that meaning wins here — this page says what each term is *in Scene*, and which record or type is
authoritative.

## The document's shapes

These arrive from the model. Scene does not reinterpret them.

| Term | Meaning | Authoritative in |
| --- | --- | --- |
| **Layout** | The application's base navigational shell. One per build, selected by a UI profile. | `Model/Layouts/Layout` |
| **Slot** | A named region a layout or template declares. Filled by one parent, or opened to many by declaring a contribution point. | `Model/Layouts/Slot` |
| **Screen template** | A reusable shape inside the shell. Says which slot of its parent it fills with `fits slot`; nesting is matched by slot name, to any depth. | `Model/Screens/ScreenTemplate` |
| **Dialog template** | A shape that opens *over* the application. Never fills a slot. | `Model/Screens/DialogTemplate` |
| **Screen** | An instance filling a template's slots. Never names a layout directly. | `Model/Screens/Screen` |
| **Element** | A node in a screen's content tree. | `Model/Elements/SceneElement` |
| **Arrangement** | How a layout or template positions its slots — `flow` or `freeform`, varying by size class. | `Model/Layouts/Arrangement` |
| **Size class** | `compact`, `regular` or `expanded`, on the width and height axes. A class, not a pixel breakpoint. | `Model/SizeClasses/` |
| **Contribution point** | A named many-to-one extension point declared on a slot. | `Model/ContributionPoints/ContributionPoint` |
| **Contribution** | One piece of content targeting a contribution point. | `Model/ContributionPoints/Contribution` |
| **Form** | A command or query input surface with its fields and how they populate. | `Model/Forms/Form` |
| **UI profile** | A build's selections: platform, size, layout, theme, packages, blueprint, start screen. Names artifacts; never contains them. | `Model/Profiles/UiProfile` |

## Interaction

What happens when a user does something. The vocabulary is closed and every operand is a model reference, which
is what makes an interaction that names nothing real a diagnostic instead of a dead control.

| Term | Meaning |
| --- | --- |
| **Interaction trigger** | What starts an interaction — a click, a submit, a screen becoming live, an observed domain event, an elapsed interval, or a declared application trigger firing. Anonymous: it exists only inside a behavior. |
| **Application trigger** | Screenplay's declared top-level signal consumed by backend reactions. A different concept with a similar name — an interaction may `raise` one, which is how the two connect. |
| **Action** | One declarative effect: execute a command, navigate, open or close a dialog, refresh a query, set state, notify, confirm, raise. |
| **Continuation** | The `on success` / `on failure` / `on result` actions that run after an action that can fail. |
| **Behavior** | A bundle of interaction-trigger-to-action bindings, attachable to an element, form, screen, template, layout, module or feature. Anonymous when written inline; named when reusable. Attachments are additive — a template's behavior and an element's both run. |
| **Action dispatcher** | The seam a renderer implements, one method per action kind. The engine sequences actions and handles continuations; the dispatcher performs the effects. |
| **Interaction context** | What an action's bindings resolve against: route parameters, screen state, the triggering item, the form's values, the trigger's payload. |

## Runtime

| Term | Meaning |
| --- | --- |
| **Route** | The renderer's concrete address for a screen. The model never contains one. |
| **Screen state** | A screen's declared values: route-backed `accepts`, and transient `state`. Owned by the engine, subscribed to by the renderer. |
| **Query state** | Where a query-backed element stands: idle, loading, loaded or failed. |
| **Finding** | A structured report that something did not resolve. Rendered in place and collectable, so "this application has no findings" is a check rather than an opinion. |

## Packaging

| Term | Meaning |
| --- | --- |
| **Package** | A named set of components a profile draws from, in override-priority order. `core` is the final fallback. |
| **Blueprint** | A shipped bundle: layouts, shell chrome, a template set and theme tokens. Selected by name in a UI profile, the same way a theme is. |
| **Theme** | A set of visual tokens, compatible with one or more packages. |
| **Shadowing** | A higher-priority package supplying a component by the same name, overriding a lower one. |

## The layers themselves

| Term | Meaning |
| --- | --- |
| **Scene model** | The platform-neutral UI model: `Cratis.Scene.Model` in C#, mirrored by `@cratis/scene.model`. The wire contract. |
| **Scene engine** | `@cratis/scene.engine` — pure functions over the model. No DOM, no framework. |
| **Scene renderer** | `@cratis/scene.react` and its siblings. Owns URLs, the DOM, effects and transports. |

## See also

- [Layering and seams](architecture/layering.md) — which layer may know what, and the invariants that enforce it.
- Screenplay's own glossary — the language-side definition of every modeled construct.
4 changes: 4 additions & 0 deletions Documentation/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

- name: Understand
items:
- name: Layering and seams
href: architecture/layering.md
- name: Glossary
href: glossary.md
- name: Packages
href: packages/toc.yml

Expand Down
2 changes: 1 addition & 1 deletion Source/DotNET/Engine/Starters/StarterProfileBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ public static class StarterProfileBuilder
/// <param name="targetPlatform">The platform the gallery runs on (e.g. <c language="csharp">web</c>).</param>
/// <param name="defaultSizeClass">The size class assumed when the renderer cannot otherwise determine one.</param>
/// <returns>A <see cref="UiProfile"/> named after the starter, scoped to exactly its own <see cref="UiStarter.Packages"/>.</returns>
public static UiProfile BuildProfile(UiStarter starter, string targetPlatform, SizeClass? defaultSizeClass = null) =>
public static UiProfile BuildProfile(UiStarter starter, string targetPlatform, TargetSizeClass? defaultSizeClass = null) =>
new(starter.Name, targetPlatform, starter.Packages, defaultSizeClass);
}
10 changes: 10 additions & 0 deletions Source/DotNET/Model/Elements/SceneElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Text.Json.Serialization;
using Cratis.Scene.Model.Elements.Panels;
using Cratis.Scene.Model.Interactions;

namespace Cratis.Scene.Model.Elements;

Expand All @@ -29,4 +30,13 @@ public abstract record SceneElement
/// Gets an open bag of properties not otherwise captured by the typed hierarchy below this type.
/// </summary>
public IReadOnlyDictionary<string, object?> Properties { get; init; } = new Dictionary<string, object?>();

/// <summary>
/// Gets the behaviors attached to the element - what happens when someone interacts with it.
/// </summary>
/// <remarks>
/// On the element rather than on a wrapper, because interaction belongs to the thing being interacted with.
/// Attachments are additive with whatever a screen, template, layout, module or feature attached further out.
/// </remarks>
public IReadOnlyList<Behavior> Behaviors { get; init; } = [];
}
10 changes: 9 additions & 1 deletion Source/DotNET/Model/Forms/Form.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Cratis.Scene.Model.Interactions;

namespace Cratis.Scene.Model.Forms;

/// <summary>
Expand All @@ -10,4 +12,10 @@ namespace Cratis.Scene.Model.Forms;
/// <param name="ForCommand">The resolved name of the command the form builds.</param>
/// <param name="PopulateSource">Where the form's initial values come from, if any.</param>
/// <param name="Fields">The form's fields.</param>
public record Form(string Name, string ForCommand, PopulateSource? PopulateSource, IReadOnlyList<FormField> Fields);
public record Form(string Name, string ForCommand, PopulateSource? PopulateSource, IReadOnlyList<FormField> Fields)
{
/// <summary>
/// Gets the behaviors attached to the form. Additive with whatever is attached further out or further in.
/// </summary>
public IReadOnlyList<Behavior> Behaviors { get; init; } = [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Scene.Model.Interactions;

/// <summary>
/// Represents an interaction started by a declared application trigger firing.
/// </summary>
/// <param name="TriggerName">The declared application trigger.</param>
public record ApplicationTriggerInteractionTrigger(string TriggerName) : InteractionTrigger;
24 changes: 24 additions & 0 deletions Source/DotNET/Model/Interactions/Behavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Scene.Model.Interactions;

/// <summary>
/// Represents a bundle of interaction trigger to action bindings, attached to something that can be
/// interacted with.
/// </summary>
/// <param name="Name">The behavior's name, or <see langword="null"/> when it was written inline on what it is attached to.</param>
/// <param name="Bindings">The trigger to action bindings.</param>
/// <param name="Order">Where this behavior runs among the others attached to the same thing, when it says.</param>
/// <remarks>
/// A named behavior and an inline one are the same shape - anonymity is a property rather than a separate kind
/// of node - so the engine has one resolution path and a renderer has one thing to subscribe to.
/// <para>
/// Attachments are additive: a behavior on a template and a behavior on an element both run, outermost first,
/// unless <paramref name="Order"/> says otherwise.
/// </para>
/// </remarks>
public record Behavior(
string? Name,
IReadOnlyList<InteractionBinding> Bindings,
int? Order = null);
10 changes: 10 additions & 0 deletions Source/DotNET/Model/Interactions/BuiltInInteractionTrigger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Scene.Model.Interactions;

/// <summary>
/// Represents one of the built-in interaction kinds.
/// </summary>
/// <param name="Kind">The kind.</param>
public record BuiltInInteractionTrigger(InteractionTriggerKind Kind) : InteractionTrigger;
13 changes: 13 additions & 0 deletions Source/DotNET/Model/Interactions/CloseDialogAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Scene.Model.Interactions;

/// <summary>
/// Dismisses the innermost dialog.
/// </summary>
public record CloseDialogAction : InteractionAction
{
/// <inheritdoc/>
public override InteractionActionKind Kind => InteractionActionKind.CloseDialog;
}
14 changes: 14 additions & 0 deletions Source/DotNET/Model/Interactions/ConfirmAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Scene.Model.Interactions;

/// <summary>
/// Gates the actions that follow on the user agreeing.
/// </summary>
/// <param name="Message">What to ask.</param>
public record ConfirmAction(InteractionMessage Message) : InteractionAction
{
/// <inheritdoc/>
public override InteractionActionKind Kind => InteractionActionKind.Confirm;
}
10 changes: 10 additions & 0 deletions Source/DotNET/Model/Interactions/EventInteractionTrigger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Scene.Model.Interactions;

/// <summary>
/// Represents an interaction started by observing a modeled domain event.
/// </summary>
/// <param name="EventName">The event being observed.</param>
public record EventInteractionTrigger(string EventName) : InteractionTrigger;
14 changes: 14 additions & 0 deletions Source/DotNET/Model/Interactions/ExecuteCommandAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Scene.Model.Interactions;

/// <summary>
/// Submits a modeled command.
/// </summary>
/// <param name="Command">The command to submit.</param>
public record ExecuteCommandAction(string Command) : InteractionAction
{
/// <inheritdoc/>
public override InteractionActionKind Kind => InteractionActionKind.ExecuteCommand;
}
Loading
Loading