diff --git a/Directory.Packages.props b/Directory.Packages.props
index 12afab62..71003fcd 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -5,9 +5,9 @@
22.54.0
0.24.0
0.18.0
- 4.147.1
+ 4.154.0
- 4.92.0
+ 4.96.0
diff --git a/Documentation/reference/llm-context.md b/Documentation/reference/llm-context.md
index 7d86ce4e..6c5b5f04 100644
--- a/Documentation/reference/llm-context.md
+++ b/Documentation/reference/llm-context.md
@@ -20,8 +20,8 @@ Every command in the catalog carries an `effect` field that states the strongest
| `effect` | Meaning | Examples |
|---|---|---|
-| `read-only` | Observes only. Reads from the Chronicle server, an application, or local files and changes nothing. | `chronicle observers list`, `context show`, `screenplay validate` |
-| `local` | Changes only the local machine: CLI configuration and contexts, cached credentials, files in the working directory, shell configuration, installed tools, or local containers. Never changes server or store state. | `context set`, `chronicle login`, `init`, `ai install` |
+| `read-only` | Observes only. Reads from the Chronicle server, an application, or local files and changes nothing. | `chronicle observers list`, `context show`, `screenplay validate`, `screenplay baseline check` |
+| `local` | Changes only the local machine: CLI configuration and contexts, cached credentials, files in the working directory, shell configuration, installed tools, or local containers. Never changes server or store state. | `context set`, `chronicle login`, `init`, `ai install`, `screenplay baseline admit` |
| `mutating` | Changes Chronicle server or store state without removing or resetting existing state. | `chronicle users add`, `chronicle jobs stop`, `chronicle observers retry-partition`, `chronicle observers clear-partition-quarantine` |
| `destructive` | Removes or resets Chronicle server or store state. | `chronicle users remove`, `chronicle observers replay`, `chronicle recommendations perform` |
diff --git a/Documentation/reference/screenplay-mcp.md b/Documentation/reference/screenplay-mcp.md
index f9ee31de..c02842d6 100644
--- a/Documentation/reference/screenplay-mcp.md
+++ b/Documentation/reference/screenplay-mcp.md
@@ -3,9 +3,9 @@ title: Screenplay MCP
description: Run the embedded Screenplay MCP server and configure project-local AI host registrations safely.
---
-`cratis screenplay mcp` hosts the embedded Screenplay 4.147.1 Model Context Protocol (MCP) server over standard input and output. `cratis version` shows the bundled compiler version; JSON output includes `screenplay.version`. It does not install tools, download packages, or check for CLI updates at startup. Native and Homebrew CLI distributions include the runtime; this command does not require a separate .NET installation.
+`cratis screenplay mcp` hosts the embedded Screenplay 4.154.0 Model Context Protocol (MCP) server over standard input and output. `cratis version` shows the bundled compiler version; JSON output includes `screenplay.version`. It does not install tools, download packages, or check for CLI updates at startup. Native and Homebrew CLI distributions include the runtime; this command does not require a separate .NET installation.
-The server can author and bind every ESM version the bundled Screenplay compiler reads; this does not establish renderability. The CLI's bundled Stage 4.92.0 renderer admits ESM v1 through v15, including event sources, streams and routes, v14 equality query filters on snapshot and observable collection queries and v15 concept generation strategies, and renders policy negation, authored XML documentation, non-optional UUID-backed generated concepts as Arc generated-value parameters, scalar and named scalar command responses, and supported event-triggered Automation slices. Observable collections return Chronicle's live read-model stream through Arc; snapshot collections return a one-time list. Projection `all` blocks whose mappings set event-source identities render as Chronicle `FromAll`; literal-mapping or mapping-less blocks still report `STAGE-ESM-017`. PascalCase event-source names render as `{Name}EventSource` with public stream-name constants; colliding constants report `STAGE-ESM-012`. Unsupported generated types (`STAGE-ESM-028`), unsupported generation/response combinations (`STAGE-ESM-038`), mixed reaction effects (`STAGE-ESM-037`), captures, non-event triggers and Translate slices (`STAGE-ESM-024`) remain refused, as do read-model absence assertions (`STAGE-ESM-027`), public events and event-target projections and reducers (`STAGE-ESM-032`), foreign origins and event-source captures (`STAGE-ESM-033`), system-identity reactions that invoke commands whose authorization reads claims or uses opaque policies (`STAGE-ESM-034`) and per-production route overrides (`STAGE-ESM-036`); reaction `runs as` identity and reaction and reducer observer filters render. A specification whose unauthenticated caller carries roles or claims is refused with `STAGE-ESM-011`, including the unchanged canonical negation example. See [Screenplay rendering](screenplay.md#executable-model-versions) for command-specific compatibility.
+The server can author and bind every ESM version the bundled Screenplay compiler reads; this does not establish renderability. The CLI's bundled Stage 4.96.0 renderer admits ESM v1 through v16, including event sources, streams and routes, v14 equality query filters on snapshot and observable collection queries, v15 concept generation strategies and v16 stored event names, and renders policy negation, authored XML documentation, non-optional UUID-backed generated concepts as Arc generated-value parameters, scalar and named scalar command responses, and supported event-triggered Automation slices. An event's `id` pin keeps its stored event type name when you rename it. Every render checks existing event type ids before publication; changes or unreadable ids report `CLI-RENDER-016` with `STAGE-EVENT-002`. Observable collections return Chronicle's live read-model stream through Arc; snapshot collections return a one-time list. Projection `all` blocks whose mappings set event-source identities render as Chronicle `FromAll`; literal-mapping or mapping-less blocks still report `STAGE-ESM-017`. PascalCase event-source names render as `{Name}EventSource` with public stream-name constants; colliding constants report `STAGE-ESM-012`. Unsupported generated types (`STAGE-ESM-028`), unsupported generation/response combinations (`STAGE-ESM-038`), mixed reaction effects (`STAGE-ESM-037`), captures, non-event triggers and Translate slices (`STAGE-ESM-024`) remain refused, as do read-model absence assertions (`STAGE-ESM-027`), public events and event-target projections and reducers (`STAGE-ESM-032`), foreign origins and event-source captures (`STAGE-ESM-033`), system-identity reactions that invoke commands whose authorization reads claims or uses opaque policies (`STAGE-ESM-034`) and per-production route overrides (`STAGE-ESM-036`); reaction `runs as` identity and reaction and reducer observer filters render. A specification whose unauthenticated caller carries roles or claims is refused with `STAGE-ESM-011`, including the unchanged canonical negation example. See [Screenplay rendering](screenplay.md#executable-model-versions) for command-specific compatibility.
For Claude Desktop and ChatGPT Desktop **installation**, see
[Screenplay desktop MCP](screenplay-desktop-mcp.md). Desktop management uses
diff --git a/Documentation/reference/screenplay.md b/Documentation/reference/screenplay.md
index d64c9c2f..d3d6bb8c 100644
--- a/Documentation/reference/screenplay.md
+++ b/Documentation/reference/screenplay.md
@@ -8,11 +8,13 @@ cratis render --workspace application.workspace.json
cratis screenplay generate [PATH]
cratis screenplay conform [--project ]
cratis screenplay validate [PATH]
+cratis screenplay baseline check [ROOT]
+cratis screenplay baseline admit [ROOT] --release [--from-esm ]
cratis screenplay mcp [PATH]
cratis screenplay desktop install
```
-The CLI bundles Screenplay 4.147.1 (including its MCP server), Stage 4.92.0, and the Arc source adapter 22.54.0. `cratis render` admits ESM v1 through v15, including the canonical screen-composition corpus observable query and query-only specification shapes, and preserves authored Scene layouts, profiles, templates, navigation and forms when a model declares screens. Guarded screen actions render as runnable Scene guards: the generated frontend evaluates the authored guard, including enum-typed comparisons against the read model's declared members, and shows the action only when it passes. The renderer still refuses unsupported constructs within those versions: captures, non-event triggers and Translate slices report `STAGE-ESM-024`; supported event-triggered Automation slices render, while reactions mixing event production and command invocation report `STAGE-ESM-037`, read-model absence assertions report `STAGE-ESM-027`, and unsupported v9 and v10 constructs report the codes listed in the admission table below. Reaction system identities and reaction/reducer observer filters render within their supported subset. It also refuses an evolved event until Stage can render its event-type migrations. Validation rejects `numbers exact` (`PLAY0268`) and `implementation` attachments on concept, built-in, and whole-command rules.
+The CLI bundles Screenplay 4.154.0 (including its MCP server), Stage 4.96.0, and the Arc source adapter 22.54.0. `cratis render` admits ESM v1 through v16, including the canonical screen-composition corpus observable query and query-only specification shapes, and preserves authored Scene layouts, profiles, templates, navigation and forms when a model declares screens. Guarded screen actions render as runnable Scene guards: the generated frontend evaluates the authored guard, including enum-typed comparisons against the read model's declared members, and shows the action only when it passes. The renderer still refuses unsupported constructs within those versions: captures, non-event triggers and Translate slices report `STAGE-ESM-024`; supported event-triggered Automation slices render, while reactions mixing event production and command invocation report `STAGE-ESM-037`, read-model absence assertions report `STAGE-ESM-027`, and unsupported v9 and v10 constructs report the codes listed in the admission table below. Reaction system identities and reaction/reducer observer filters render within their supported subset. It also refuses an evolved event until Stage can render its event-type migrations. Validation rejects `numbers exact` (`PLAY0268`) and `implementation` attachments on concept, built-in, and whole-command rules.
For the embedded stdio server, the `visualize-model` MCP App, and native AI host registration, see [Screenplay MCP](screenplay-mcp.md). For Claude Desktop and ChatGPT Desktop installation, see [Screenplay desktop MCP](screenplay-desktop-mcp.md).
@@ -28,11 +30,11 @@ A newer bundled compiler can produce a newer executable semantic model (ESM) ver
|---|---|
| `cratis screenplay validate` | Admitted. Without `--executable` it parses and merges v7 syntax and runs the source checks only; with `--executable` it also binds the model and reports binding errors. |
| `cratis screenplay mcp` | Admitted for authoring and reading. The embedded server binds v7 models, pages their executable model (generated properties, responses, generation fixtures and return expectations) and reports execution readiness. The embedded server offers deterministic reference execution through `run-specifications`, including retained proposal candidates. |
-| `cratis render` | Admitted. Policy negation renders. Server-generated, non-optional UUID-backed concepts render as Arc generated-value parameters. Scalar responses and named responses whose fields come from scalar command properties render. Unsupported generated types report `STAGE-ESM-028`; unsupported generation/response combinations report `STAGE-ESM-038`. Stage refuses a specification whose unauthenticated caller carries roles or claims with `STAGE-ESM-011`; this includes the unchanged canonical negation example. A refusal leaves no publishable artifacts. Versions newer than v15 are still blocked by `CLI-RENDER-004`, naming the version. |
+| `cratis render` | Admitted. Policy negation renders. Server-generated, non-optional UUID-backed concepts render as Arc generated-value parameters. Scalar responses and named responses whose fields come from scalar command properties render. Unsupported generated types report `STAGE-ESM-028`; unsupported generation/response combinations report `STAGE-ESM-038`. Stage refuses a specification whose unauthenticated caller carries roles or claims with `STAGE-ESM-011`; this includes the unchanged canonical negation example. A refusal leaves no publishable artifacts. Versions newer than v16 are still blocked by `CLI-RENDER-004`, naming the version. |
| `cratis screenplay generate` | From Arc 22.52.0, the Arc adapter recovers supported generated UUID values and command responses by default for commands without successful scenarios. Output containing these constructs compiles to ESM v7, which `cratis render` admits. The renderer supports non-optional UUID-backed generated concepts and scalar or named scalar command responses; unsupported types or combinations still receive typed refusals. Unsupported shapes and values needed by pre-generation protection remain in code with `SP0052`. Marten and Critter Stack retain their existing recovery surface. |
-| `cratis run` | The Stage 4.92.0 container compiles the mounted `.play` files with its own bundled Screenplay compiler. Passing `cratis screenplay validate` does not establish Stage support for every construct. |
+| `cratis run` | The Stage 4.96.0 container compiles the mounted `.play` files with its own bundled Screenplay compiler. Passing `cratis screenplay validate` does not establish Stage support for every construct. |
-The bundled Screenplay reader supports ESM schemas through v15. ESM v5 adds keyed read-model absence assertions; v8 adds event sources, streams, command routes and specification routes. ESM v9 adds public events, foreign event origins, translation direction, event-target projections/reducers and event-source captures; v10 adds reaction system identity (`runsAs`), per-production route overrides (`SemanticProducedEvent.Route`) and observer filters (`SemanticReaction.From` and `SemanticReducer.From`). Version reading and command admission are separate:
+The bundled Screenplay reader supports ESM schemas through v16. ESM v5 adds keyed read-model absence assertions; v8 adds event sources, streams, command routes and specification routes. ESM v9 adds public events, foreign event origins, translation direction, event-target projections/reducers and event-source captures; v10 adds reaction system identity (`runsAs`), per-production route overrides (`SemanticProducedEvent.Route`) and observer filters (`SemanticReaction.From` and `SemanticReducer.From`). Version reading and command admission are separate:
| Command | ESM v8 admission | ESM v9 admission | ESM v10 admission |
|---|---|---|---|
@@ -42,10 +44,10 @@ The bundled Screenplay reader supports ESM schemas through v15. ESM v5 adds keye
| `cratis screenplay generate` | Checks recovered source with the bundled compiler. The Arc adapter's output remains ESM v7; recovery is provider-owned. | Does not execute or render these constructs. Recovery remains provider-owned. | Does not execute or render these constructs. Recovery remains provider-owned. |
| `cratis view` | Serves embedded or generated source through the bundled event-model explorer. | Viewing is not renderer admission. | Viewing is not renderer admission. |
| Generation statistics | Count declarations in compiled source, including documents with event routes. | Counts do not establish executable or renderer support. | Counts do not establish executable or renderer support. |
-| `cratis render` | Admitted through Stage 4.92.0, including routed commands and supported routed specifications. PascalCase event-source names render as `{Name}EventSource` with public stream-name constants; colliding constants report `STAGE-ESM-012`. Unsupported constructs still block publication. | Admitted for construct-level planning. Public events and event-target projections/reducers report `STAGE-ESM-032`; foreign origins and event-source captures report `STAGE-ESM-033`; translation direction reports `STAGE-ESM-024`. | Admitted for construct-level planning. Reaction `runsAs` renders with the declared system roles; invoking a command whose authorization reads claims or uses opaque policies reports `STAGE-ESM-034`. Reaction `from` filters (`SemanticReaction.From`) render as Chronicle source/stream attributes. Per-production route overrides (`SemanticProducedEvent.Route`) report `STAGE-ESM-036`. Reducer `from` filters (`SemanticReducer.From`) render as Chronicle source/stream attributes; specifications requiring filtered read-model replay remain refused. |
-| `cratis run` | Refused by the Stage 4.92.0 container: `cratis run` starts Stage's default `eventmodel` engine, which reports `STAGE-ESM-030` for named event sources, streams and routes because only Stage's semantic engine runs them. Render the model with `cratis render` instead. | Stage applies its own runtime admission; CLI validation is not Stage admission. | Stage applies its own runtime admission; CLI validation is not Stage admission. |
+| `cratis render` | Admitted through Stage 4.96.0, including routed commands and supported routed specifications. PascalCase event-source names render as `{Name}EventSource` with public stream-name constants; colliding constants report `STAGE-ESM-012`. Unsupported constructs still block publication. | Admitted for construct-level planning. Public events and event-target projections/reducers report `STAGE-ESM-032`; foreign origins and event-source captures report `STAGE-ESM-033`; translation direction reports `STAGE-ESM-024`. | Admitted for construct-level planning. Reaction `runsAs` renders with the declared system roles; invoking a command whose authorization reads claims or uses opaque policies reports `STAGE-ESM-034`. Reaction `from` filters (`SemanticReaction.From`) render as Chronicle source/stream attributes. Per-production route overrides (`SemanticProducedEvent.Route`) report `STAGE-ESM-036`. Reducer `from` filters (`SemanticReducer.From`) render as Chronicle source/stream attributes; specifications requiring filtered read-model replay remain refused. |
+| `cratis run` | Refused by the Stage 4.96.0 container: `cratis run` starts Stage's default `eventmodel` engine, which reports `STAGE-ESM-030` for named event sources, streams and routes because only Stage's semantic engine runs them. Render the model with `cratis render` instead. | Stage applies its own runtime admission; CLI validation is not Stage admission. | Stage applies its own runtime admission; CLI validation is not Stage admission. |
-Version admission does not admit every construct. A selected scope without unsupported constructs can plan normally at v9 or v10; a typed renderer refusal leaves no publishable artifacts. ESM v11 (lossless lowering of Double-mode number literals within ±(2^53−1); `numbers exact` documents still do not bind), v12 enum member numbers and v13 concept compliance are admitted by the bundled renderer. Snapshot collection queries render as one-time queries; observable collection queries return Chronicle's live read-model stream through Arc. Both support a `by` key and ESM v14 equality query filters, with optional filters narrowing only when supplied; DateTime filters remain refused with `STAGE-ESM-010`. Protected observable collection-success specifications remain refused with `STAGE-ESM-011`; denial specifications still render. Projection `all` blocks whose mappings set event-source identities render as Chronicle `FromAll`; literal-mapping or mapping-less `all` blocks remain refused with `STAGE-ESM-017`. ESM v15 concept generation strategies render with freshness scopes on generated event sources. Time-ordered factories use the wall clock, not a command-receipt timestamp. Newer versions remain blocked by `CLI-RENDER-004` before target planning or publication. Non-text secret concepts without personal-data classification are refused by both the renderer and Host with `STAGE-ESM-041`.
+Version admission does not admit every construct. A selected scope without unsupported constructs can plan normally at v9 or v10; a typed renderer refusal leaves no publishable artifacts. ESM v11 (lossless lowering of Double-mode number literals within ±(2^53−1); `numbers exact` documents still do not bind), v12 enum member numbers and v13 concept compliance are admitted by the bundled renderer. Snapshot collection queries render as one-time queries; observable collection queries return Chronicle's live read-model stream through Arc. Both support a `by` key and ESM v14 equality query filters, with optional filters narrowing only when supplied; DateTime filters remain refused with `STAGE-ESM-010`. Protected observable collection-success specifications remain refused with `STAGE-ESM-011`; denial specifications still render. Projection `all` blocks whose mappings set event-source identities render as Chronicle `FromAll`; literal-mapping or mapping-less `all` blocks remain refused with `STAGE-ESM-017`. ESM v15 concept generation strategies render with freshness scopes on generated event sources. Time-ordered factories use the wall clock, not a command-receipt timestamp. ESM v16 stored event names render: an event's `id` pin keeps its stored Chronicle event type name when you rename the event. For example, an event named `Renamed` with `id "Original"` renders with `[EventType("Original")]`. Newer versions remain blocked by `CLI-RENDER-004` before target planning or publication. Non-text secret concepts without personal-data classification are refused by both the renderer and Host with `STAGE-ESM-041`.
### Routed command example
@@ -82,6 +84,8 @@ The model binds to ESM v8. Rendering emits `EventSources/AccountEventSource.cs`
Compiles one `.play` file, every `.play` file beneath one folder, or the exact document set in a canonical workspace as a single logical application, then asks a statically bundled renderer target for a complete artifact plan. Authored `description` and event `documentation` are rendered as XML doc comments (`` and ``). Metadata the renderer cannot use, such as characters that are invalid in XML, blocks rendering with `CLI-RENDER-005`. When the documents cannot be read as a Screenplay workspace, for example a file with an upper-case `.PLAY` extension, the model renders without the comments and the `CLI-RENDER-006` warning says why. New artifacts are published only after source compilation, ESM binding, execution-plan admission, target planning, and artifact validation all succeed. Without `--check`, recovery of an interrupted earlier publication still runs before new planning and can restore or clean the destination even when that planning fails.
+Before publishing, every render checks existing Chronicle event type ids in the destination, at application scope and selected scopes. Publication proceeds only when Stage confirms compatibility. An id change or an existing id that cannot be read safely produces `CLI-RENDER-016` with Stage's `STAGE-EVENT-002` diagnostic and exits with 5. The message lists each path, type, and old and planned id. Pin the event's `id` to keep its stored event type name when renaming it. The refusal writes no new artifacts; `--force` does not bypass it. `--check` performs the same check without writing.
+
```bash
cratis render ./plays \
--target cratis \
@@ -122,7 +126,7 @@ Scoped publication preserves other scopes' files as managed. It deletes an absen
Historical owners let a feature or module render remove files of a deleted slice. Artifacts with unresolved source ids and no matching owner are retained and listed as `orphaned`; a later full render removes them. This is semantic ownership, not an inference from generated file paths.
-Before replacing shared policies, both render and `--check` verify their destination layout. An aggregate or unrecognized layout produces `CLI-RENDER-013`, names the affected paths, and asks you to render the whole application once. Migrate with a full render before using scopes; the CLI refuses rather than silently rendering more than you requested.
+Before replacing shared policies in a scoped render, both render and `--check` verify their destination layout. An aggregate or unrecognized layout produces `CLI-RENDER-013`, names the affected paths, and asks you to render the whole application once. Migrate with a full render before using scopes; the CLI refuses rather than silently rendering more than you requested.
For incremental adoption, render an admitted feature into a fresh destination, render its admitted dependencies or additional disjoint scopes as they become available, and use `--check --scope` to detect drift without writing. Once the whole model is admitted, a full render accepts all scoped artifacts as managed. A successful scoped publication still requires the same generated-application build and test verification as a full publication.
@@ -681,6 +685,39 @@ Expected address-mode limits such as `AddressKeysOnly`, `IdentitiesNotCompared`
Matching is by exact kind and address, not rename inference. A renamed or moved declaration appears as removal plus addition. Without executable binding on both sides, Screenplay falls back to declaration-level comparison and reports that limit. It does not analyze opaque code bodies, open external implementation files, execute specifications or prove behavioral equivalence. Review extraction diagnostics and provenance as well as the verdict; a clean check cannot prove semantics the source provider did not recover.
+## `cratis screenplay baseline`
+
+Protect released event contracts from accidental edits outside MCP. `check` compares the current model with the admitted release baseline without reconciling or writing identities. `admit` monotonically records released event revisions and atomically writes the baseline using Screenplay's recovery journal.
+
+Both commands require persisted `.screenplay/identities.json`, created by an applied MCP proposal. `ROOT` is the model directory and defaults to the current directory, honoring `mcpServers.screenplay.root` in `.cratis/ai.json` through the existing MCP root resolver. Pass an explicit root to select a different model.
+
+Run the read-only gate in CI:
+
+```bash
+cratis screenplay baseline check ./Model
+```
+
+In your release job, after choosing the release tag, admit the shipped revisions and retain the updated `.screenplay/release-baseline.json` alongside the model and its identities:
+
+```bash
+cratis screenplay baseline admit ./Model --release "$TAG"
+```
+
+`--release ` is required. Add `--from-esm ` to admit a canonical shipped ESM instead of the current model. Admission does not authorize changing an already admitted revision. An absent baseline can be checked, but absent identities, unreadable metadata and pending recovery refuse the operation.
+
+Plain text is the default. Violations print as `Kind: Event revision N`, followed by `at Path` when available. Failures go to standard error as `FailureKind: Reason`. Successful admission prints `Admitted N event revision(s) in release baseline REVISION.`
+
+Use `-o json` or `-o json-compact` for a standard-output object containing `violations`, `failureKind`, `reason`, `admittedEntryCount`, `baselineRevision` and `exitCode`; null fields are omitted.
+
+These commands use **check-specific exit codes**, not the general CLI codes:
+
+| Exit code | Meaning |
+|---|---|
+| `0` | Clean check or successful admission. |
+| `1` | Released event revision violations. |
+| `2` | Unreadable, refused, or could not run. |
+| `-1` (`255` on POSIX) | CLI usage error, such as unknown options or a missing `--release`; unexpected failures also use this code. |
+
## `cratis screenplay validate [PATH]`
Compiles Screenplay documents and reports everything the compiler found. It does not care what wrote them — `screenplay generate`, [`cratis prologue`](prologue.md), or a person designing a system before any code exists.
@@ -701,14 +738,16 @@ cratis screenplay validate ./plays --check navigation --check field-origins --ex
### Model questions
-`cratis screenplay validate` checks source and optional executable binding; it does not gate model notes. Use the check command in Screenplay's standalone CLI to report or fail on unresolved questions after [installing the Screenplay tool](/screenplay/tool/#install-the-cli):
+Add `--open-questions report|fail` to read `.screenplay/notes.json` after compiler diagnostics. The option is optional and cannot be repeated; without it, notes are ignored.
```bash
-screenplay ./plays --open-questions report
-screenplay ./plays --scope Billing.Invoices.SendInvoice --open-questions fail
+cratis screenplay validate ./plays --open-questions report
+cratis screenplay validate ./plays --scope Billing.Invoices.SendInvoice --open-questions fail
```
-`report` lists open questions without failing because they remain. `fail` exits `1` when an open question remains in the selected set; unreadable notes exit `2`. Question scope includes descendants and ancestor notes. Without persisted identity state, Screenplay reports questions unscoped as orphaned rather than hiding them. Tasks and ordinary notes do not fail this gate.
+`report` lists open questions without failing because they remain. `fail` exits `5` when an open question remains in the selected set; unreadable notes or identity state exit `1`. Compiler errors still exit `5`. Question scope includes descendants and ancestor notes. Without persisted identity state, Screenplay reports all questions unscoped as orphaned rather than hiding them. Tasks and ordinary notes do not fail this gate, and questions do not participate in `--warnings-as-errors`.
+
+For a file target, notes are read from its parent directory; for a folder, from that folder. JSON summaries include `openQuestions` with `findings`, identity/notes statuses, effective scope and any failure details.
### Scoped validation
@@ -790,6 +829,9 @@ These are three separate verdicts, and each command checks only its own:
| Compilation or completeness checks report warnings and `--warnings-as-errors` or `--warnaserror` is set | `5` | Validation error. |
| A `--check` selection is unrecognized | `5` | Validation error naming the selection and listing accepted names and codes. |
| An option is unknown | `5` | Validation error naming the option and pointing to help. |
+| `--open-questions fail` reports open questions | `5` | Validation failed; findings remain visible. |
+| `--open-questions` is invalid or repeated | `5` | Validation error requiring one `report` or `fail` value. |
+| Requested model notes or identity state cannot be read | `1` | Invalid input; compiler errors still take precedence with exit `5`. |
| `--executable` is set and the model does not bind | `5` | Validation error. |
| CLI usage is invalid, such as a missing option value or combining `--scope` with `--executable` | `-1` (`255` on POSIX) | CLI usage error. Unexpected failures also use this code. |
diff --git a/README.md b/README.md
index 6b03f196..cd7559c8 100644
--- a/README.md
+++ b/README.md
@@ -459,7 +459,7 @@ The CLI repository carries additional command groups whose exact behavior and st
- `cratis arc` inspects registered commands and queries in a running [Arc](https://github.com/Cratis/Arc) application.
- `cratis view` opens a .NET application as an event model in the browser, from the Screenplay documents it embeds or ones generated from its source. See the [View command reference](https://github.com/Cratis/cli/blob/main/Documentation/reference/view.md).
-- `cratis screenplay` and `cratis render` work with Cratis Screenplay (`.play`) documents — generation, validation, and rendering from files, with nothing running. The CLI bundles Screenplay 4.147.1 and Stage 4.92.0; rendering admits ESM v1 through v15, including event sources, streams, routed commands and policy negation, and carries authored `description` and event `documentation` into XML doc comments. Server-generated values render with v15 concept generation strategies and freshness scopes, snapshot collection queries render as one-time lists, observable collection queries return Chronicle's live stream through Arc, equality filters render on both, and scalar or named command responses render. Projection `all` blocks with event-source identity mappings render as Chronicle `FromAll`; literal-mapping or mapping-less blocks still report `STAGE-ESM-017`. PascalCase event-source names render as `{Name}EventSource` with public stream-name constants; colliding constants report `STAGE-ESM-012`; unsupported generated types report `STAGE-ESM-028` and unsupported generation/response combinations `STAGE-ESM-038`; specifications whose unauthenticated callers carry roles or claims are refused with `STAGE-ESM-011`. See the [Screenplay command reference](https://github.com/Cratis/cli/blob/main/Documentation/reference/screenplay.md) for the supported constructs and other renderer limits.
+- `cratis screenplay` and `cratis render` work with Cratis Screenplay (`.play`) documents — generation, validation, and rendering from files, with nothing running. The CLI bundles Screenplay 4.154.0 and Stage 4.96.0; rendering admits ESM v1 through v16, including event sources, streams, routed commands and policy negation, and carries authored `description` and event `documentation` into XML doc comments. ESM v16 keeps an event's stored type name through its `id` pin when you rename it. Every render checks existing event type ids before publication and refuses changes or unreadable ids with `CLI-RENDER-016` and `STAGE-EVENT-002`. Server-generated values render with v15 concept generation strategies and freshness scopes, snapshot collection queries render as one-time lists, observable collection queries return Chronicle's live stream through Arc, equality filters render on both, and scalar or named command responses render. Projection `all` blocks with event-source identity mappings render as Chronicle `FromAll`; literal-mapping or mapping-less blocks still report `STAGE-ESM-017`. PascalCase event-source names render as `{Name}EventSource` with public stream-name constants; colliding constants report `STAGE-ESM-012`; unsupported generated types report `STAGE-ESM-028` and unsupported generation/response combinations `STAGE-ESM-038`; specifications whose unauthenticated callers carry roles or claims are refused with `STAGE-ESM-011`. See the [Screenplay command reference](https://github.com/Cratis/cli/blob/main/Documentation/reference/screenplay.md) for the supported constructs and other renderer limits.
- `cratis screenplay desktop install --clients claude,chatgpt` acquires verified, self-contained local Screenplay desktop packages. Complete installation/enabling in each host's UI; use `status`, `update`, and `uninstall` to manage Cratis-owned sources safely. See [desktop MCP installation](https://github.com/Cratis/cli/blob/main/Documentation/reference/screenplay-desktop-mcp.md).
- The [canonical CLI page](https://cratis.io/cli/) carries the currently admitted command-group documentation.
diff --git a/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/given/an_existing_event_publication.cs b/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/given/an_existing_event_publication.cs
new file mode 100644
index 00000000..b2823cc9
--- /dev/null
+++ b/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/given/an_existing_event_publication.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Text;
+using Cratis.Cli.Commands.Render.Publication;
+
+namespace Cratis.Cli.for_ArtifactPublisher.when_checking_event_identities.given;
+
+public class an_existing_event_publication : for_ScreenplayPlanning.when_planning_scopes.given.a_scoped_model
+{
+ protected string _eventPath = null!;
+ protected Dictionary _before = null!;
+ private protected ArtifactPublicationCheckResult _check = null!;
+ protected Exception? _error;
+
+ async Task Establish()
+ {
+ var plan = await Plan();
+ await Publish(plan);
+ _eventPath = plan.Artifacts!.Artifacts.Single(artifact => Encoding.UTF8.GetString(artifact.Bytes.AsSpan())
+ .Contains("public record ProjectRegistered(", StringComparison.Ordinal)).RelativePath;
+ _before = Directory.GetFiles(_destination, "*", SearchOption.AllDirectories)
+ .ToDictionary(path => path, File.ReadAllBytes, StringComparer.Ordinal);
+ }
+
+ protected void PinEvent(string storedName) => File.WriteAllText(_projectFile,
+ ProjectSource.Replace("event ProjectRegistered", $"event ProjectRegistered\n id \"{storedName}\"", StringComparison.Ordinal));
+
+ protected bool DestinationIsUnchanged() =>
+ Directory.GetFiles(_destination, "*", SearchOption.AllDirectories).Order(StringComparer.Ordinal)
+ .SequenceEqual(_before.Keys.Order(StringComparer.Ordinal)) &&
+ _before.All(pair => File.ReadAllBytes(pair.Key).SequenceEqual(pair.Value));
+}
diff --git a/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/with_a_directory_at_an_event_path.cs b/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/with_a_directory_at_an_event_path.cs
new file mode 100644
index 00000000..08f034a3
--- /dev/null
+++ b/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/with_a_directory_at_an_event_path.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Cli.Commands.Render.Publication;
+
+namespace Cratis.Cli.for_ArtifactPublisher.when_checking_event_identities;
+
+public class with_a_directory_at_an_event_path : given.an_existing_event_publication
+{
+ string _eventFile = null!;
+
+ void Establish()
+ {
+ _eventFile = Path.Combine(_destination, _eventPath);
+ File.Delete(_eventFile);
+ Directory.CreateDirectory(_eventFile);
+ }
+
+ async Task Because()
+ {
+ var plan = await Plan();
+ _error = await Catch.Exception(() => _publisher.Publish(Request(plan) with { Force = true }, CancellationToken.None));
+ }
+
+ [Fact] void should_refuse_publication() => _error.ShouldBeOfExactType();
+ [Fact] void should_explain_that_the_existing_file_could_not_be_read() => _error!.Message.ShouldContain("could not be read for the publication check");
+ [Fact] void should_leave_the_directory_in_place() => Directory.Exists(_eventFile).ShouldBeTrue();
+}
diff --git a/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/with_a_pinned_rename.cs b/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/with_a_pinned_rename.cs
new file mode 100644
index 00000000..be0ed570
--- /dev/null
+++ b/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/with_a_pinned_rename.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Cli.Commands.Render.Publication;
+
+namespace Cratis.Cli.for_ArtifactPublisher.when_checking_event_identities;
+
+public class with_a_pinned_rename : given.an_existing_event_publication
+{
+ ArtifactPublicationResult _result = null!;
+
+ void Establish()
+ {
+ PinEvent("ProjectRegistered");
+ File.WriteAllText(_projectFile, File.ReadAllText(_projectFile)
+ .Replace("ProjectRegistered", "ProjectRegistrationRecorded", StringComparison.Ordinal)
+ .Replace("id \"ProjectRegistrationRecorded\"", "id \"ProjectRegistered\"", StringComparison.Ordinal));
+ File.WriteAllText(_lookupFile, LookupSource.Replace("ProjectRegistered", "ProjectRegistrationRecorded", StringComparison.Ordinal));
+ }
+
+ async Task Because()
+ {
+ var plan = await Plan();
+ _check = await _publisher.Check(Request(plan), CancellationToken.None);
+ _result = await Publish(plan);
+ }
+
+ [Fact] void should_admit_the_read_only_check() => _check.Refused.ShouldEqual(0);
+ [Fact] void should_publish_the_renamed_event() => _result.Written.ShouldBeGreaterThan(0);
+ [Fact] void should_keep_the_stored_id() => File.ReadAllText(Path.Combine(_destination, _eventPath)).ShouldContain("[global::Cratis.Chronicle.Events.EventTypeAttribute(\"ProjectRegistered\")]\npublic record ProjectRegistrationRecorded(");
+}
diff --git a/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/with_a_scoped_id_change.cs b/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/with_a_scoped_id_change.cs
new file mode 100644
index 00000000..378e84fb
--- /dev/null
+++ b/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/with_a_scoped_id_change.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Cli.Commands.Render.Publication;
+
+namespace Cratis.Cli.for_ArtifactPublisher.when_checking_event_identities;
+
+public class with_a_scoped_id_change : given.an_existing_event_publication
+{
+ void Establish() => PinEvent("OriginalProjectRegistered");
+
+ async Task Because()
+ {
+ var plan = await Plan("Projects.Registration.RegisterProject");
+ _check = await _publisher.Check(Request(plan), CancellationToken.None);
+ _error = await Catch.Exception(() => Publish(plan));
+ }
+
+ [Fact] void should_refuse_publication() => _error.ShouldBeOfExactType();
+ [Fact] void should_report_the_event_identity_refusal() => ((UnsafeArtifactPublication)_error!).DiagnosticCode.ShouldEqual("CLI-RENDER-016");
+ [Fact] void should_refuse_the_read_only_check() => _check.Refused.ShouldEqual(1);
+ [Fact] void should_report_the_event_identity_code_in_the_receipt() => _check.Receipt.Changes.Single().Code.ShouldEqual("CLI-RENDER-016");
+ [Fact] void should_surface_the_stage_diagnostic() => _check.Receipt.Changes.Single().Reason!.ShouldContain("STAGE-EVENT-002: Event type");
+ [Fact] void should_show_the_old_and_new_id() => _check.Receipt.Changes.Single().Reason!.ShouldContain("ProjectRegistered -> OriginalProjectRegistered");
+ [Fact] void should_leave_every_destination_byte_unchanged() => DestinationIsUnchanged().ShouldBeTrue();
+ [Fact] void should_create_no_control_state() => Directory.Exists(ArtifactPublicationStorage.ControlPath(_destination)).ShouldBeFalse();
+}
diff --git a/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/with_an_application_id_change.cs b/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/with_an_application_id_change.cs
new file mode 100644
index 00000000..a7c7cd97
--- /dev/null
+++ b/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/with_an_application_id_change.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Cli.Commands.Render.Publication;
+
+namespace Cratis.Cli.for_ArtifactPublisher.when_checking_event_identities;
+
+public class with_an_application_id_change : given.an_existing_event_publication
+{
+ void Establish() => PinEvent("OriginalProjectRegistered");
+
+ async Task Because()
+ {
+ var plan = await Plan();
+ _check = await _publisher.Check(Request(plan), CancellationToken.None);
+ _error = await Catch.Exception(() => Publish(plan));
+ }
+
+ [Fact] void should_refuse_publication() => _error.ShouldBeOfExactType();
+ [Fact] void should_report_the_event_identity_refusal() => ((UnsafeArtifactPublication)_error!).DiagnosticCode.ShouldEqual("CLI-RENDER-016");
+ [Fact] void should_refuse_the_read_only_check() => _check.Refused.ShouldEqual(1);
+ [Fact] void should_report_the_event_identity_code_in_the_receipt() => _check.Receipt.Changes.Single().Code.ShouldEqual("CLI-RENDER-016");
+ [Fact] void should_name_the_changed_path() => _check.Receipt.Changes.Single().Reason!.ShouldContain(_eventPath);
+ [Fact] void should_name_the_type() => _check.Receipt.Changes.Single().Reason!.ShouldContain("ProjectRegistered:");
+ [Fact] void should_show_the_old_and_new_id() => _check.Receipt.Changes.Single().Reason!.ShouldContain("ProjectRegistered -> OriginalProjectRegistered");
+ [Fact] void should_surface_the_stage_diagnostic() => _check.Receipt.Changes.Single().Reason!.ShouldContain("STAGE-EVENT-002: Event type");
+ [Fact] void should_explain_the_pin_remedy() => _error!.Message.ShouldContain("Pin the event's 'id'");
+ [Fact] void should_leave_every_destination_byte_unchanged() => DestinationIsUnchanged().ShouldBeTrue();
+ [Fact] void should_create_no_control_state() => Directory.Exists(ArtifactPublicationStorage.ControlPath(_destination)).ShouldBeFalse();
+}
diff --git a/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/with_an_unchanged_application_id.cs b/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/with_an_unchanged_application_id.cs
new file mode 100644
index 00000000..1498b4e0
--- /dev/null
+++ b/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/with_an_unchanged_application_id.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Cli.Commands.Render.Publication;
+
+namespace Cratis.Cli.for_ArtifactPublisher.when_checking_event_identities;
+
+public class with_an_unchanged_application_id : given.an_existing_event_publication
+{
+ ArtifactPublicationResult _result = null!;
+
+ void Establish() => File.WriteAllText(_projectFile,
+ ProjectSource.Replace("A project was registered", "The project registration was recorded", StringComparison.Ordinal));
+
+ async Task Because()
+ {
+ var plan = await Plan();
+ _check = await _publisher.Check(Request(plan), CancellationToken.None);
+ _result = await Publish(plan);
+ }
+
+ [Fact] void should_admit_the_read_only_check() => _check.Refused.ShouldEqual(0);
+ [Fact] void should_publish_the_changed_artifacts() => _result.Written.ShouldBeGreaterThan(0);
+ [Fact] void should_preserve_the_argument_free_event_id() => File.ReadAllText(Path.Combine(_destination, _eventPath)).ShouldContain("[global::Cratis.Chronicle.Events.EventTypeAttribute]\npublic record ProjectRegistered(");
+ [Fact] void should_publish_the_new_documentation() => File.ReadAllText(Path.Combine(_destination, _eventPath)).ShouldContain("The project registration was recorded");
+}
diff --git a/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/with_an_unreadable_application_id.cs b/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/with_an_unreadable_application_id.cs
new file mode 100644
index 00000000..be2c9298
--- /dev/null
+++ b/Source/Cli.Specs/for_ArtifactPublisher/when_checking_event_identities/with_an_unreadable_application_id.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Cli.Commands.Render.Publication;
+
+namespace Cratis.Cli.for_ArtifactPublisher.when_checking_event_identities;
+
+public class with_an_unreadable_application_id : given.an_existing_event_publication
+{
+ void Establish() => File.WriteAllText(Path.Combine(_destination, _eventPath),
+ "namespace Shop.Projects.Registration.RegisterProject; [EventType(StoredId)] public record ProjectRegistered;");
+
+ async Task Because()
+ {
+ var plan = await Plan();
+ var request = Request(plan) with { Force = true };
+ _check = await _publisher.Check(request, CancellationToken.None);
+ _error = await Catch.Exception(() => _publisher.Publish(request, CancellationToken.None));
+ }
+
+ [Fact] void should_refuse_publication_even_with_force() => _error.ShouldBeOfExactType();
+ [Fact] void should_report_the_event_identity_refusal() => ((UnsafeArtifactPublication)_error!).DiagnosticCode.ShouldEqual("CLI-RENDER-016");
+ [Fact] void should_explain_the_unreadable_id() => _check.Receipt.Changes.Single().Reason!.ShouldContain("STAGE-EVENT-002: Event type");
+ [Fact] void should_name_the_unreadable_expression() => _error!.Message.ShouldContain("StoredId -> ProjectRegistered");
+ [Fact] void should_leave_the_unreadable_file_unchanged() => File.ReadAllText(Path.Combine(_destination, _eventPath)).ShouldContain("[EventType(StoredId)]");
+ [Fact] void should_create_no_control_state() => Directory.Exists(ArtifactPublicationStorage.ControlPath(_destination)).ShouldBeFalse();
+}
diff --git a/Source/Cli.Specs/for_CliApp/when_requesting_screenplay_help.cs b/Source/Cli.Specs/for_CliApp/when_requesting_screenplay_help.cs
index 50b6d2c8..6b23d268 100644
--- a/Source/Cli.Specs/for_CliApp/when_requesting_screenplay_help.cs
+++ b/Source/Cli.Specs/for_CliApp/when_requesting_screenplay_help.cs
@@ -19,7 +19,7 @@ async Task Because()
{
string[] arguments = command.Branch == typeof(ScreenplayBranch)
? ["screenplay", command.Name, "--help"]
- : ["screenplay", "desktop", command.Name, "--help"];
+ : ["screenplay", command.Branch!.GetCustomAttribute()!.Name, command.Name, "--help"];
await Run(arguments);
_results.Add(string.Join(' ', arguments), (_exitCode, _output, _error));
}
diff --git a/Source/Cli.Specs/for_CratisRenderTarget/when_planning_a_supported_application.cs b/Source/Cli.Specs/for_CratisRenderTarget/when_planning_a_supported_application.cs
index b9ab9010..8c06390b 100644
--- a/Source/Cli.Specs/for_CratisRenderTarget/when_planning_a_supported_application.cs
+++ b/Source/Cli.Specs/for_CratisRenderTarget/when_planning_a_supported_application.cs
@@ -31,8 +31,8 @@ Task Because()
[Fact] void should_plan_the_exact_facade_target_version() => _result.TargetVersion.ShouldEqual(_expected.TargetVersion);
[Fact] void should_plan_the_exact_facade_renderer() => _result.Renderer.ShouldEqual(_expected.Renderer);
[Fact] void should_plan_the_exact_facade_renderer_version() => _result.RendererVersion.ShouldEqual(_expected.RendererVersion);
- [Fact] void should_load_the_released_stage_4920_renderer_package() =>
- typeof(CratisRendering).Assembly.GetCustomAttribute()!.InformationalVersion.Split('+')[0].ShouldEqual("4.92.0");
+ [Fact] void should_load_the_released_stage_4960_renderer_package() =>
+ typeof(CratisRendering).Assembly.GetCustomAttribute()!.InformationalVersion.Split('+')[0].ShouldEqual("4.96.0");
[Fact] void should_plan_the_same_application_name() => _result.ApplicationName.ShouldEqual(_expected.ApplicationName);
[Fact] void should_plan_the_same_semantic_revision() => _result.SemanticRevision.ShouldEqual(_expected.SemanticRevision);
[Fact] void should_plan_the_exact_facade_artifacts() =>
diff --git a/Source/Cli.Specs/for_CratisRenderTarget/when_planning_stored_event_names.cs b/Source/Cli.Specs/for_CratisRenderTarget/when_planning_stored_event_names.cs
new file mode 100644
index 00000000..f47d29fe
--- /dev/null
+++ b/Source/Cli.Specs/for_CratisRenderTarget/when_planning_stored_event_names.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Text;
+using Cratis.Screenplay.CanonicalCorpus;
+using Cratis.Screenplay.Semantics;
+using Cratis.Stage.Contracts.Rendering;
+
+namespace Cratis.Cli.for_CratisRenderTarget;
+
+public class when_planning_stored_event_names : given.a_cratis_render_target
+{
+ SemanticCompilation _compilation = null!;
+ ArtifactRenderPlan _plan = null!;
+ string _code = null!;
+
+ void Because()
+ {
+ var source = StoredEventNamesCorpus.V16.SourceForms.Single(_ => _.Name == "single").Documents.Single().Text;
+ var (compilation, executionPlan) = Compile(source, "StoredEventNames");
+ _compilation = compilation;
+ _plan = _target.Plan(compilation, executionPlan, null, null, [], [], [], [], []);
+ _code = string.Join('\n', _plan.Artifacts.Where(_ => _.RelativePath.EndsWith(".cs", StringComparison.Ordinal))
+ .Select(_ => Encoding.UTF8.GetString(_.Bytes.AsSpan())));
+ }
+
+ [Fact] void should_compile_to_v16() => _compilation.Model.SemanticVersion.ShouldEqual(SemanticVersion.V16);
+ [Fact] void should_admit_v16() => RenderedSemanticVersions.Check(_compilation.Model.SemanticVersion).ShouldBeNull();
+ [Fact] void should_plan_successfully() => _plan.Success.ShouldBeTrue();
+ [Fact] void should_store_the_renamed_event_under_its_pinned_name() => _code.ShouldContain("[global::Cratis.Chronicle.Events.EventTypeAttribute(\"Original\")]\npublic record Renamed(");
+ [Fact] void should_keep_unpinned_events_argument_free() => _code.ShouldContain("[global::Cratis.Chronicle.Events.EventTypeAttribute]\npublic record Recorded(");
+}
diff --git a/Source/Cli.Specs/for_RenderCommand/given/an_event_id_refusal.cs b/Source/Cli.Specs/for_RenderCommand/given/an_event_id_refusal.cs
new file mode 100644
index 00000000..e5348305
--- /dev/null
+++ b/Source/Cli.Specs/for_RenderCommand/given/an_event_id_refusal.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Cli.Commands.Render.Publication;
+using NSubstitute.ExceptionExtensions;
+
+namespace Cratis.Cli.for_RenderCommand.given;
+
+public class an_event_id_refusal : a_check_command
+{
+ void Establish()
+ {
+ _settings.Check = false;
+ _command = new RenderCommand(_planning, _publication);
+ _publication.Publish(Arg.Any(), Arg.Any())
+ .Throws(new UnsafeArtifactPublication("STAGE-EVENT-002: Event type 'Records.Renamed' changes from Original to Renamed. Pin the event's 'id'.", "CLI-RENDER-016"));
+ }
+}
diff --git a/Source/Cli.Specs/for_RenderCommand/when_rendering/and_event_id_publication_is_refused_in_json.cs b/Source/Cli.Specs/for_RenderCommand/when_rendering/and_event_id_publication_is_refused_in_json.cs
new file mode 100644
index 00000000..6cfc65f8
--- /dev/null
+++ b/Source/Cli.Specs/for_RenderCommand/when_rendering/and_event_id_publication_is_refused_in_json.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Text.Json;
+
+namespace Cratis.Cli.for_RenderCommand.when_rendering;
+
+[Collection(CliSpecsCollection.Name)]
+public class and_event_id_publication_is_refused_in_json : given.an_event_id_refusal
+{
+ (int ExitCode, string Stdout, string Stderr) _output;
+
+ string _message = null!;
+
+ async Task Because()
+ {
+ _output = await Capture();
+ using var diagnostic = JsonDocument.Parse(_output.Stderr.Split('\n')[0]);
+ _message = diagnostic.RootElement.GetProperty("diagnostics")[0].GetProperty("message").GetString()!;
+ }
+
+ [Fact] void should_return_validation_error() => _output.ExitCode.ShouldEqual(ExitCodes.ValidationError);
+ [Fact] void should_surface_the_cli_code() => _output.Stderr.ShouldContain("CLI-RENDER-016");
+ [Fact] void should_surface_the_stage_diagnostic() => _output.Stderr.ShouldContain("STAGE-EVENT-002: Event type");
+ [Fact] void should_report_the_pin_remedy() => _message.ShouldContain("Pin the event's 'id'");
+ [Fact] void should_report_no_success_on_stdout() => _output.Stdout.ShouldBeEmpty();
+}
diff --git a/Source/Cli.Specs/for_RenderCommand/when_rendering/and_event_id_publication_is_refused_in_text.cs b/Source/Cli.Specs/for_RenderCommand/when_rendering/and_event_id_publication_is_refused_in_text.cs
new file mode 100644
index 00000000..1028bcc0
--- /dev/null
+++ b/Source/Cli.Specs/for_RenderCommand/when_rendering/and_event_id_publication_is_refused_in_text.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Cratis.Cli.for_RenderCommand.when_rendering;
+
+[Collection(CliSpecsCollection.Name)]
+public class and_event_id_publication_is_refused_in_text : given.an_event_id_refusal
+{
+ (int ExitCode, string Stdout, string Stderr) _output;
+
+ void Establish() => _settings.Output = OutputFormats.Table;
+ async Task Because() => _output = await Capture();
+
+ [Fact] void should_return_validation_error() => _output.ExitCode.ShouldEqual(ExitCodes.ValidationError);
+ [Fact] void should_surface_the_cli_code() => _output.Stderr.ShouldContain("CLI-RENDER-016");
+ [Fact] void should_surface_the_stage_diagnostic() => _output.Stderr.ShouldContain("STAGE-EVENT-002: Event type");
+ [Fact] void should_report_the_pin_remedy() => _output.Stderr.ShouldContain("Pin the event's 'id'");
+}
diff --git a/Source/Cli.Specs/for_RenderedSemanticVersions/when_checking_admitted_versions.cs b/Source/Cli.Specs/for_RenderedSemanticVersions/when_checking_admitted_versions.cs
index dcd362cf..0bf5dbf5 100644
--- a/Source/Cli.Specs/for_RenderedSemanticVersions/when_checking_admitted_versions.cs
+++ b/Source/Cli.Specs/for_RenderedSemanticVersions/when_checking_admitted_versions.cs
@@ -14,16 +14,18 @@ public class when_checking_admitted_versions : Specification
ScreenplayDiagnostic? _v13;
ScreenplayDiagnostic? _v14;
ScreenplayDiagnostic? _v15;
+ ScreenplayDiagnostic? _v16;
void Because()
{
- _newer = RenderedSemanticVersions.Check(new SemanticVersion(16, 0));
+ _newer = RenderedSemanticVersions.Check(new SemanticVersion(17, 0));
_v10 = RenderedSemanticVersions.Check(SemanticVersion.V10);
_v11 = RenderedSemanticVersions.Check(SemanticVersion.V11);
_v12 = RenderedSemanticVersions.Check(SemanticVersion.V12);
_v13 = RenderedSemanticVersions.Check(SemanticVersion.V13);
_v14 = RenderedSemanticVersions.Check(SemanticVersion.V14);
_v15 = RenderedSemanticVersions.Check(SemanticVersion.V15);
+ _v16 = RenderedSemanticVersions.Check(SemanticVersion.V16);
}
[Fact] void should_keep_v10_admitted() => _v10.ShouldBeNull();
@@ -32,8 +34,9 @@ void Because()
[Fact] void should_admit_v13_concept_compliance() => _v13.ShouldBeNull();
[Fact] void should_admit_v14_query_filters() => _v14.ShouldBeNull();
[Fact] void should_admit_v15_concept_generation_strategies() => _v15.ShouldBeNull();
- [Fact] void should_refuse_versions_newer_than_v15() => _newer!.Code.ShouldEqual("CLI-RENDER-004");
- [Fact] void should_name_the_refused_version() => _newer!.Message.ShouldContain("ESM v16.0");
+ [Fact] void should_admit_v16_stored_event_names() => _v16.ShouldBeNull();
+ [Fact] void should_refuse_versions_newer_than_v16() => _newer!.Code.ShouldEqual("CLI-RENDER-004");
+ [Fact] void should_name_the_refused_version() => _newer!.Message.ShouldContain("ESM v17.0");
[Fact] void should_block_rendering_newer_versions() => _newer!.Severity.ShouldEqual(ScreenplayDiagnosticSeverity.Error);
[Fact] void should_describe_the_explicit_version_gate() => _newer!.Message.ShouldContain("Newer model versions require explicit renderer admission");
}
diff --git a/Source/Cli.Specs/for_RunCommand/when_running/with_a_folder_and_the_default_stage_tag.cs b/Source/Cli.Specs/for_RunCommand/when_running/with_a_folder_and_the_default_stage_tag.cs
index 7b69b0c5..48195bf9 100644
--- a/Source/Cli.Specs/for_RunCommand/when_running/with_a_folder_and_the_default_stage_tag.cs
+++ b/Source/Cli.Specs/for_RunCommand/when_running/with_a_folder_and_the_default_stage_tag.cs
@@ -15,8 +15,8 @@ public class with_a_folder_and_the_default_stage_tag : given.a_run_command
[Unix.Fact] void should_start_the_released_stage_image_that_matches_the_renderer_packages() =>
DockerArguments.ShouldContain($"cratis/stage:{StageContainer.DefaultTag}");
- [Unix.Fact] void should_run_stage_4920_by_default() =>
- DockerArguments.ShouldContain("cratis/stage:4.92.0");
+ [Unix.Fact] void should_run_stage_4960_by_default() =>
+ DockerArguments.ShouldContain("cratis/stage:4.96.0");
[Unix.Fact] void should_mount_only_the_selected_folder_read_only() =>
DockerArguments.ShouldContain($"{_folder}:/eventmodel:ro");
diff --git a/Source/Cli.Specs/for_RunCommand/when_running/with_a_play_file_and_the_default_stage_tag.cs b/Source/Cli.Specs/for_RunCommand/when_running/with_a_play_file_and_the_default_stage_tag.cs
index 081828f9..ba55394e 100644
--- a/Source/Cli.Specs/for_RunCommand/when_running/with_a_play_file_and_the_default_stage_tag.cs
+++ b/Source/Cli.Specs/for_RunCommand/when_running/with_a_play_file_and_the_default_stage_tag.cs
@@ -20,8 +20,8 @@ void Establish()
[Unix.Fact] void should_start_the_released_stage_image_that_matches_the_renderer_packages() =>
DockerArguments.ShouldContain($"cratis/stage:{StageContainer.DefaultTag}");
- [Unix.Fact] void should_run_stage_4920_by_default() =>
- DockerArguments.ShouldContain("cratis/stage:4.92.0");
+ [Unix.Fact] void should_run_stage_4960_by_default() =>
+ DockerArguments.ShouldContain("cratis/stage:4.96.0");
[Unix.Fact] void should_mount_only_the_selected_file_read_only() =>
DockerArguments.ShouldContain($"{_path}:/eventmodel/input.play:ro");
diff --git a/Source/Cli.Specs/for_ScreenplayReleaseBaseline/given/a_release_model.cs b/Source/Cli.Specs/for_ScreenplayReleaseBaseline/given/a_release_model.cs
new file mode 100644
index 00000000..cb63652f
--- /dev/null
+++ b/Source/Cli.Specs/for_ScreenplayReleaseBaseline/given/a_release_model.cs
@@ -0,0 +1,149 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using System.Text.Json;
+using Cratis.Screenplay.Mcp;
+
+namespace Cratis.Cli.for_ScreenplayReleaseBaseline.given;
+
+public class a_release_model : Specification
+{
+ protected const string Source = "module M\n feature F\n slice StateChange Record\n event Recorded\n value String\n";
+
+ ///
+ /// The variables that make the CLI detect an AI agent and switch output format; removed so local runs match CI.
+ ///
+ static readonly string[] AgentEnvironmentVariables =
+ [
+ AiAgentEnvironment.NonInteractiveEnvironmentVariable, "CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "CURSOR_TRACE_DIR",
+ "TERM_PROGRAM", "WINDSURF_SESSION_ID", "PI_CODING_AGENT", "PI_SESSION_ID"
+ ];
+
+ protected string _root;
+ protected string _document;
+ protected int _exitCode;
+ protected string _output;
+ protected string _error;
+
+ void Establish()
+ {
+ _root = Directory.CreateDirectory(Path.Combine(WithoutLinks(Path.GetTempPath()), "cratis-cli-baseline-specs", Guid.NewGuid().ToString())).FullName;
+ _document = Path.Combine(_root, "application.play");
+ File.WriteAllText(_document, Source);
+ PersistIdentities();
+ }
+
+ protected async Task Run(params string[] args)
+ {
+ var start = new ProcessStartInfo("dotnet") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, WorkingDirectory = _root };
+ start.Environment[UpdateChecker.DisableEnvVar] = "1";
+ start.Environment["NO_COLOR"] = "1";
+ foreach (var agentVariable in AgentEnvironmentVariables)
+ {
+ start.Environment.Remove(agentVariable);
+ }
+ start.ArgumentList.Add(typeof(ValidateScreenplayCommand).Assembly.Location);
+ foreach (var argument in args)
+ {
+ start.ArgumentList.Add(argument);
+ }
+ using var process = Process.Start(start);
+ using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(20));
+ var output = process.StandardOutput.ReadToEndAsync(deadline.Token);
+ var error = process.StandardError.ReadToEndAsync(deadline.Token);
+ await process.WaitForExitAsync(deadline.Token);
+ _exitCode = process.ExitCode;
+ _output = await output;
+ _error = await error;
+ }
+
+ void PersistIdentities()
+ {
+ using var output = new StringWriter();
+ JsonElement opened = default;
+ string? documentId = null;
+ string? proposalId = null;
+ using var input = new workflow_input([
+ () => "{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{},\"clientInfo\":{\"name\":\"spec\",\"version\":\"1\"}}}",
+ () => "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}",
+ () => Request("open-workspace", new { applicationName = "Model" }),
+ () =>
+ {
+ opened = Result(output);
+ return Request("read-workspace", new { expectedRevision = opened.GetProperty("revision").GetString(), view = "documents" });
+ },
+ () =>
+ {
+ documentId = Result(output).GetProperty("page").GetProperty("items")[0].GetProperty("documentId").GetString();
+ return Request("propose-source", new
+ {
+ expectedRevision = opened.GetProperty("revision").GetString(),
+ expectedCatalogRevision = opened.GetProperty("catalogRevision").GetString(),
+ formatting = "CanonicalizeTouchedDocuments",
+ documents = new[] { new { operation = "replace-document", documentId, source = Source } }
+ });
+ },
+ () =>
+ {
+ proposalId = Result(output).GetProperty("proposalId").GetString();
+ return Request("apply", new
+ {
+ expectedRevision = opened.GetProperty("revision").GetString(),
+ expectedCatalogRevision = opened.GetProperty("catalogRevision").GetString(),
+ proposalId
+ });
+ }
+ ]);
+ ScreenplayMcpServer.Run(_root, input, output);
+ Result(output).GetProperty("success").GetBoolean().ShouldBeTrue();
+ File.Exists(Path.Combine(_root, ".screenplay", "identities.json")).ShouldBeTrue();
+ }
+
+ static string Request(string name, object arguments) => JsonSerializer.Serialize(new { jsonrpc = "2.0", id = 1, method = "tools/call", @params = new { name, arguments } });
+
+ static JsonElement Result(StringWriter output)
+ {
+ using var response = JsonDocument.Parse(output.ToString().Split('\n', StringSplitOptions.RemoveEmptyEntries)[^1]);
+ response.RootElement.GetProperty("result").GetProperty("isError").GetBoolean().ShouldBeFalse();
+ return response.RootElement.GetProperty("result").GetProperty("structuredContent").Clone();
+ }
+
+ void Destroy() => Directory.Delete(_root, true);
+
+ ///
+ /// Resolves symbolic links in a path: the Screenplay workspace refuses roots below one, and macOS keeps the temp folder below /var.
+ ///
+ /// The path to resolve.
+ /// The path without symbolic links.
+ static string WithoutLinks(string path)
+ {
+ var directory = new DirectoryInfo(path);
+ if (directory.Parent is null)
+ {
+ return directory.FullName;
+ }
+
+ var resolved = Path.Combine(WithoutLinks(directory.Parent.FullName), directory.Name);
+ return new DirectoryInfo(resolved).ResolveLinkTarget(returnFinalTarget: true)?.FullName ?? resolved;
+ }
+
+ sealed class workflow_input(Func[] steps) : TextReader
+ {
+ int _index;
+ int _position;
+ string _line = string.Empty;
+
+ public override int Read()
+ {
+ if (_position == _line.Length)
+ {
+ if (_index == steps.Length) return -1;
+ _line = steps[_index++]() + "\n";
+ _position = 0;
+ }
+
+ return _line[_position++];
+ }
+ }
+}
diff --git a/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_admitting/with_a_release_label.cs b/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_admitting/with_a_release_label.cs
new file mode 100644
index 00000000..54a2c289
--- /dev/null
+++ b/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_admitting/with_a_release_label.cs
@@ -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.
+
+using Cratis.Screenplay.Workspaces;
+
+namespace Cratis.Cli.for_ScreenplayReleaseBaseline.when_admitting;
+
+public class with_a_release_label : given.a_release_model
+{
+ Task Because() => Run("screenplay", "baseline", "admit", _root, "--release", "v1");
+ [Fact] void should_admit() => _exitCode.ShouldEqual(0);
+ [Fact] void should_print_count_and_revision() => _output.ShouldContain($"Admitted 1 event revision(s) in release baseline {ReleaseBaselineStore.Revision(File.ReadAllBytes(Path.Combine(_root, ".screenplay", ReleaseBaselineStore.FileName)))}.");
+}
diff --git a/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_admitting/without_a_release_label.cs b/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_admitting/without_a_release_label.cs
new file mode 100644
index 00000000..27b13eef
--- /dev/null
+++ b/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_admitting/without_a_release_label.cs
@@ -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.
+
+using Cratis.Screenplay.Workspaces;
+
+namespace Cratis.Cli.for_ScreenplayReleaseBaseline.when_admitting;
+
+public class without_a_release_label : given.a_release_model
+{
+ Task Because() => Run("screenplay", "baseline", "admit", _root);
+ [Fact] void should_reject_usage() => _exitCode.ShouldEqual(255);
+ [Fact] void should_require_release() => _output.ShouldContain("--release is required");
+ [Fact] void should_not_write_baseline() => File.Exists(Path.Combine(_root, ".screenplay", ReleaseBaselineStore.FileName)).ShouldBeFalse();
+}
diff --git a/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/with_a_changed_revision.cs b/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/with_a_changed_revision.cs
new file mode 100644
index 00000000..ea460489
--- /dev/null
+++ b/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/with_a_changed_revision.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Screenplay.Mcp;
+
+namespace Cratis.Cli.for_ScreenplayReleaseBaseline.when_checking;
+
+public class with_a_changed_revision : given.a_release_model
+{
+ void Establish()
+ {
+ ReleaseBaselineCommands.Admit(_root, "v1").ExitCode.ShouldEqual(0);
+ File.WriteAllText(_document, Source.Replace("value String", "value Int", StringComparison.Ordinal));
+ }
+ Task Because() => Run("screenplay", "baseline", "check", _root);
+ [Fact] void should_report_violations() => _exitCode.ShouldEqual(1);
+ [Fact] void should_name_the_changed_event() => _output.ShouldContain("revision 1");
+}
diff --git a/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/with_a_malformed_ai_configuration.cs b/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/with_a_malformed_ai_configuration.cs
new file mode 100644
index 00000000..a3b91be3
--- /dev/null
+++ b/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/with_a_malformed_ai_configuration.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Cratis.Cli.for_ScreenplayReleaseBaseline.when_checking;
+
+public class with_a_malformed_ai_configuration : given.a_release_model
+{
+ void Establish()
+ {
+ Directory.CreateDirectory(Path.Combine(_root, ".cratis"));
+ File.WriteAllText(Path.Combine(_root, ".cratis", "ai.json"), "{ not json");
+ }
+
+ Task Because() => Run("screenplay", "baseline", "check");
+ [Fact] void should_report_that_it_could_not_run() => _exitCode.ShouldEqual(2);
+ [Fact] void should_not_crash() => _error.ShouldNotContain("Unhandled exception");
+}
diff --git a/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/with_an_unchanged_revision.cs b/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/with_an_unchanged_revision.cs
new file mode 100644
index 00000000..a7815a57
--- /dev/null
+++ b/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/with_an_unchanged_revision.cs
@@ -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.
+
+using Cratis.Screenplay.Mcp;
+
+namespace Cratis.Cli.for_ScreenplayReleaseBaseline.when_checking;
+
+public class with_an_unchanged_revision : given.a_release_model
+{
+ void Establish() => ReleaseBaselineCommands.Admit(_root, "v1").ExitCode.ShouldEqual(0);
+ Task Because() => Run("screenplay", "baseline", "check", _root);
+ [Fact] void should_be_clean() => _exitCode.ShouldEqual(0);
+ [Fact] void should_not_report_a_failure() => _error.ShouldBeEmpty();
+}
diff --git a/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/with_an_unknown_option.cs b/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/with_an_unknown_option.cs
new file mode 100644
index 00000000..c33cb320
--- /dev/null
+++ b/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/with_an_unknown_option.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Cratis.Cli.for_ScreenplayReleaseBaseline.when_checking;
+
+public class with_an_unknown_option : given.a_release_model
+{
+ Task Because() => Run("screenplay", "baseline", "check", _root, "--typo");
+ [Fact] void should_reject_usage() => _exitCode.ShouldEqual(255);
+ [Fact] void should_name_the_unknown_option() => _output.ShouldContain("Unknown option");
+}
diff --git a/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/with_json_output.cs b/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/with_json_output.cs
new file mode 100644
index 00000000..b114efef
--- /dev/null
+++ b/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/with_json_output.cs
@@ -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.
+
+using System.Text.Json;
+
+namespace Cratis.Cli.for_ScreenplayReleaseBaseline.when_checking;
+
+public class with_json_output : given.a_release_model
+{
+ Task Because() => Run("screenplay", "baseline", "check", _root, "-o", "json-compact");
+ [Fact] void should_be_clean() => _exitCode.ShouldEqual(0);
+ [Fact] void should_report_machine_readable_exit_code() => JsonDocument.Parse(_output).RootElement.GetProperty("exitCode").GetInt32().ShouldEqual(0);
+ [Fact] void should_report_no_violations() => JsonDocument.Parse(_output).RootElement.GetProperty("violations").GetArrayLength().ShouldEqual(0);
+}
diff --git a/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/without_persisted_identities.cs b/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/without_persisted_identities.cs
new file mode 100644
index 00000000..9bd262a9
--- /dev/null
+++ b/Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/without_persisted_identities.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Cratis.Cli.for_ScreenplayReleaseBaseline.when_checking;
+
+public class without_persisted_identities : given.a_release_model
+{
+ void Establish() => File.Delete(Path.Combine(_root, ".screenplay", "identities.json"));
+ Task Because() => Run("screenplay", "baseline", "check", _root);
+ [Fact] void should_refuse() => _exitCode.ShouldEqual(2);
+ [Fact] void should_explain_the_missing_state() => _error.ShouldContain("PersistedIdentitiesRequired:");
+}
diff --git a/Source/Cli.Specs/for_StageContainer/when_resolving_the_default_tag.cs b/Source/Cli.Specs/for_StageContainer/when_resolving_the_default_tag.cs
index f9f7fa70..8afb75bf 100644
--- a/Source/Cli.Specs/for_StageContainer/when_resolving_the_default_tag.cs
+++ b/Source/Cli.Specs/for_StageContainer/when_resolving_the_default_tag.cs
@@ -26,7 +26,7 @@ void Establish() => _renderer = typeof(ArtifactRenderPlan).Assembly
[Fact] void should_run_the_stage_the_renderer_came_from() => _tag.ShouldEqual(_renderer);
- [Fact] void should_run_the_released_stage_4920_image() => _tag.ShouldEqual("4.92.0");
+ [Fact] void should_run_the_released_stage_4960_image() => _tag.ShouldEqual("4.96.0");
[Fact] void should_not_run_a_moving_tag() => _tag.ShouldNotEqual(StageContainer.FallbackTag);
diff --git a/Source/Cli.Specs/for_ValidateScreenplayCommand/given/a_model_with_questions.cs b/Source/Cli.Specs/for_ValidateScreenplayCommand/given/a_model_with_questions.cs
new file mode 100644
index 00000000..0f774e4d
--- /dev/null
+++ b/Source/Cli.Specs/for_ValidateScreenplayCommand/given/a_model_with_questions.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Screenplay.Semantics;
+using Cratis.Screenplay.Workspaces;
+
+namespace Cratis.Cli.for_ValidateScreenplayCommand.given;
+
+public class a_model_with_questions : for_ScreenplayReleaseBaseline.given.a_release_model
+{
+ protected string _notesPath;
+
+ void Establish()
+ {
+ _notesPath = Path.Combine(_root, ".screenplay", ModelNotesStore.FileName);
+ var notes = new ModelNotesDocument(ApplicationIdentity.Create("Model").ToString(),
+ [
+ new ModelNote(ModelNotesStore.NewId(), "question", "Who may record?", new(Application: true), null, "team", DateTimeOffset.UtcNow, [], null)
+ ]);
+ File.WriteAllBytes(_notesPath, ModelNotesStore.Serialize(notes));
+ }
+}
diff --git a/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_an_invalid_mode.cs b/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_an_invalid_mode.cs
new file mode 100644
index 00000000..64ff7e73
--- /dev/null
+++ b/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_an_invalid_mode.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Cratis.Cli.for_ValidateScreenplayCommand.when_reporting_open_questions;
+
+public class with_an_invalid_mode : given.a_model_with_questions
+{
+ Task Because() => Run("screenplay", "validate", _root, "--open-questions", "invalid");
+ [Fact] void should_fail_validation() => _exitCode.ShouldEqual(5);
+ [Fact] void should_explain_the_valid_modes() => _output.ShouldContain("--open-questions requires report or fail");
+}
diff --git a/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_compiler_errors_and_unreadable_notes.cs b/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_compiler_errors_and_unreadable_notes.cs
new file mode 100644
index 00000000..85f9d171
--- /dev/null
+++ b/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_compiler_errors_and_unreadable_notes.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Cratis.Cli.for_ValidateScreenplayCommand.when_reporting_open_questions;
+
+public class with_compiler_errors_and_unreadable_notes : given.a_model_with_questions
+{
+ void Establish()
+ {
+ File.WriteAllText(_notesPath, "<<<< unreadable");
+ File.WriteAllText(_document, "module M\n feature F\n slice Record\n");
+ }
+ Task Because() => Run("screenplay", "validate", _root, "--open-questions", "fail");
+ [Fact] void should_preserve_compiler_error_exit_code() => _exitCode.ShouldEqual(5);
+}
diff --git a/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_fail_mode.cs b/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_fail_mode.cs
new file mode 100644
index 00000000..bb134b25
--- /dev/null
+++ b/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_fail_mode.cs
@@ -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.
+
+using System.Text.Json;
+
+namespace Cratis.Cli.for_ValidateScreenplayCommand.when_reporting_open_questions;
+
+public class with_fail_mode : given.a_model_with_questions
+{
+ Task Because() => Run("screenplay", "validate", _root, "--open-questions", "fail", "-o", "json-compact");
+ [Fact] void should_fail_validation() => _exitCode.ShouldEqual(5);
+ [Fact] void should_include_findings_in_json() => JsonDocument.Parse(_output).RootElement.GetProperty("openQuestions").GetProperty("findings")[0].GetProperty("message").GetString().ShouldEqual("Who may record?");
+ [Fact] void should_mark_the_result_invalid() => JsonDocument.Parse(_output).RootElement.GetProperty("valid").GetBoolean().ShouldBeFalse();
+}
diff --git a/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_repeated_modes.cs b/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_repeated_modes.cs
new file mode 100644
index 00000000..c6d0f72b
--- /dev/null
+++ b/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_repeated_modes.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Cratis.Cli.for_ValidateScreenplayCommand.when_reporting_open_questions;
+
+public class with_repeated_modes : given.a_model_with_questions
+{
+ Task Because() => Run("screenplay", "validate", _root, "--open-questions", "report", "--open-questions", "fail");
+ [Fact] void should_fail_validation() => _exitCode.ShouldEqual(5);
+ [Fact] void should_explain_the_repeat() => _output.ShouldContain("cannot be repeated");
+}
diff --git a/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_report_mode.cs b/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_report_mode.cs
new file mode 100644
index 00000000..203e45cd
--- /dev/null
+++ b/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_report_mode.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Cratis.Cli.for_ValidateScreenplayCommand.when_reporting_open_questions;
+
+public class with_report_mode : given.a_model_with_questions
+{
+ Task Because() => Run("screenplay", "validate", _root, "--open-questions", "report", "-o", "plain");
+ [Fact] void should_remain_valid() => _exitCode.ShouldEqual(0);
+ [Fact] void should_print_the_question() => _output.ShouldContain("model-note.open-question: Who may record?");
+ [Fact] void should_print_the_count() => _output.ShouldContain("Model notes: 1 open question(s)");
+}
diff --git a/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_unreadable_notes.cs b/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_unreadable_notes.cs
new file mode 100644
index 00000000..6fd83f7a
--- /dev/null
+++ b/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_unreadable_notes.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Cratis.Cli.for_ValidateScreenplayCommand.when_reporting_open_questions;
+
+public class with_unreadable_notes : given.a_model_with_questions
+{
+ void Establish() => File.WriteAllText(_notesPath, "<<<< unreadable");
+ Task Because() => Run("screenplay", "validate", _root, "--open-questions", "report");
+ [Fact] void should_report_invalid_input() => _exitCode.ShouldEqual(1);
+ [Fact] void should_explain_the_read_failure() => _output.ShouldContain("UnreadableNotes");
+}
diff --git a/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_warnings_as_errors.cs b/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_warnings_as_errors.cs
new file mode 100644
index 00000000..380f62cc
--- /dev/null
+++ b/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/with_warnings_as_errors.cs
@@ -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.Cli.for_ValidateScreenplayCommand.when_reporting_open_questions;
+
+public class with_warnings_as_errors : given.a_model_with_questions
+{
+ Task Because() => Run("screenplay", "validate", _root, "--open-questions", "report", "--warnings-as-errors");
+ [Fact] void should_not_treat_questions_as_compiler_warnings() => _exitCode.ShouldEqual(0);
+}
diff --git a/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/without_the_option.cs b/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/without_the_option.cs
new file mode 100644
index 00000000..2b0eac24
--- /dev/null
+++ b/Source/Cli.Specs/for_ValidateScreenplayCommand/when_reporting_open_questions/without_the_option.cs
@@ -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.Cli.for_ValidateScreenplayCommand.when_reporting_open_questions;
+
+public class without_the_option : given.a_model_with_questions
+{
+ void Establish() => File.WriteAllText(_notesPath, "<<<< unreadable");
+ Task Because() => Run("screenplay", "validate", _root);
+ [Fact] void should_ignore_unreadable_notes() => _exitCode.ShouldEqual(0);
+ [Fact] void should_not_report_questions() => _output.ShouldNotContain("Model notes:");
+ [Fact] void should_not_report_a_notes_failure() => _output.ShouldNotContain("UnreadableNotes");
+}
diff --git a/Source/Cli/Commands/Render/Publication/ArtifactPublicationPreparation.cs b/Source/Cli/Commands/Render/Publication/ArtifactPublicationPreparation.cs
index 533b1e33..b81eb74e 100644
--- a/Source/Cli/Commands/Render/Publication/ArtifactPublicationPreparation.cs
+++ b/Source/Cli/Commands/Render/Publication/ArtifactPublicationPreparation.cs
@@ -22,21 +22,13 @@ internal static class ArtifactPublicationPreparation
{
public static PreparedArtifactPublication Prepare(ArtifactPublicationRequest request)
{
- if (request.Plan.Success && request.Selection is null)
- {
- Directory.CreateDirectory(Path.GetFullPath(request.Destination));
- }
-
var analysis = Analyze(request);
if (analysis.Refusals.Count > 0)
{
throw new UnsafeArtifactPublication(analysis.Refusals[0].Reason, analysis.Refusals[0].Code);
}
- if (request.Selection is not null)
- {
- Directory.CreateDirectory(Path.GetFullPath(request.Destination));
- }
+ Directory.CreateDirectory(Path.GetFullPath(request.Destination));
return analysis.Prepared!;
}
@@ -62,13 +54,14 @@ public static ArtifactPublicationAnalysis Analyze(ArtifactPublicationRequest req
return Refused(exception.Message);
}
+ var publicationRefusal = CheckPublication(request.Plan, destination);
+ if (publicationRefusal is not null)
+ {
+ return publicationRefusal;
+ }
+
if (request.Selection is not null)
{
- var policyCheck = CratisRendering.CheckPublication(request.Plan, path => ReadExisting(destination, path));
- if (policyCheck is CratisPublicationCheck.RequiresApplicationScope required)
- {
- return Refused($"Incompatible policy layout: {string.Join(", ", required.Paths)}. Render the whole application once before rendering a scope.", "CLI-RENDER-013");
- }
var missing = MissingDependencies(request, previous);
if (missing.Count > 0)
{
@@ -114,7 +107,7 @@ public static ArtifactPublicationAnalysis Analyze(ArtifactPublicationRequest req
var owned = previousByPath.GetValueOrDefault(artifact.Path);
var exists = File.Exists(path);
- // An unmanaged file is refused without reading it.
+ // An unmanaged file is refused without hashing it; Stage's publication check may already have read it for event type ids.
var currentHash = exists && owned is not null ? ArtifactPublicationStorage.Hash(path) : null;
var reason = ActiveRefusal(artifact.Path, path, owned, exists, currentHash, request.Force);
if (reason is not null)
@@ -203,6 +196,36 @@ static bool IsSelected(ManagedArtifact artifact, ArtifactPublicationRequest requ
((artifact.Owners is null || artifact.Owners.Count == 0) && artifact.Sources.Count > 0 &&
artifact.Sources.All(id => request.Selection!.Contains(id, request.SemanticAddresses)));
+ static ArtifactPublicationAnalysis? CheckPublication(ArtifactRenderPlan plan, string destination)
+ {
+ CratisPublicationCheck check;
+ try
+ {
+ check = CratisRendering.CheckPublication(plan, path => ReadExisting(destination, path));
+ }
+ catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
+ {
+ return Refused($"An existing destination file could not be read for the publication check: {exception.Message}");
+ }
+
+ return check switch
+ {
+ CratisPublicationCheck.Compatible => null,
+ CratisPublicationCheck.RequiresApplicationScope required => Refused(
+ $"Incompatible policy layout: {string.Join(", ", required.Paths)}. Render the whole application once before rendering a scope.",
+ "CLI-RENDER-013"),
+ CratisPublicationCheck.ChangesEventTypeIds eventIds => Refused(
+ string.Join(' ', new[]
+ {
+ $"Stored event type ids would change or could not be determined: {string.Join("; ", eventIds.Changes.Select(change => $"{change.Path}: {change.TypeName}: {change.ExistingId} -> {change.PlannedId ?? ""}"))}.",
+ string.Join("; ", eventIds.Diagnostics.Select(diagnostic => $"{diagnostic.Code}: {diagnostic.Message}")),
+ "Pin the event's 'id' to keep its stored event type id."
+ }.Where(part => part.Length > 0)),
+ "CLI-RENDER-016"),
+ _ => Refused("The Stage publication check did not confirm compatibility.")
+ };
+ }
+
static string? ReadExisting(string destination, string relativePath)
{
ArtifactPublicationStorage.EnsureSafePath(destination, relativePath);
diff --git a/Source/Cli/Commands/Render/RenderedSemanticVersions.cs b/Source/Cli/Commands/Render/RenderedSemanticVersions.cs
index d10381f1..eca61f96 100644
--- a/Source/Cli/Commands/Render/RenderedSemanticVersions.cs
+++ b/Source/Cli/Commands/Render/RenderedSemanticVersions.cs
@@ -18,7 +18,8 @@ namespace Cratis.Cli.Commands.Render;
/// reach its construct-level admission: unsupported public/foreign events, captures and production route
/// overrides receive typed refusals; reaction identity and observer filters render within their supported subset.
/// ESM v11 lossless Double-mode number literals, v12 enum member numbers, v13 concept compliance,
-/// v14 equality query filters and v15 concept generation strategies with freshness scopes are admitted by Stage 4.92.0.
+/// v14 equality query filters, v15 concept generation strategies with freshness scopes and v16 stored event names
+/// are admitted by Stage 4.96.0.
/// Newer versions remain refused until their renderer admission has been reviewed.
///
public static class RenderedSemanticVersions
@@ -31,7 +32,7 @@ public static class RenderedSemanticVersions
///
/// Gets the newest model version cratis render admits.
///
- public static SemanticVersion Newest { get; } = SemanticVersion.V15;
+ public static SemanticVersion Newest { get; } = SemanticVersion.V16;
///
/// Reports a model version cratis render does not admit.
diff --git a/Source/Cli/Commands/Screenplay/AdmitBaselineScreenplayCommand.cs b/Source/Cli/Commands/Screenplay/AdmitBaselineScreenplayCommand.cs
new file mode 100644
index 00000000..3f89367f
--- /dev/null
+++ b/Source/Cli/Commands/Screenplay/AdmitBaselineScreenplayCommand.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Screenplay.Mcp;
+
+namespace Cratis.Cli.Commands.Screenplay;
+
+///
+/// Admits released event revisions into the local release baseline.
+///
+[CommandEffect(CommandEffect.Local)]
+[CliCommand("admit", "Admit released event revisions into the release baseline", Branch = typeof(ScreenplayBranch.Baseline))]
+[CliExample("screenplay", "baseline", "admit", "./Model", "--release", "v1.0.0")]
+[CliExample("screenplay", "baseline", "admit", "./Model", "--release", "v1.0.0", "--from-esm", "./shipped.esm.json")]
+[LlmDescription("Monotonically admits released event revisions and atomically writes the local release baseline under Screenplay's recovery journal. Requires persisted .screenplay/identities.json and --release. Exit codes: 0 admitted, 1 violations, 2 unreadable or refused; CLI usage errors use -1 (255 on POSIX). These differ from the general CLI exit codes.")]
+[LlmOption("[ROOT]", "string", "Model root. Defaults to the current directory, honoring mcpServers.screenplay.root in .cratis/ai.json.")]
+[LlmOption("--release", "string", "Required release label for admission.")]
+[LlmOption("--from-esm", "string", "Optional canonical shipped ESM file; otherwise admit the current model.")]
+[LlmOutputAdvice("json-compact", "Structured violations, failureKind, reason, admittedEntryCount, baselineRevision and exitCode; plain text by default.")]
+public class AdmitBaselineScreenplayCommand : Command
+{
+ ///
+ public override int Execute(CommandContext context, AdmitBaselineScreenplaySettings settings, CancellationToken cancellationToken) =>
+ ScreenplayReleaseBaseline.Execute(context, settings, root => ReleaseBaselineCommands.Admit(root, settings.Release!, settings.FromEsm));
+}
diff --git a/Source/Cli/Commands/Screenplay/AdmitBaselineScreenplaySettings.cs b/Source/Cli/Commands/Screenplay/AdmitBaselineScreenplaySettings.cs
new file mode 100644
index 00000000..72d2153a
--- /dev/null
+++ b/Source/Cli/Commands/Screenplay/AdmitBaselineScreenplaySettings.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Cratis.Cli.Commands.Screenplay;
+
+///
+/// Settings for admitting a Screenplay release baseline.
+///
+public class AdmitBaselineScreenplaySettings : BaselineScreenplaySettings
+{
+ ///
+ /// Gets or sets the release label.
+ ///
+ [CommandOption("--release ")]
+ [Description("Required release label for the admitted event revisions.")]
+ public string? Release { get; set; }
+
+ ///
+ /// Gets or sets the canonical shipped executable semantic model file.
+ ///
+ [CommandOption("--from-esm ")]
+ [Description("Canonical shipped executable semantic model to admit instead of the current model.")]
+ public string? FromEsm { get; set; }
+
+ ///
+ public override ValidationResult Validate() => string.IsNullOrWhiteSpace(Release)
+ ? ValidationResult.Error("--release is required for baseline admission.")
+ : base.Validate();
+}
diff --git a/Source/Cli/Commands/Screenplay/BaselineScreenplaySettings.cs b/Source/Cli/Commands/Screenplay/BaselineScreenplaySettings.cs
new file mode 100644
index 00000000..00d90c22
--- /dev/null
+++ b/Source/Cli/Commands/Screenplay/BaselineScreenplaySettings.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Cratis.Cli.Commands.Screenplay;
+
+///
+/// Settings for checking a Screenplay release baseline.
+///
+public class BaselineScreenplaySettings : GlobalSettings
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public BaselineScreenplaySettings() => Output = OutputFormats.Plain;
+
+ ///
+ /// Gets or sets the model root containing persisted identities.
+ ///
+ [CommandArgument(0, "[ROOT]")]
+ [Description("Model root containing .screenplay/identities.json. Defaults to the current directory, honoring the configured Screenplay MCP root.")]
+ public string? Root { get; set; }
+}
diff --git a/Source/Cli/Commands/Screenplay/CheckBaselineScreenplayCommand.cs b/Source/Cli/Commands/Screenplay/CheckBaselineScreenplayCommand.cs
new file mode 100644
index 00000000..26ae8230
--- /dev/null
+++ b/Source/Cli/Commands/Screenplay/CheckBaselineScreenplayCommand.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Screenplay.Mcp;
+
+namespace Cratis.Cli.Commands.Screenplay;
+
+///
+/// Checks released event revisions without changing source or identity state.
+///
+[CommandEffect(CommandEffect.ReadOnly)]
+[CliCommand("check", "Check released event revisions against the release baseline", Branch = typeof(ScreenplayBranch.Baseline))]
+[CliExample("screenplay", "baseline", "check")]
+[CliExample("screenplay", "baseline", "check", "./Model", "-o", "json-compact")]
+[LlmDescription("Checks the model's released event revisions using persisted .screenplay/identities.json and its release baseline. Never reconciles or writes identities. Exit codes: 0 clean, 1 violations, 2 unreadable or refused; CLI usage errors use -1 (255 on POSIX). These differ from the general CLI exit codes.")]
+[LlmOption("[ROOT]", "string", "Model root. Defaults to the current directory, honoring mcpServers.screenplay.root in .cratis/ai.json.")]
+[LlmOutputAdvice("json-compact", "Structured violations, failureKind, reason, admittedEntryCount, baselineRevision and exitCode; plain text by default.")]
+public class CheckBaselineScreenplayCommand : Command
+{
+ ///
+ public override int Execute(CommandContext context, BaselineScreenplaySettings settings, CancellationToken cancellationToken) =>
+ ScreenplayReleaseBaseline.Execute(context, settings, ReleaseBaselineCommands.Check);
+}
diff --git a/Source/Cli/Commands/Screenplay/ScreenplayOpenQuestions.cs b/Source/Cli/Commands/Screenplay/ScreenplayOpenQuestions.cs
new file mode 100644
index 00000000..04e04dcd
--- /dev/null
+++ b/Source/Cli/Commands/Screenplay/ScreenplayOpenQuestions.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Cratis.Screenplay.Mcp;
+using Cratis.Screenplay.Workspaces;
+
+namespace Cratis.Cli.Commands.Screenplay;
+
+internal static class ScreenplayOpenQuestions
+{
+ internal static ModelNoteQuestionsOutcome? Read(string target, ValidateScreenplaySettings settings) => settings.OpenQuestions.Length == 0
+ ? null
+ : ModelNoteQuestions.Read(Directory.Exists(target) ? target : Path.GetDirectoryName(target)!, settings.Scope);
+
+ internal static int ExitCode(ModelNoteQuestionsOutcome? outcome, ValidateScreenplaySettings settings) => outcome?.GetExitCode(settings.OpenQuestions.FirstOrDefault() == "fail") switch
+ {
+ 1 => ExitCodes.ValidationError,
+ 2 => ExitCodes.NotFound,
+ _ => ExitCodes.Success
+ };
+
+ internal static void Write(string format, ModelNoteQuestionsOutcome? outcome)
+ {
+ if (outcome is null) return;
+ if (outcome.FailureKind is not null)
+ {
+ OutputFormatter.WriteError(format, $"{outcome.FailureKind}: {outcome.Reason}", "Check that the model notes and persisted identities are readable", ExitCodes.NotFoundCode);
+ return;
+ }
+ if (new[] { OutputFormats.Json, OutputFormats.JsonCompact, OutputFormats.JsonQuiet, OutputFormats.Quiet }.Contains(format, StringComparer.Ordinal)) return;
+ if (outcome.IdentityStatus == "absent")
+ {
+ Console.WriteLine("Identity state absent: .screenplay/identities.json is missing; notes cannot be scoped. Reporting open questions unscoped as orphaned (identity-state-absent).");
+ }
+ foreach (var finding in outcome.Findings)
+ {
+ Console.WriteLine($"{Address(finding, outcome.OrphanReason)}: {finding.Severity} {finding.Code}: {finding.Message} ({finding.NoteId})");
+ }
+ Console.WriteLine($"Model notes: {outcome.Findings.Length} open question(s){(outcome.EffectiveScope is null ? string.Empty : " in scope")}");
+ }
+
+ static string Address(ModelNotesFinding finding, string? orphanReason)
+ {
+ if (finding.Address is not null)
+ {
+ return string.Join('.', finding.Address.Parts.Where(part => part.Kind != "Application").Select(part => part.Key));
+ }
+
+ return orphanReason is null ? "" : $"";
+ }
+}
diff --git a/Source/Cli/Commands/Screenplay/ScreenplayReleaseBaseline.cs b/Source/Cli/Commands/Screenplay/ScreenplayReleaseBaseline.cs
new file mode 100644
index 00000000..f759ad7b
--- /dev/null
+++ b/Source/Cli/Commands/Screenplay/ScreenplayReleaseBaseline.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Cratis. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Text.Json;
+using Cratis.Cli.Commands.Ai;
+using Cratis.Screenplay.Mcp;
+
+namespace Cratis.Cli.Commands.Screenplay;
+
+internal static class ScreenplayReleaseBaseline
+{
+ internal static int Execute(CommandContext context, BaselineScreenplaySettings settings, Func run)
+ {
+ var format = !settings.Quiet && string.Equals(settings.Output, OutputFormats.Auto, StringComparison.OrdinalIgnoreCase)
+ ? OutputFormats.Plain
+ : settings.ResolveOutputFormat();
+ if (context.Remaining.Parsed.Count > 0 || context.Remaining.Raw.Count > 0)
+ {
+ var option = context.Remaining.Raw.Count > 0 ? context.Remaining.Raw[0] : context.Remaining.Parsed.First().Key;
+ OutputFormatter.WriteError(format, $"Unknown option '{option}'.", "Run 'cratis screenplay baseline --help' for usage", "CLI-BASELINE-USAGE");
+ return -1;
+ }
+
+ ReleaseBaselineOutcome outcome;
+ try
+ {
+ var currentDirectory = Directory.GetCurrentDirectory();
+ var root = ScreenplayMcpRoot.Resolve(settings.Root, null, null, currentDirectory, Environment.GetEnvironmentVariable) ?? currentDirectory;
+ outcome = run(root);
+ }
+ catch (Exception exception) when (exception is AiMcpConfigurationInvalid or ScreenplayWorkspaceStateConflict or ScreenplayWorkspaceMetadataConflict or JsonException or IOException or UnauthorizedAccessException)
+ {
+ outcome = new(2, [], exception.GetType().Name, exception.Message);
+ }
+
+ OutputFormatter.WriteObject(format, outcome, result =>
+ {
+ foreach (var violation in result.Violations)
+ {
+ Console.WriteLine($"{violation.Kind}: {violation.Event} revision {violation.Revision}{(violation.Path is null ? string.Empty : $" at {violation.Path}")}");
+ }
+ if (result.AdmittedEntryCount is { } count)
+ {
+ Console.WriteLine($"Admitted {count} event revision(s) in release baseline {result.BaselineRevision}.");
+ }
+ });
+ if (outcome.Reason is not null && format is not (OutputFormats.Json or OutputFormats.JsonCompact or OutputFormats.JsonQuiet))
+ {
+ Console.Error.WriteLine(outcome.Reason.StartsWith($"{outcome.FailureKind}:", StringComparison.Ordinal)
+ ? outcome.Reason
+ : $"{outcome.FailureKind}: {outcome.Reason}");
+ }
+
+ return outcome.ExitCode;
+ }
+}
diff --git a/Source/Cli/Commands/Screenplay/ValidateScreenplayCommand.cs b/Source/Cli/Commands/Screenplay/ValidateScreenplayCommand.cs
index 58e84ad2..ca1ccae1 100644
--- a/Source/Cli/Commands/Screenplay/ValidateScreenplayCommand.cs
+++ b/Source/Cli/Commands/Screenplay/ValidateScreenplayCommand.cs
@@ -10,7 +10,7 @@ namespace Cratis.Cli.Commands.Screenplay;
/// Compiles Cratis Screenplay (.play ) documents and reports everything the compiler found, whatever wrote
/// them — screenplay generate , prologue , or a person.
///
-[LlmDescription("Compiles Cratis Screenplay (.play) documents and reports every diagnostic the compiler produces. Takes a root .play file together with its imports, or a folder in which case every .play file beneath it is compiled as one application. Nothing needs to be running. Use repeatable --check selections for opt-in structural completeness warnings; these are skipped when source compilation has errors and do not prove runtime completeness. Completeness and --executable can be checked together. --scope selects source diagnostics for a module, feature or slice and its direct dependents; the exit code covers only that set and whole-application counts are always reported separately. Check executable binding for the whole application separately. Diagnostics go to standard error, grouped by severity; errors fail validation, while warnings fail only with --warnings-as-errors (alias --warnaserror). Exit codes: 0 for valid source (and binding with --executable); 1 for missing/invalid input or scope; 5 for validation errors, warnings with --warnings-as-errors, unknown options or --check selections; -1 (255 on POSIX) for CLI usage errors or unexpected failures.")]
+[LlmDescription("Compiles Cratis Screenplay (.play) documents and reports every diagnostic the compiler produces. Takes a root .play file together with its imports, or a folder in which case every .play file beneath it is compiled as one application. Nothing needs to be running. Use repeatable --check selections for opt-in structural completeness warnings; these are skipped when source compilation has errors and do not prove runtime completeness. Completeness and --executable can be checked together. --scope selects source diagnostics for a module, feature or slice and its direct dependents; the exit code covers only that set and whole-application counts are always reported separately. Check executable binding for the whole application separately. Diagnostics go to standard error, grouped by severity; errors fail validation, while warnings fail only with --warnings-as-errors (alias --warnaserror). --open-questions report prints open model questions after compiler diagnostics; fail gates on them with exit 5. Notes are ignored without the option and do not participate in --warnings-as-errors. Exit codes: 0 for valid source (and binding with --executable); 1 for missing/invalid input or scope, or unreadable model notes; 5 for validation errors, warnings with --warnings-as-errors, open questions in fail mode, unknown options or invalid --check/--open-questions selections; -1 (255 on POSIX) for CLI usage errors or unexpected failures.")]
[CommandEffect(CommandEffect.ReadOnly)]
[CliCommand("validate", "Validate Screenplay (.play) documents. Exit 0 valid, 1 invalid input or scope, 5 validation failed", Branch = typeof(ScreenplayBranch))]
[CliExample("screenplay", "validate")]
@@ -18,12 +18,14 @@ namespace Cratis.Cli.Commands.Screenplay;
[CliExample("screenplay", "validate", "./plays")]
[CliExample("screenplay", "validate", "--executable", "./plays")]
[CliExample("screenplay", "validate", "./plays", "--check", "all")]
+[CliExample("screenplay", "validate", "./plays", "--open-questions", "fail")]
[CliExample("screenplay", "validate", "./plays", "--check", "navigation", "--check", "field-origins", "--executable")]
[LlmOption("[PATH]", "string", "Root Screenplay (.play) file with its imports, or folder to compile every .play file beneath as one application. Defaults to the current directory.")]
[LlmOption("--warnings-as-errors", "boolean", "Treat compiler and completeness warnings as validation errors (exit 5). --warnaserror is an alias.")]
[LlmOption("--warnaserror", "boolean", "Alias of --warnings-as-errors; compiler and completeness warnings fail validation (exit 5).")]
[LlmOption("--scope", "string", "Case-sensitive Module[.Feature[.Slice]] address. Selects source diagnostics and direct dependents; whole-application counts remain visible. Unknown or ambiguous scopes fail with exit 1. Combine with --check, not --executable.")]
[LlmOption("--check", "string[]", "Repeatable; selections union. Accepts comma-separated data-bindings (PLAY0530, PLAY0531), input-surfaces (PLAY0532, PLAY0533), field-origins (PLAY0534), query-keys (PLAY0535), event-consumers (PLAY0536), navigation (PLAY0537), personas (PLAY0574–PLAY0576), purposes (PLAY0602–PLAY0606), privilege (PLAY0652), enum-pinning (PLAY0671), or all. Also accepts diagnostic codes. Skipped on source compilation errors. Findings are structural warnings, not proof of runtime completeness.")]
+[LlmOption("--open-questions", "string", "Optional report or fail, not repeatable. Reports open model questions after diagnostics; fail exits 5 if any remain. Unreadable notes exit 1; compiler errors still exit 5. Absent option ignores notes; --warnings-as-errors is unchanged. Honors --scope when identities exist; otherwise reports all questions as unscoped orphans.")]
[LlmOption("--executable", "boolean", "Also bind the model into an executable semantic model; fails with binding diagnostics such as PLAY0268 when it does not bind. Never renders or writes files. A pass without this option only means the source is valid.")]
[LlmOutputAdvice("json-compact", "The summary goes to standard output and the diagnostics to standard error; json-compact makes both machine-readable.")]
public class ValidateScreenplayCommand : Command
@@ -58,6 +60,12 @@ public override int Execute(CommandContext context, ValidateScreenplaySettings s
return ExitCodes.ValidationError;
}
+ if (settings.OpenQuestions.Length > 1 || settings.OpenQuestions.Any(mode => mode is not ("report" or "fail")))
+ {
+ OutputFormatter.WriteError(format, "--open-questions requires report or fail and cannot be repeated.", "Run 'cratis screenplay validate --help' for usage", ExitCodes.ValidationErrorCode);
+ return ExitCodes.ValidationError;
+ }
+
var checkValidation = settings.ValidateChecks();
if (!checkValidation.Successful)
{
@@ -112,6 +120,8 @@ public override int Execute(CommandContext context, ValidateScreenplaySettings s
}
ScreenplayDiagnosticsWriter.Write(format, validated.Diagnostics);
+ var openQuestions = ScreenplayOpenQuestions.Read(target.Path!, settings);
+ ScreenplayOpenQuestions.Write(format, openQuestions);
var exitCode = validated.Executable == false
? ExitCodes.ValidationError
@@ -132,15 +142,16 @@ public override int Execute(CommandContext context, ValidateScreenplaySettings s
? "Fix the reported errors and warnings in the Screenplay document"
: "Fix the reported errors in the Screenplay document";
OutputFormatter.WriteError(format, message, suggestion, ExitCodes.ValidationErrorCode);
- if (checks.Selected.Count > 0 || validated.Scoped is not null)
+ if (checks.Selected.Count > 0 || validated.Scoped is not null || openQuestions is not null)
{
- WriteResult(format, target.Path!, validated, false, settings.WarningsAsErrors);
+ WriteResult(format, target.Path!, validated, false, settings.WarningsAsErrors, openQuestions);
}
return exitCode;
}
- WriteResult(format, target.Path!, validated, warningsAsErrors: settings.WarningsAsErrors);
- return ExitCodes.Success;
+ var notesExitCode = ScreenplayOpenQuestions.ExitCode(openQuestions, settings);
+ WriteResult(format, target.Path!, validated, notesExitCode == ExitCodes.Success, settings.WarningsAsErrors, openQuestions);
+ return notesExitCode;
}
static string CheckName(CompletenessCheck check) => check switch
@@ -154,7 +165,7 @@ public override int Execute(CommandContext context, ValidateScreenplaySettings s
_ => check.ToString()
};
- static void WriteResult(string format, string targetPath, ValidatedScreenplay validated, bool success = true, bool warningsAsErrors = false)
+ static void WriteResult(string format, string targetPath, ValidatedScreenplay validated, bool success = true, bool warningsAsErrors = false, ModelNoteQuestionsOutcome? openQuestions = null)
{
if (string.Equals(format, OutputFormats.Quiet, StringComparison.Ordinal))
{
@@ -175,6 +186,7 @@ static void WriteResult(string format, string targetPath, ValidatedScreenplay va
{
Path = targetPath,
Valid = success,
+ OpenQuestions = openQuestions,
Files = validated.FileCount,
Diagnostics = validated.Diagnostics.Count,
Errors = validated.Diagnostics.Count(diagnostic => diagnostic.Severity == ScreenplayDiagnosticSeverity.Error),
diff --git a/Source/Cli/Commands/Screenplay/ValidateScreenplaySettings.cs b/Source/Cli/Commands/Screenplay/ValidateScreenplaySettings.cs
index f34f7325..33a6fbe1 100644
--- a/Source/Cli/Commands/Screenplay/ValidateScreenplaySettings.cs
+++ b/Source/Cli/Commands/Screenplay/ValidateScreenplaySettings.cs
@@ -46,6 +46,14 @@ public class ValidateScreenplaySettings : GlobalSettings
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "Spectre.Console.Cli uses arrays for repeatable options.")]
public string[] Checks { get; set; } = [];
+ ///
+ /// Gets or sets the optional open-question mode. Only one selection is allowed.
+ ///
+ [CommandOption("--open-questions ")]
+ [Description("Report open model questions (report), or fail validation if any remain (fail). Cannot be repeated. Unreadable notes exit 1.")]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "Capture repeated options so the command can reject them with exit 5.")]
+ public string[] OpenQuestions { get; set; } = [];
+
///
/// Gets the union of the selected completeness checks after settings validation.
///
diff --git a/Source/Cli/Registration/ScreenplayBranch.cs b/Source/Cli/Registration/ScreenplayBranch.cs
index e6d8b84c..55f7c3fc 100644
--- a/Source/Cli/Registration/ScreenplayBranch.cs
+++ b/Source/Cli/Registration/ScreenplayBranch.cs
@@ -8,7 +8,7 @@ namespace Cratis.Cli.Registration;
///
/// Authoring and generation of Cratis Screenplay (.play ) documents.
///
-[CliBranch("screenplay", "Work with Cratis Screenplay (.play) documents. Generate a Screenplay from source code, check code conformance to an authored model, validate your documents, host the embedded MCP server, or manage its desktop distribution.")]
+[CliBranch("screenplay", "Work with Cratis Screenplay (.play) documents. Generate a Screenplay from source code, check code conformance to an authored model, validate your documents, host the embedded MCP server, manage its desktop distribution, or admit and check its release baseline.")]
public static class ScreenplayBranch
{
///
@@ -16,4 +16,10 @@ public static class ScreenplayBranch
///
[CliBranch("desktop", "Install, update, inspect, and remove the Screenplay MCP server in Claude Desktop and ChatGPT Desktop.")]
public static class Desktop;
+
+ ///
+ /// Release baseline of a Screenplay model: which event revisions a release shipped.
+ ///
+ [CliBranch("baseline", "Admit and check the release baseline that records which event revisions a release shipped.")]
+ public static class Baseline;
}