From 72410005b11b785f59b62401b20503652070caad Mon Sep 17 00:00:00 2001 From: woksin Date: Tue, 13 Oct 2026 16:55:29 +0200 Subject: [PATCH 01/15] Bundle Screenplay 4.152.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5a627b2c..af1f3ae7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,7 +5,7 @@ 22.54.0 0.24.0 0.18.0 - 4.147.1 + 4.152.0 4.92.0 From 32c85cda758e6c3c76a9388ef43dc71c8f27c02c Mon Sep 17 00:00:00 2001 From: woksin Date: Tue, 13 Oct 2026 20:59:14 +0200 Subject: [PATCH 02/15] Bundle Screenplay 4.154.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index af1f3ae7..58811df6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,7 +5,7 @@ 22.54.0 0.24.0 0.18.0 - 4.152.0 + 4.154.0 4.92.0 From eb4dc54be73eb933bf8e0511cabfd38786e4acca Mon Sep 17 00:00:00 2001 From: woksin Date: Tue, 13 Oct 2026 21:08:36 +0200 Subject: [PATCH 03/15] Expose Screenplay release baseline commands (#351) --- .../given/a_release_model.cs | 118 ++++++++++++++++++ .../when_admitting/with_a_release_label.cs | 13 ++ .../when_admitting/without_a_release_label.cs | 14 +++ .../when_checking/with_a_changed_revision.cs | 18 +++ .../with_an_unchanged_revision.cs | 14 +++ .../when_checking/with_an_unknown_option.cs | 11 ++ .../when_checking/with_json_output.cs | 14 +++ .../without_persisted_identities.cs | 12 ++ .../AdmitBaselineScreenplayCommand.cs | 25 ++++ .../AdmitBaselineScreenplaySettings.cs | 29 +++++ .../Screenplay/BaselineScreenplaySettings.cs | 22 ++++ .../CheckBaselineScreenplayCommand.cs | 23 ++++ .../Screenplay/ScreenplayReleaseBaseline.cs | 53 ++++++++ 13 files changed, 366 insertions(+) create mode 100644 Source/Cli.Specs/for_ScreenplayReleaseBaseline/given/a_release_model.cs create mode 100644 Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_admitting/with_a_release_label.cs create mode 100644 Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_admitting/without_a_release_label.cs create mode 100644 Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/with_a_changed_revision.cs create mode 100644 Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/with_an_unchanged_revision.cs create mode 100644 Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/with_an_unknown_option.cs create mode 100644 Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/with_json_output.cs create mode 100644 Source/Cli.Specs/for_ScreenplayReleaseBaseline/when_checking/without_persisted_identities.cs create mode 100644 Source/Cli/Commands/Screenplay/AdmitBaselineScreenplayCommand.cs create mode 100644 Source/Cli/Commands/Screenplay/AdmitBaselineScreenplaySettings.cs create mode 100644 Source/Cli/Commands/Screenplay/BaselineScreenplaySettings.cs create mode 100644 Source/Cli/Commands/Screenplay/CheckBaselineScreenplayCommand.cs create mode 100644 Source/Cli/Commands/Screenplay/ScreenplayReleaseBaseline.cs 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..762cdac5 --- /dev/null +++ b/Source/Cli.Specs/for_ScreenplayReleaseBaseline/given/a_release_model.cs @@ -0,0 +1,118 @@ +// 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"; + protected string _root; + protected string _document; + protected int _exitCode; + protected string _output; + protected string _error; + + void Establish() + { + _root = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), 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"; + 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); + + 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..37f689ec --- /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() => _error.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_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..6b612340 --- /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() => _error.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/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