From 2fa56e14c0d1b78f2dea9ee6afce15651e383a42 Mon Sep 17 00:00:00 2001 From: woksin Date: Mon, 10 Aug 2026 19:31:37 +0200 Subject: [PATCH 1/4] Update the Cratis packages to the Screenplay 2.x set Cratis.Screenplay 1.6.4 to 2.1.0, with the two packages built against it moved in the same step: Cratis.Arc.Screenplay to 21.2.0 and the Prologue packages to 1.2.0. Both embed the compiler's syntax types, which are positional records, so a version of either built against Screenplay 1.x resolves against 2.1.0, compiles clean and then throws MissingMethodException the first time a document is generated. Arc.Screenplay 21.1.0 is not enough - it still depends on Screenplay 1.5.2; only 21.2.0 is the 2.x rebuild. Chronicle packages to 16.21.0, which also moves the transitive Cratis.Arc.MongoDB off 21.0.0 and its unlisted SharpCompress dependency. --- Directory.Packages.props | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2ffc34f..f4b5c94 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,17 +5,17 @@ - - - + + + - - - - - - + + + + + + @@ -47,7 +47,7 @@ - + From 604f9c370c051a41794c3bd32d93aa19d8188695 Mon Sep 17 00:00:00 2001 From: woksin Date: Mon, 10 Aug 2026 19:31:45 +0200 Subject: [PATCH 2/4] Report the code the compiler assigns to a validation diagnostic Screenplay 2.x assigns every diagnostic a stable PLAY code. The code was being thrown away and reported as empty, which was accurate against the 1.x compiler and left every diagnostic from 'screenplay validate' impossible to look up, suppress or match on. --- Documentation/reference/screenplay.md | 6 +++--- .../when_validating/and_the_document_has_an_error.cs | 2 +- .../Cli/Commands/Screenplay/ScreenplayDiagnosticsWriter.cs | 4 ++-- Source/Cli/Commands/Screenplay/ScreenplayValidation.cs | 7 ++++--- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Documentation/reference/screenplay.md b/Documentation/reference/screenplay.md index f7bf74b..6be48c6 100644 --- a/Documentation/reference/screenplay.md +++ b/Documentation/reference/screenplay.md @@ -154,14 +154,14 @@ 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: +Diagnostics go to **standard error**, grouped by severity with errors first, in the same shape `generate` uses. Each line carries the compiler's `PLAY` code, then the file and the position within it: ```text errors (1): - error: [MyApp.play(5,5)] Invalid slice declaration 'slice Reserving' - expected 'slice ' + error PLAY0027: [MyApp.play(5,5)] Invalid slice declaration 'slice Reserving' - expected 'slice ' warnings (1): - warning: [MyApp.play(787,11)] Unknown event 'InvitationToJoinAdaAccepted' - declare it with 'event InvitationToJoinAdaAccepted' + warning PLAY0166: [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. diff --git a/Source/Cli.Specs/for_ScreenplayValidation/when_validating/and_the_document_has_an_error.cs b/Source/Cli.Specs/for_ScreenplayValidation/when_validating/and_the_document_has_an_error.cs index 8c137ea..1a56a26 100644 --- a/Source/Cli.Specs/for_ScreenplayValidation/when_validating/and_the_document_has_an_error.cs +++ b/Source/Cli.Specs/for_ScreenplayValidation/when_validating/and_the_document_has_an_error.cs @@ -15,5 +15,5 @@ public class and_the_document_has_an_error : given.a_folder_with_documents [Fact] void should_compile_the_document() => _result.FileCount.ShouldEqual(1); [Fact] void should_report_an_error() => ScreenplayDiagnostics.HasErrors(_result.Diagnostics).ShouldBeTrue(); [Fact] void should_point_at_the_file_and_position() => _result.Diagnostics[0].Location.ShouldEqual("MyApp.play(5,5)"); - [Fact] void should_not_invent_a_code() => _result.Diagnostics[0].Code.ShouldBeEmpty(); + [Fact] void should_carry_the_code_the_compiler_assigned() => _result.Diagnostics[0].Code.ShouldEqual("PLAY0027"); } diff --git a/Source/Cli/Commands/Screenplay/ScreenplayDiagnosticsWriter.cs b/Source/Cli/Commands/Screenplay/ScreenplayDiagnosticsWriter.cs index 9952734..c331f27 100644 --- a/Source/Cli/Commands/Screenplay/ScreenplayDiagnosticsWriter.cs +++ b/Source/Cli/Commands/Screenplay/ScreenplayDiagnosticsWriter.cs @@ -64,8 +64,8 @@ public static void Write(string format, IEnumerable diagno /// The diagnostic to write. /// The line. /// - /// The code and the location are both left out when they are absent — the compiler behind - /// screenplay validate assigns no codes, and a diagnostic about a whole document has no location. + /// The code and the location are both left out when they are absent — a diagnostic about a whole document has + /// no location, and not every reporting system assigns codes. /// public static string LineFor(ScreenplayDiagnostic diagnostic) { diff --git a/Source/Cli/Commands/Screenplay/ScreenplayValidation.cs b/Source/Cli/Commands/Screenplay/ScreenplayValidation.cs index 3390222..8be5eb2 100644 --- a/Source/Cli/Commands/Screenplay/ScreenplayValidation.cs +++ b/Source/Cli/Commands/Screenplay/ScreenplayValidation.cs @@ -46,13 +46,14 @@ public ValidatedScreenplay Validate(string targetPath) /// The diagnostic the compiler reported. /// The . /// - /// The compiler does not assign codes, so the code is left empty. The location carries the file and the position - /// within it, in the file(line,column) form editors and build logs already understand. + /// The compiler assigns every diagnostic a stable PLAY code, which is carried through so that a + /// diagnostic can be looked up, suppressed or matched on rather than only read. The location carries the file + /// and the position within it, in the file(line,column) form editors and build logs already understand. /// static ScreenplayDiagnostic Map(PlayFile file, Diagnostic diagnostic) => new( (ScreenplayDiagnosticSeverity)(int)diagnostic.Severity, - string.Empty, + diagnostic.Code, diagnostic.Message, $"{file.RelativePath}({diagnostic.Location.Line},{diagnostic.Location.Column})"); From eb0c2d507364c84baeac3036f428feafbb0477b8 Mon Sep 17 00:00:00 2001 From: woksin Date: Mon, 10 Aug 2026 19:31:57 +0200 Subject: [PATCH 3/4] Spec generating a document against the real generator The generator and the compiler it is built against meet only at run time, so a mismatched pair of packages builds clean and passes every spec and then throws MissingMethodException the first time anyone runs 'screenplay generate'. Nothing in the suite ran the generator, so that class of failure could only be found by hand. Generating is separated from loading so it can be driven from a compilation built from source. Loading one from disk needs an MSBuild workspace, which starts a build host process of its own and is neither quick nor reliable enough to sit in front of this. --- .../given/an_application_built_from_source.cs | 71 +++++++++++++++++++ ..._source_declares_a_command_and_an_event.cs | 17 +++++ .../Screenplay/ArcScreenplayGeneration.cs | 18 ++++- 3 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 Source/Cli.Specs/for_ArcScreenplayGeneration/given/an_application_built_from_source.cs create mode 100644 Source/Cli.Specs/for_ArcScreenplayGeneration/when_generating/and_the_source_declares_a_command_and_an_event.cs diff --git a/Source/Cli.Specs/for_ArcScreenplayGeneration/given/an_application_built_from_source.cs b/Source/Cli.Specs/for_ArcScreenplayGeneration/given/an_application_built_from_source.cs new file mode 100644 index 0000000..450d8e0 --- /dev/null +++ b/Source/Cli.Specs/for_ArcScreenplayGeneration/given/an_application_built_from_source.cs @@ -0,0 +1,71 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Cratis.Cli.for_ArcScreenplayGeneration.given; + +/// +/// Builds an application from source, so that generating from it runs the real generator rather than a substitute. +/// +/// +/// Everything the CLI knows about the Cratis.Arc.Screenplay generator and the Cratis.Screenplay +/// compiler it holds through package references, and a mismatched pair of the two compiles clean and specs green +/// and then throws the first time a document is generated for real — the +/// generator is built against the compiler's syntax types, and those are positional records whose constructors +/// change shape between major versions. Nothing short of generating catches that. +/// +/// The source declares the two attributes the generator looks for rather than referencing Arc and Chronicle to get +/// them. The generator resolves them by full name, so declaring them is enough to describe an application, and it +/// keeps the compilation to one syntax tree and no package restore. +/// +/// +public class an_application_built_from_source : Specification +{ + /// + /// The name the compilation, and therefore the project the document is generated from, goes by. + /// + protected const string ProjectName = "Bookshop"; + + const string Source = + "namespace Cratis.Arc.Commands.ModelBound { public class CommandAttribute : System.Attribute { } }\n" + + "namespace Cratis.Chronicle.Events { public class EventTypeAttribute : System.Attribute { } }\n" + + "\n" + + "namespace Bookshop.Lending.Reserving\n" + + "{\n" + + " [Cratis.Chronicle.Events.EventType]\n" + + " public record BookReserved(string MemberId);\n" + + "\n" + + " [Cratis.Arc.Commands.ModelBound.Command]\n" + + " public record ReserveBook(string BookId);\n" + + "}\n"; + + /// + /// Gets what loading the application would have produced. + /// + protected LoadedCompilation Loaded { get; private set; } + + void Establish() => Loaded = new( + [CSharpCompilation.Create( + ProjectName, + [CSharpSyntaxTree.ParseText(Source)], + References(), + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))], + [ProjectName], + []); + + /// + /// Gets the references the source needs to compile. + /// + /// The for every assembly this process was started with. + /// + /// A compilation without references resolves nothing, not even , and the generator reports + /// source that did not compile rather than the application it describes. Taking what this process already runs + /// against is enough, and keeps the fixture from naming individual framework assemblies. + /// + static IEnumerable References() => + ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!) + .Split(Path.PathSeparator) + .Select(assembly => MetadataReference.CreateFromFile(assembly)); +} diff --git a/Source/Cli.Specs/for_ArcScreenplayGeneration/when_generating/and_the_source_declares_a_command_and_an_event.cs b/Source/Cli.Specs/for_ArcScreenplayGeneration/when_generating/and_the_source_declares_a_command_and_an_event.cs new file mode 100644 index 0000000..ef93d04 --- /dev/null +++ b/Source/Cli.Specs/for_ArcScreenplayGeneration/when_generating/and_the_source_declares_a_command_and_an_event.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_ArcScreenplayGeneration.when_generating; + +public class and_the_source_declares_a_command_and_an_event : given.an_application_built_from_source +{ + GeneratedScreenplay _result; + + void Because() => _result = ArcScreenplayGeneration.GenerateFrom(Loaded, $"{ProjectName}.csproj", ScreenplayGenerationOptions.Default); + + [Fact] void should_say_which_project_it_read() => _result.Projects.ShouldContainOnly([ProjectName]); + [Fact] void should_arrange_the_namespace_into_a_feature_and_a_slice() => _result.Source.ShouldContain("feature Lending"); + [Fact] void should_describe_the_command() => _result.Source.ShouldContain("command ReserveBook"); + [Fact] void should_describe_the_event() => _result.Source.ShouldContain("event BookReserved"); + [Fact] void should_not_report_anything() => _result.Diagnostics.ShouldBeEmpty(); +} diff --git a/Source/Cli/Commands/Screenplay/ArcScreenplayGeneration.cs b/Source/Cli/Commands/Screenplay/ArcScreenplayGeneration.cs index f3c187c..47b758d 100644 --- a/Source/Cli/Commands/Screenplay/ArcScreenplayGeneration.cs +++ b/Source/Cli/Commands/Screenplay/ArcScreenplayGeneration.cs @@ -16,9 +16,23 @@ namespace Cratis.Cli.Commands.Screenplay; public sealed class ArcScreenplayGeneration : IScreenplayGeneration { /// - public async Task Generate(string targetPath, ScreenplayGenerationOptions options, CancellationToken cancellationToken) + public async Task Generate(string targetPath, ScreenplayGenerationOptions options, CancellationToken cancellationToken) => + GenerateFrom(await ScreenplayCompilationLoader.Load(targetPath, cancellationToken), targetPath, options); + + /// + /// Generates the document from compilations that have already been loaded. + /// + /// What was loaded from the target. + /// The solution or project the compilations came from. + /// The options shaping the generated document. + /// The . + /// + /// Kept apart from loading so that generating can be exercised against a compilation built from source. Loading + /// one from disk needs an MSBuild workspace, and standing that up is neither quick nor reliable enough to put + /// in front of the only check that the generator and the compiler it is built against still agree. + /// + internal static GeneratedScreenplay GenerateFrom(LoadedCompilation loaded, string targetPath, ScreenplayGenerationOptions options) { - var loaded = await ScreenplayCompilationLoader.Load(targetPath, cancellationToken); if (loaded.Compilations.Count == 0) { return new GeneratedScreenplay(string.Empty, loaded.Diagnostics); From 0b1edefa56cc0655c281d9a639f3bf78f3165e8a Mon Sep 17 00:00:00 2001 From: woksin Date: Mon, 10 Aug 2026 19:48:29 +0200 Subject: [PATCH 4/4] Hold the Chronicle packages at 16.19.3 Chronicle 16.20.0 flips the default of skipTlsValidation: the client validated nothing by default and now validates the server certificate unless the connection string opts out. The CLI's own default server is chronicle://localhost:35000 with nothing else on it, and a development server serves a self-signed certificate, so every local connection starts failing with "The SSL connection could not be established" - 108 of the 161 integration specs. That is a breaking change for the most common way the CLI is used and wants deciding on its own terms rather than arriving inside a Screenplay bump. 16.19.3 is the last version with the old default. --- Directory.Packages.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f4b5c94..b38ba10 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,8 +6,8 @@ - - + + @@ -47,7 +47,7 @@ - +