From 3341cfd2dcfe86f5c30b35bca8f6a1ec5e813abf Mon Sep 17 00:00:00 2001 From: woksin Date: Thu, 27 Aug 2026 05:43:29 +0200 Subject: [PATCH 1/3] Adopt Generation bounded source evidence release --- Directory.Packages.props | 8 ++++---- Integration/PackageConsumer/Program.cs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index db5361d..57e9ece 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,10 +5,10 @@ - - - - + + + + diff --git a/Integration/PackageConsumer/Program.cs b/Integration/PackageConsumer/Program.cs index aef9702..03507fa 100644 --- a/Integration/PackageConsumer/Program.cs +++ b/Integration/PackageConsumer/Program.cs @@ -61,10 +61,10 @@ static void AssertDependencyGraph() using var document = JsonDocument.Parse(File.ReadAllText(dependencyFile)); var libraries = document.RootElement.GetProperty("libraries"); - AssertPackage(libraries, "Cratis.Screenplay.Generation.Contracts/0.13.2"); - AssertPackage(libraries, "Cratis.Screenplay.Generation/0.13.2"); - AssertPackage(libraries, "Cratis.Screenplay.Generation.DotNet/0.13.2"); - AssertPackage(libraries, "Cratis.Screenplay.Generation.DotNet.Vogen/0.13.2"); + AssertPackage(libraries, "Cratis.Screenplay.Generation.Contracts/0.15.0"); + AssertPackage(libraries, "Cratis.Screenplay.Generation/0.15.0"); + AssertPackage(libraries, "Cratis.Screenplay.Generation.DotNet/0.15.0"); + AssertPackage(libraries, "Cratis.Screenplay.Generation.DotNet.Vogen/0.15.0"); if (libraries.EnumerateObject().Any(_ => _.Name.StartsWith("Vogen/", StringComparison.Ordinal))) { From b8656a45502d49ca36c7fd66066465ede8ba0ddd Mon Sep 17 00:00:00 2001 From: woksin Date: Thu, 27 Aug 2026 09:59:43 +0200 Subject: [PATCH 2/3] Add bounded source migration characterizations --- .../when_analyzing_marten_documents.cs | 3 ++ ...rating_targeted_wolverine_event_streams.cs | 14 ++++++++- .../given/a_marten_document_application.cs | 20 +++++++++++- ...erine_targeted_event_stream_application.cs | 31 +++++++++++++++++++ 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_analyzing_marten_documents.cs b/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_analyzing_marten_documents.cs index 05793fc..f12cc33 100644 --- a/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_analyzing_marten_documents.cs +++ b/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayAdapter/when_analyzing_marten_documents.cs @@ -23,6 +23,9 @@ public class when_analyzing_marten_documents : given.a_marten_document_applicati [Fact] void should_not_invent_an_event_built_read_model() => _graph.Artifacts.Any(_ => _.Key.Kind == ArtifactKind.ReadModel).ShouldBeFalse(); [Fact] void should_report_each_ordinary_document_language_gap() => Contribution.Diagnostics.Count(_ => _.Code == MartenDiagnosticCodes.DocumentModelOmitted).ShouldEqual(3); [Fact] void should_diagnose_unresolved_and_ambiguous_identity_configurations_without_guessing() => Contribution.Diagnostics.Count(_ => _.Code == MartenDiagnosticCodes.DocumentIdentityUnresolved).ShouldEqual(2); + [Fact] void should_not_discover_a_document_from_a_generated_syntax_tree() => _graph.Artifacts.Any(_ => _.Key.Kind == ArtifactKind.Document && _.Variants.Single().Definition.Name == "GeneratedStudent").ShouldBeFalse(); + [Fact] void should_not_contribute_a_fact_for_a_generated_document() => Contribution.Facts.OfType().Any(_ => _.Definition.Name == "GeneratedStudent").ShouldBeFalse(); + [Fact] void should_not_report_a_document_model_omission_for_a_generated_document() => Contribution.Diagnostics.Any(_ => _.Code == MartenDiagnosticCodes.DocumentModelOmitted && _.Message.Contains("GeneratedStudent", StringComparison.Ordinal)).ShouldBeFalse(); IReadOnlyList DocumentEvidence => Artifact("Student").Variants.Single().Evidence; ArtifactDefinition Document => Artifact("Student").Variants.Single().Definition; diff --git a/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayGenerator/when_generating_targeted_wolverine_event_streams.cs b/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayGenerator/when_generating_targeted_wolverine_event_streams.cs index 8bc047b..f9f9019 100644 --- a/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayGenerator/when_generating_targeted_wolverine_event_streams.cs +++ b/Source/DotNET/CritterStack.Specs/for_CritterStackScreenplayGenerator/when_generating_targeted_wolverine_event_streams.cs @@ -70,8 +70,15 @@ [Fact] void should_make_conventional_version_diagnostics_parameter_specific() => [Fact] void should_not_guess_the_receiver_alias_event() => EventNames.ShouldNotContain("AliasedEvent"); [Fact] void should_not_guess_an_event_before_an_opaque_spread() => EventNames.ShouldNotContain("OpaqueLeadingEvent"); [Fact] void should_not_guess_object_dynamic_variable_or_helper_payloads() => EventNames.Any(_ => new[] { "OpaqueObjectEvent", "DynamicEvent", "VariableEvent", "HelperEvent", "NestedContainerEvent" }.Contains(_, StringComparer.Ordinal)).ShouldBeFalse(); - [Fact] void should_report_each_unresolved_exact_append_once() => Diagnostics(WolverineDiagnosticCodes.EventWriteTargetUnresolved).Count.ShouldEqual(6); + [Fact] void should_report_each_unresolved_exact_append_once() => Diagnostics(WolverineDiagnosticCodes.EventWriteTargetUnresolved).Count.ShouldEqual(9); [Fact] void should_locate_each_unresolved_append_in_authored_source() => Diagnostics(WolverineDiagnosticCodes.EventWriteTargetUnresolved).All(_ => _.Source?.Path == "Transfers/Handlers.cs").ShouldBeTrue(); + [Fact] void should_anchor_each_unresolved_append_at_its_authored_occurrence() => Diagnostics(WolverineDiagnosticCodes.EventWriteTargetUnresolved).All(_ => _.Source!.StartLine > 0 && _.Source.StartColumn > 0).ShouldBeTrue(); + [Fact] void should_preserve_each_unresolved_append_occurrence_without_collapsing_locations() => Diagnostics(WolverineDiagnosticCodes.EventWriteTargetUnresolved).Select(_ => (_.Source!.StartLine, _.Source.StartColumn)).Distinct().Count().ShouldEqual(9); + [Fact] void should_report_unresolved_appends_in_authored_declaration_order() => UnresolvedAppendStartLines.SequenceEqual(UnresolvedAppendStartLines.Order()).ShouldBeTrue(); + [Fact] void should_not_promote_a_member_receiver_append_to_an_event() => EventNames.ShouldNotContain("MemberReceiverEvent"); + [Fact] void should_not_promote_an_object_round_trip_receiver_append_to_an_event() => EventNames.ShouldNotContain("ObjectRoundTripEvent"); + [Fact] void should_not_flatten_a_nested_payload_container() => EventNames.ShouldNotContain("NestedContainerEvent"); + [Fact] void should_recognize_an_event_with_a_computed_constructor_value_by_type() => EventNames.ShouldContain("OrderConfirmed"); [Fact] void should_report_each_attributed_multiple_stream_binding_once() => Diagnostics(WolverineDiagnosticCodes.MultipleStreamMetadataOmitted).Count.ShouldEqual(6); [Fact] void should_locate_each_multiple_stream_metadata_loss() => Diagnostics(WolverineDiagnosticCodes.MultipleStreamMetadataOmitted).All(_ => _.Source?.Path == "Transfers/Handlers.cs").ShouldBeTrue(); [Fact] void should_report_version_and_concurrency_metadata_per_binding() => Diagnostics(WolverineDiagnosticCodes.StreamVersionOmitted).Count.ShouldEqual(7); @@ -136,4 +143,9 @@ bool HasArtifactNamed(params string[] names) => _result.Graph.Artifacts.Any(_ => _.Variants.Any(variant => names.Contains(variant.Definition.Name, StringComparer.Ordinal))); IReadOnlyList Diagnostics(string code) => [.. _result.Diagnostics.Where(_ => _.Code == code)]; + + IReadOnlyList UnresolvedAppendStartLines => + [ + .. Diagnostics(WolverineDiagnosticCodes.EventWriteTargetUnresolved).Select(_ => _.Source!.StartLine) + ]; } diff --git a/Source/DotNET/CritterStack.Specs/given/a_marten_document_application.cs b/Source/DotNET/CritterStack.Specs/given/a_marten_document_application.cs index 65c1ab5..a4a6e1d 100644 --- a/Source/DotNET/CritterStack.Specs/given/a_marten_document_application.cs +++ b/Source/DotNET/CritterStack.Specs/given/a_marten_document_application.cs @@ -85,6 +85,23 @@ public static void Configure(Marten.StoreOptions options) } """; + const string GeneratedApplicationSource = + """ + // + namespace Students; + + public class GeneratedStudent + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + } + + public static class GeneratedStudentEndpoints + { + public static void Store(GeneratedStudent student, Marten.IDocumentSession session) => session.Store(student); + } + """; + static readonly IReadOnlyList _references = [ .. ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!) @@ -100,7 +117,8 @@ void Establish() "Students", [ CSharpSyntaxTree.ParseText(FrameworkSource, path: "/workspace/Framework.cs"), - CSharpSyntaxTree.ParseText(ApplicationSource, path: "/workspace/Students/Students.cs") + CSharpSyntaxTree.ParseText(ApplicationSource, path: "/workspace/Students/Students.cs"), + CSharpSyntaxTree.ParseText(GeneratedApplicationSource, path: "/workspace/Students/GeneratedStudent.g.cs") ], _references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable)); diff --git a/Source/DotNET/CritterStack.Specs/given/a_wolverine_targeted_event_stream_application.cs b/Source/DotNET/CritterStack.Specs/given/a_wolverine_targeted_event_stream_application.cs index b29467b..88659ee 100644 --- a/Source/DotNET/CritterStack.Specs/given/a_wolverine_targeted_event_stream_application.cs +++ b/Source/DotNET/CritterStack.Specs/given/a_wolverine_targeted_event_stream_application.cs @@ -119,6 +119,9 @@ public record InspectAccount(System.Guid AccountId); public record ConventionalVersionCommand(System.Guid FromId, System.Guid ToId, long Version); public record FalseIdentityCommand([property: Transfers.Identity] System.Guid Candidate); public record LegacyAppend(System.Guid AccountId); + public record MemberReceiverAppend(System.Guid Id); + public record ObjectRoundTripAppend(System.Guid Id); + public record ObjectPayloadAppend(System.Guid Id); public class Account { @@ -165,6 +168,8 @@ public record SagaSiblingAppended(System.Guid Id); public record RouteFollowUp(System.Guid Id); public record RouteAppended(System.Guid Id); public record LegacyAppended(System.Guid Id); + public record MemberReceiverEvent(System.Guid Id); + public record ObjectRoundTripEvent(System.Guid Id); public sealed class BoundaryResponse : Wolverine.IResponseAware; public sealed class BoundaryEffect : Wolverine.ISideEffect; @@ -176,6 +181,11 @@ public partial class GeneratedBaseSaga; public interface IAccountEventStream : JasperFx.Events.IEventStream; + public sealed class MemberReceiverStream + { + public JasperFx.Events.IEventStream Inner { get; set; } = null!; + } + public sealed class UnrelatedStream { public void AppendOne(object @event) { } @@ -403,6 +413,27 @@ public static class SagaMixedAppendHandler public static void Handle(SagaMixedAppend command, JasperFx.Events.IEventStream stream) => stream.AppendMany(new TransferSaga(), new SagaSiblingAppended(command.Id)); } + + public static class MemberReceiverAppendHandler + { + public static void Handle(MemberReceiverAppend command, MemberReceiverStream stream) => + stream.Inner.AppendOne(new MemberReceiverEvent(command.Id)); + } + + public static class ObjectRoundTripAppendHandler + { + public static void Handle(ObjectRoundTripAppend command, JasperFx.Events.IEventStream stream) + { + object boxed = stream; + ((JasperFx.Events.IEventStream)boxed).AppendOne(new ObjectRoundTripEvent(command.Id)); + } + } + + public static class ObjectPayloadAppendHandler + { + public static void Handle(ObjectPayloadAppend command, JasperFx.Events.IEventStream stream) => + stream.AppendOne(new object()); + } """; const string GeneratedApplicationSource = From 9e15e24108be905e4634765b25423e11e99cbf76 Mon Sep 17 00:00:00 2001 From: woksin Date: Thu, 27 Aug 2026 11:01:40 +0200 Subject: [PATCH 3/3] Migrate readers to shared source evidence helpers --- .../Marten/MartenDocumentFacts.cs | 4 +-- .../Wolverine/WolverineEventStreams.cs | 30 +++++++++++-------- .../Wolverine/WolverineMethodSyntax.cs | 5 ++-- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/Source/DotNET/CritterStack/Marten/MartenDocumentFacts.cs b/Source/DotNET/CritterStack/Marten/MartenDocumentFacts.cs index 1a05de7..a0d2810 100644 --- a/Source/DotNET/CritterStack/Marten/MartenDocumentFacts.cs +++ b/Source/DotNET/CritterStack/Marten/MartenDocumentFacts.cs @@ -32,9 +32,9 @@ public static MartenDiscoveryResult Discover( foreach (var tree in project.AuthoredSyntaxTrees.Where(_ => !DotNetGeneratedSource.IsGenerated(_))) { var semanticModel = project.Compilation.GetSemanticModel(tree); - foreach (var invocation in tree.GetRoot().DescendantNodes().OfType()) + foreach (var invocation in DotNetSource.AuthoredInvocationsIn(tree.GetRoot(), project)) { - if (semanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol method || !IsMarten(method)) + if (DotNetInvocations.MethodFor(invocation, semanticModel) is not { } method || !IsMarten(method)) { continue; } diff --git a/Source/DotNET/CritterStack/Wolverine/WolverineEventStreams.cs b/Source/DotNET/CritterStack/Wolverine/WolverineEventStreams.cs index 6e5363d..b0fa116 100644 --- a/Source/DotNET/CritterStack/Wolverine/WolverineEventStreams.cs +++ b/Source/DotNET/CritterStack/Wolverine/WolverineEventStreams.cs @@ -124,9 +124,10 @@ public static WolverineEventStreamAppendDiscovery Appends( foreach (var (declaration, semanticModel) in WolverineMethodSyntax.Declarations(method, project)) { - foreach (var invocationSyntax in declaration.DescendantNodes().OfType()) + foreach (var invocationSyntax in DotNetSource.AuthoredInvocationsIn(declaration, project)) { if (!IsInDirectHandlerBody(invocationSyntax, declaration) || + DotNetInvocations.MethodFor(invocationSyntax, semanticModel) is not { } invocationMethod || semanticModel.GetOperation(invocationSyntax) is not IInvocationOperation invocation || !TryGetAppendModel(invocation, project, out var invokedModel)) { @@ -135,7 +136,9 @@ public static WolverineEventStreamAppendDiscovery Appends( hasDirectWrite = true; var source = CritterStackSource.RangeForProject(invocationSyntax.GetLocation(), project); - if (ReceiverParameter(invocation.Instance) is not { } receiver || + if (DotNetInvocations.ReceiverFor(invocationSyntax, invocationMethod, semanticModel) is not { } receiverExpression || + (semanticModel.GetOperation(receiverExpression) ?? invocation.Instance) is not { } receiverOperation || + ReceiverParameter(receiverOperation) is not { } receiver || bindings.FirstOrDefault(binding => SymbolEqualityComparer.Default.Equals(binding.Parameter, receiver) && SymbolEqualityComparer.Default.Equals(binding.ModelType, invokedModel)) is not { } binding) @@ -205,20 +208,13 @@ static bool TryGetAppendModel( } if (!SymbolEqualityComparer.Default.Equals(method.ContainingType.OriginalDefinition, streamDefinition) || - method.ContainingType.TypeArguments.SingleOrDefault() is not INamedTypeSymbol model || - !streamDefinition.GetMembers(method.Name).OfType().Any(candidate => - SymbolEqualityComparer.Default.Equals(candidate.OriginalDefinition, method.OriginalDefinition)) || - method.Parameters.Length != 1) + method.ContainingType.TypeArguments.SingleOrDefault() is not INamedTypeSymbol model) { return false; } - var isExactAppend = method.Name switch - { - "AppendOne" => method.Parameters[0].Type.SpecialType == SpecialType.System_Object, - "AppendMany" => IsObjectArray(method.Parameters[0].Type) || IsObjectEnumerable(method.Parameters[0].Type), - _ => false - }; + var isExactAppend = ExpectedAppendSignatures(streamDefinition) + .Any(signature => DotNetMethodSignatures.Matches(method, signature)); if (isExactAppend) { modelType = model; @@ -227,6 +223,16 @@ static bool TryGetAppendModel( return isExactAppend; } + static IEnumerable ExpectedAppendSignatures(INamedTypeSymbol streamDefinition) => + streamDefinition.GetMembers("AppendOne") + .OfType() + .Where(candidate => candidate.Parameters is [{ Type.SpecialType: SpecialType.System_Object }]) + .Concat(streamDefinition.GetMembers("AppendMany") + .OfType() + .Where(candidate => candidate.Parameters is [var parameter] && + (IsObjectArray(parameter.Type) || IsObjectEnumerable(parameter.Type)))) + .Select(DotNetMethodSignatures.From); + static IReadOnlyList EventStreamInterfaces(ITypeSymbol type) { if (type is not INamedTypeSymbol named) diff --git a/Source/DotNET/CritterStack/Wolverine/WolverineMethodSyntax.cs b/Source/DotNET/CritterStack/Wolverine/WolverineMethodSyntax.cs index 9763f98..0f0e641 100644 --- a/Source/DotNET/CritterStack/Wolverine/WolverineMethodSyntax.cs +++ b/Source/DotNET/CritterStack/Wolverine/WolverineMethodSyntax.cs @@ -13,10 +13,11 @@ static class WolverineMethodSyntax IMethodSymbol method, DotNetProjectCompilation project) { + var authoredDeclarations = DotNetSource.AuthoredDeclarationsOf(method, project.AuthoredSyntaxTrees).ToHashSet(); foreach (var syntaxReference in method.DeclaringSyntaxReferences) { - if (syntaxReference.GetSyntax() is MethodDeclarationSyntax declaration && - project.AuthoredSyntaxTrees.Contains(declaration.SyntaxTree) && + if (authoredDeclarations.Contains(syntaxReference) && + syntaxReference.GetSyntax() is MethodDeclarationSyntax declaration && !DotNetGeneratedSource.IsGenerated(declaration.SyntaxTree)) { yield return (declaration, project.Compilation.GetSemanticModel(declaration.SyntaxTree));