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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@
</PropertyGroup>
<ItemGroup>
<!-- Cratis -->
<PackageVersion Include="Cratis.Arc.Screenplay" Version="20.65.0" />
<PackageVersion Include="Cratis.Chronicle.Connections" Version="16.0.6" />
<PackageVersion Include="Cratis.Chronicle.Contracts" Version="16.0.6" />
<PackageVersion Include="Cratis.Fundamentals" Version="7.16.4" />
<PackageVersion Include="Cratis.Fundamentals" Version="7.16.6" />
<!-- Prologue -->
<PackageVersion Include="Cratis.Prologue.Configuration" Version="1.1.0" />
<PackageVersion Include="Cratis.Prologue.Contracts" Version="1.1.0" />
<PackageVersion Include="Cratis.Prologue.Interpretation" Version="1.1.0" />
<PackageVersion Include="Cratis.Prologue.Interpreter.Contracts" Version="1.1.0" />
<PackageVersion Include="Cratis.Prologue.Screenplay" Version="1.1.0" />
<PackageVersion Include="Cratis.Screenplay" Version="1.4.0" />
<PackageVersion Include="Cratis.Screenplay" Version="1.5.2" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
<!-- CLI -->
<PackageVersion Include="MongoDB.Driver" Version="3.10.0" />
Expand All @@ -27,6 +28,12 @@
<PackageVersion Include="System.Text.Json" Version="10.0.9" />
<!-- Source Generator -->
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" />
<!-- Source reading — loading a solution or project into a Roslyn compilation for Screenplay generation. -->
<PackageVersion Include="Microsoft.Build.Locator" Version="1.11.2" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="5.6.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.6.0" />
<!-- Compile-time only: MSBuildLocator resolves the MSBuild assemblies from the installed .NET SDK at run time. -->
<PackageVersion Include="Microsoft.Build.Framework" Version="17.11.48" />
<!-- Analysis -->
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" NoWarn="NU5104" />
<PackageVersion Include="Roslynator.Analyzers" Version="4.15.0" />
Expand Down
1 change: 1 addition & 0 deletions Documentation/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This section documents the cross-cutting mechanics of the `cratis` CLI: the flag

- [LLM](llm.md) — Configuring the language model that Cratis tools like Prologue use.
- [Prologue](prologue.md) — Capturing a running system and interpreting the captures into a Screenplay.
- [Screenplay](screenplay.md) — Generating a Screenplay from the source code of a Cratis Arc application, and validating the documents you have.
- [Global Options](global-options.md) — Flags such as `--output`, `--quiet`, `--yes`, and `--debug` that apply to every command.
- [Output Formats](output-formats.md) — The four output formats (`table`, `plain`, `json`, `json-compact`) with guidance on when to use each and token cost comparisons.
- [Connection](connection.md) — Connection string format, resolution order, and environment variable configuration.
145 changes: 145 additions & 0 deletions Documentation/reference/screenplay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Screenplay

`cratis screenplay` works with Cratis Screenplay (`.play`) documents. It generates one from the source code of a Cratis Arc application — so the event model your team reads is derived from the code that actually runs rather than maintained alongside it — and it compiles the documents you already have.

```bash
cratis screenplay generate [PATH]
cratis screenplay validate [PATH]
```

**Nothing needs to be running.** This is what separates `cratis screenplay` from [`cratis arc`](../arc/index.md): every `arc` command talks to a *running* application over HTTP, while `screenplay` only ever reads files. The result is reproducible from a checkout — commit it, diff it, and run it in CI, on a machine where the application was never started.

Fetching a `.play` document from a running Arc application over its introspection endpoint is a separate, complementary route: it trades the SDK requirement for the requirement that the application be running. That route does not exist yet — neither the Arc endpoint nor a CLI command for it — so generating from source is today the only way to derive a Screenplay from a Cratis Arc application.

## `cratis screenplay generate [PATH]`

Reads a solution or project, derives the event model from the Arc artifacts it finds — commands, events, read models, projections, reactors, constraints, and the concepts they are built from — and writes a Screenplay document.

By default the document goes to standard output, so it composes with the shell:

```bash
cratis screenplay generate > MyApp.play
```

Pass `--file` to write it directly instead. The output is written as raw UTF-8, byte for byte, ending in exactly one newline — regenerating an unchanged model produces an identical file.

### Arguments

| Argument | Description |
|---|---|
| `PATH` | Solution (`.slnx`, `.sln`), project (`.csproj`), or folder to read. Defaults to the current directory. |

### Options

| Option | Description |
|---|---|
| `--file <FILE>` | File to write the generated Screenplay to. Writes to standard output when not given. |
| `--domain <NAME>` | Name of the domain the generated document belongs to. Defaults to the assembly or root namespace of the project. |
| `--module <NAME>` | Name of the module every discovered feature is placed within. Defaults to the domain. |
| `--skip-segments <COUNT>` | Number of leading namespace segments to skip when inferring features and slices. |

The output file uses `--file` rather than `-o`, because `-o/--output` is the global output *format* flag — see [Global Options](global-options.md).

```bash
cratis screenplay generate
cratis screenplay generate ./MyApp.slnx --file MyApp.play
cratis screenplay generate ./Source/MyApp/MyApp.csproj
cratis screenplay generate --domain Library --module Lending --file Library.play
```

### Finding the solution or project

When `PATH` is a solution or project file, that file is read. When it is a folder — or is omitted entirely — the CLI looks in that folder and then in each parent folder in turn, stopping at the first one that holds a match. Within a folder it prefers `.slnx`, then `.sln`, then `.csproj`. Two candidates of the same kind in one folder is reported rather than guessed at.

A Screenplay describes one application, so a solution has to narrow down to a single project:

1. Projects whose name ends in `.Specs`, `.Specifications`, `.Tests`, `.Test`, or `.IntegrationTests` are dropped.
2. If any project that remains produces an executable, the libraries are dropped — an Arc application is the executable.
3. Exactly one project must remain. Anything else is reported, listing the candidates, so you can pass the project you meant.

### Diagnostics

Anything the generator cannot express in Screenplay is reported rather than silently dropped — a projection operator with no counterpart, a validator rule that has no equivalent, a construct only available as compiled metadata because it lives in a referenced package.

Diagnostics always go to **standard error**, grouped by severity with errors first, so redirecting standard output to a `.play` file never mixes them in:

```text
errors (1):
error SP0203: [Library.Lending.Reserving] projection uses $combine, which has no Screenplay counterpart

warnings (2):
warning SP0110: [Library.Authors] command handler body is not available in this compilation
warning SP0141: [Library.Authors.Registration] validator rule Must() cannot be expressed
```

With `-o json` or `-o json-compact` the same diagnostics are written to standard error as a JSON object instead.

**Warnings and information do not fail the command** — the document is still written. **An error does**: nothing is written and the command exits with a validation error, because a document that does not describe the source faithfully is worse than no document.

### Prerequisites

The command loads the project through MSBuild, so the **.NET SDK** must be installed — the same SDK you build the project with. Packages must be restorable; a project that cannot be restored cannot be read.

The project does **not** have to have been built first. Sources MSBuild generates as part of a build — such as the strongly typed classes a `.resx` file declares with `<Generator>MSBuild:Compile</Generator>` — are produced while the project is read, so the model is derived from exactly what a real build compiles.

### Errors

| Condition | Result |
|---|---|
| `PATH` does not exist | Not-found error. |
| `PATH` is a file that is not a solution or project | Not-found error. |
| No solution or project found in `PATH` or any parent folder | Not-found error. |
| The solution holds more than one candidate project | Validation error listing the candidates. |
| Generation reports one or more errors, with `--file` | Validation error; the document is written anyway. |
| Generation reports one or more errors, writing to standard output | Validation error; nothing is written. |

An error means the document does not describe the source faithfully — but a document that is 99% right plus honest diagnostics is more useful than nothing at all, so `--file` still writes it. Read the diagnostics before trusting it, and re-run with `screenplay validate` to see what the Screenplay compiler makes of the result.

Standard output is the exception: whatever consumes `cratis screenplay generate > MyApp.play` cannot tell a partial document from a complete one, so nothing is written there. Pass `--file` when you want the partial document.

## `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.

`PATH` is a Screenplay (`.play`) file, or a folder — in which case every `.play` file beneath it is compiled. It defaults to the current directory.

```bash
cratis screenplay validate # every .play file beneath the current folder
cratis screenplay validate ./MyApp.play # one document
cratis screenplay validate ./plays # every .play file beneath a folder
```

### Compiler diagnostics

Diagnostics go to **standard error**, grouped by severity with errors first, in the same shape `generate` uses. The compiler does not assign codes, so each line carries the file and the position within it instead:

```text
errors (1):
error: [MyApp.play(5,5)] Invalid slice declaration 'slice Reserving' - expected 'slice <Type> <Name>'

warnings (1):
warning: [MyApp.play(787,11)] Unknown event 'InvitationToJoinAdaAccepted' - declare it with 'event InvitationToJoinAdaAccepted'
```

With `-o json` or `-o json-compact` the same diagnostics are written to standard error as a JSON object instead.

**Warnings and information do not fail the command. An error does** — which is what makes this usable as a CI gate on a committed `.play` file.

### Validation outcomes

| Condition | Result |
|---|---|
| `PATH` does not exist | Not-found error. |
| `PATH` is a file that is not a `.play` file | Not-found error. |
| No `.play` file found in the folder | Not-found error — validating nothing is never the answer you wanted. |
| Compilation reports one or more errors | Validation error. |

## Where a Screenplay comes from

Generating from source is one of three ways to arrive at a `.play` file, and they meet in the same place:

- **From source** — `screenplay generate`, for an application that already exists in Cratis Arc. Needs the .NET SDK and a checkout; needs nothing running.
- **From a running system** — [`cratis prologue`](prologue.md) captures what a system does and interprets it into a Screenplay, for systems built without Cratis.
- **By hand** — write the `.play` file as the design, before any code exists.

Whichever route you take, `screenplay validate` compiles the result and [`cratis run`](run.md) boots it in a local Stage sandbox.
2 changes: 2 additions & 0 deletions Documentation/reference/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
href: llm.md
- name: Prologue
href: prologue.md
- name: Screenplay
href: screenplay.md
- name: Global Options
href: global-options.md
- name: Output Formats
Expand Down
3 changes: 3 additions & 0 deletions Source/Cli.Specs/Usings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@
global using Cratis.Cli.Commands.Llm;
global using Cratis.Cli.Commands.Prologue;
global using Cratis.Cli.Commands.Run;
global using Cratis.Cli.Commands.Screenplay;
global using Cratis.Specifications;
global using NSubstitute;
global using Spectre.Console.Cli;
global using Xunit;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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_DesignTimeResourceGeneration.given;

public class a_temporary_folder : Specification
{
protected string _folder;

void Establish() => _folder = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())).FullName;

void Destroy()
{
if (Directory.Exists(_folder))
{
Directory.Delete(_folder, true);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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_DesignTimeResourceGeneration.when_building_the_targets_file;

public class and_an_existing_file_is_chained : given.a_temporary_folder
{
string _chained;
string _result;

void Establish()
{
_chained = Path.Combine(_folder, "Custom.After.targets");
File.WriteAllText(_chained, "<Project />");
}

void Because() => _result = DesignTimeResourceGeneration.Targets(_chained);

[Fact] void should_keep_importing_it() => _result.ShouldContain($"<Import Project=\"{_chained}\" />");
[Fact] void should_still_run_the_resource_target_chain() => _result.ShouldContain("DependsOnTargets=\"PrepareResources\"");
}
Original file line number Diff line number Diff line change
@@ -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_DesignTimeResourceGeneration.when_building_the_targets_file;

public class and_nothing_is_chained : Specification
{
string _result;

void Because() => _result = DesignTimeResourceGeneration.Targets(null);

[Fact] void should_run_the_resource_target_chain() => _result.ShouldContain("DependsOnTargets=\"PrepareResources\"");
[Fact] void should_run_it_before_the_compiler_inputs_are_gathered() => _result.ShouldContain("BeforeTargets=\"CoreCompile\"");
[Fact] void should_not_import_anything() => _result.ShouldNotContain("<Import");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Cli.for_DesignTimeResourceGeneration.when_building_the_targets_file;

public class and_the_chained_file_does_not_exist : given.a_temporary_folder
{
string _result;

void Because() => _result = DesignTimeResourceGeneration.Targets(Path.Combine(_folder, "Missing.targets"));

[Fact] void should_not_import_it() => _result.ShouldNotContain("<Import");
[Fact] void should_still_run_the_resource_target_chain() => _result.ShouldContain("DependsOnTargets=\"PrepareResources\"");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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_DesignTimeResourceGeneration.when_building_the_targets_file;

public class and_the_chained_path_holds_markup_characters : given.a_temporary_folder
{
string _chained;
string _result;

void Establish()
{
_chained = Path.Combine(_folder, "Custom & After.targets");
File.WriteAllText(_chained, "<Project />");
}

void Because() => _result = DesignTimeResourceGeneration.Targets(_chained);

[Fact] void should_escape_them() => _result.ShouldContain("Custom &amp; After.targets");
[Fact] void should_leave_the_file_readable_as_markup() => _result.ShouldNotContain("& After");
}
22 changes: 22 additions & 0 deletions Source/Cli.Specs/for_DesignTimeResourceGeneration/when_creating.cs
Original file line number Diff line number Diff line change
@@ -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.for_DesignTimeResourceGeneration;

public class when_creating : Specification
{
DesignTimeResourceGeneration _generation;
string _file;

void Because()
{
_generation = DesignTimeResourceGeneration.Create();
_file = _generation.GlobalProperties[DesignTimeResourceGeneration.HookProperty];
}

[Fact] void should_hook_the_targets_file_into_every_project() => DesignTimeResourceGeneration.HookProperty.ShouldEqual("CustomAfterMicrosoftCommonTargets");
[Fact] void should_write_the_targets_file() => File.Exists(_file).ShouldBeTrue();
[Fact] void should_write_the_resource_target_chain_into_it() => File.ReadAllText(_file).ShouldContain("PrepareResources");

void Destroy() => _generation.Dispose();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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_DesignTimeResourceGeneration.when_disposing;

public class and_it_has_already_been_disposed : Specification
{
DesignTimeResourceGeneration _generation;
Exception _error;

void Establish()
{
_generation = DesignTimeResourceGeneration.Create();
_generation.Dispose();
}

void Because() => _error = Catch.Exception(_generation.Dispose);

[Fact] void should_not_fail() => _error.ShouldBeNull();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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_DesignTimeResourceGeneration.when_disposing;

public class and_the_targets_file_was_written : Specification
{
DesignTimeResourceGeneration _generation;
string _file;

void Establish()
{
_generation = DesignTimeResourceGeneration.Create();
_file = _generation.GlobalProperties[DesignTimeResourceGeneration.HookProperty];
}

void Because() => _generation.Dispose();

[Fact] void should_remove_the_targets_file() => File.Exists(_file).ShouldBeFalse();
}
Loading
Loading