From 7a1ecb0bc5ffc3bcdde073d7dd60f682a414a057 Mon Sep 17 00:00:00 2001 From: Einar Date: Thu, 23 Jul 2026 17:40:30 +0200 Subject: [PATCH 1/2] Align the invoicing sample with the documented language idioms The sample taught several forms the language definitions reject or deprecate: a redundant projection-level automap (AutoMap is the default), a named sequence its own events never reach, all where every is the correct construct for events the projection subscribes to, a redundant literal keyword on a quoted string, the undocumented set-to alternative to =, an arithmetic mapping referencing a property its source event does not carry, and events used without a declaration or import. The all block moves to a new SystemActivity slice mirroring its documented system-wide use case. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../for_ScreenplayCompiler/invoicing.play | 26 ++++++++++++++----- .../when_compiling_the_invoicing_sample.cs | 6 ++--- .../screenplay-editor/samples/invoicing.play | 26 ++++++++++++++----- 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/Source/DotNET/Screenplay/for_ScreenplayCompiler/invoicing.play b/Source/DotNET/Screenplay/for_ScreenplayCompiler/invoicing.play index 1e5627f..a8a052d 100644 --- a/Source/DotNET/Screenplay/for_ScreenplayCompiler/invoicing.play +++ b/Source/DotNET/Screenplay/for_ScreenplayCompiler/invoicing.play @@ -11,6 +11,9 @@ domain Sales import Customers.CustomerRegistered +import Customers.CustomerAccountClosed +import Shipping.InvoiceShipped +import Shipping.InvoiceShippingCleared concept InvoiceId : Uuid concept CustomerId : Uuid @@ -554,8 +557,7 @@ module Invoicing authorize IsAuthenticated projection InvoiceList => InvoiceListReadModel - sequence invoices - all + every lastActivityAt = $eventContext.occurred from InvoiceRegistered key invoiceId id = $eventSourceId @@ -565,7 +567,7 @@ module Invoicing status = "draft" lineCount = 0 dueDate = dueDate - channel = literal "portal" + channel = "portal" from InvoiceSent, InvoicePaid touched = true from InvoiceMarkedOverdue @@ -609,7 +611,7 @@ module Invoicing registeredForSubject = $causedBy.subject displayLabel = `${invoiceNumber} (${currency})` from InvoiceSent - set status to "sent" + status = "sent" sentAt = sentAt from InvoicePaid status = "paid" @@ -665,7 +667,6 @@ module Invoicing authorize IsAccountant projection InvoiceLineReport => InvoiceLineReportReadModel - automap from InvoiceLineItemAdded key InvoiceLineKey invoiceId = invoiceId @@ -696,11 +697,11 @@ module Invoicing from InvoicePaid decrement sentCount increment paidCount - add paidTotal by amount from InvoiceMarkedOverdue increment overdueCount from InvoiceWrittenOff - subtract paidTotal by amount + add writeOffTotal by amount + subtract outstandingTotal by amount from InvoiceCancelled decrement totalCount @@ -754,6 +755,17 @@ module Invoicing label $strings.invoices.actions.newInvoice navigate to RegisterInvoiceScreen + slice StateView SystemActivity + description "Counts every event in the system for the activity ticker" + + query GetSystemActivity => SystemActivityReadModel + authorize IsAccountant + + projection SystemActivity => SystemActivityReadModel + all + count totalEvents + lastOccurred = $eventContext.occurred + slice Automation NotifyCustomerOnInvoiceRegistered reactor NotifyCustomer diff --git a/Source/DotNET/Screenplay/for_ScreenplayCompiler/when_compiling_the_invoicing_sample.cs b/Source/DotNET/Screenplay/for_ScreenplayCompiler/when_compiling_the_invoicing_sample.cs index 13f2c99..e6175f1 100644 --- a/Source/DotNET/Screenplay/for_ScreenplayCompiler/when_compiling_the_invoicing_sample.cs +++ b/Source/DotNET/Screenplay/for_ScreenplayCompiler/when_compiling_the_invoicing_sample.cs @@ -22,7 +22,8 @@ void Because() [Fact] void should_succeed() => _result.Success.ShouldBeTrue(); [Fact] void should_have_no_diagnostics() => _result.Diagnostics.ShouldBeEmpty(); [Fact] void should_have_the_domain() => _result.Value!.Domain!.Name.ShouldEqual("Sales"); - [Fact] void should_have_the_import() => _result.Value!.Imports.Single().Name.ShouldEqual("CustomerRegistered"); + [Fact] void should_have_all_imports() => _result.Value!.Imports.Count().ShouldEqual(4); + [Fact] void should_have_the_customer_import() => _result.Value!.Imports.Select(_ => _.Name).ShouldContain("CustomerRegistered"); [Fact] void should_have_all_concepts() => _result.Value!.Concepts.Count().ShouldEqual(12); [Fact] void should_have_the_enum_concept_values() => _result.Value!.Concepts.Single(_ => _.Name == "InvoiceStatus").Values.Count().ShouldEqual(5); [Fact] void should_capture_pii_attributes() => _result.Value!.Concepts.Single(_ => _.Name == "EmailAddress").Attributes.ShouldContain("pii"); @@ -44,7 +45,7 @@ void Because() [Fact] void should_have_the_command_description() => RegisterCommand.Description.ShouldEqual("Registers a new invoice with its lines and payment terms"); [Fact] void should_have_both_layouts() => _result.Value!.Modules.Single().Layouts.Count().ShouldEqual(2); [Fact] void should_have_the_master_detail_slots() => _result.Value!.Modules.Single().Layouts.First().Slots.ShouldContainOnly("sidebar", "main"); - [Fact] void should_have_all_slices() => _feature.Slices.Count().ShouldEqual(12); + [Fact] void should_have_all_slices() => _feature.Slices.Count().ShouldEqual(13); [Fact] void should_have_the_nested_feature() => _feature.Features.Single().Name.ShouldEqual("Adjustments"); [Fact] void should_have_the_nested_feature_slices() => _feature.Features.Single().Slices.Select(_ => _.Name).ShouldContainOnly("ApplyDiscount", "WriteOffInvoice"); [Fact] void should_parse_conditional_produces() => RegisterCommand.Produces.Count(_ => _.When is not null).ShouldEqual(4); @@ -88,7 +89,6 @@ void Because() [Fact] void should_parse_the_details_projection_every() => Slice("InvoiceDetails").Projection!.Blocks.OfType().Single().IncludeChildren.ShouldBeFalse(); [Fact] void should_parse_the_details_projection_remove_via_join() => Slice("InvoiceDetails").Projection!.Blocks.OfType().Single().Event.ShouldEqual("CustomerAccountClosed"); [Fact] void should_parse_the_details_projection_nested() => Slice("InvoiceDetails").Projection!.Blocks.OfType().Single().Property.ShouldEqual("shipping"); - [Fact] void should_parse_the_list_projection_sequence() => Slice("InvoiceList").Projection!.Sequence.ShouldEqual("invoices"); [Fact] void should_parse_the_line_report_composite_key() => ((CompositeKeySyntax)Slice("InvoiceLineReport").Projection!.Blocks.OfType().Single().Key!).Type.ShouldEqual("InvoiceLineKey"); [Fact] void should_parse_the_summary_counters() => Slice("InvoiceDashboard").Projection!.Blocks.OfType().First().Mappings.OfType().Count().ShouldEqual(2); [Fact] void should_parse_the_dashboard_screen_layout() => Slice("InvoiceDashboard").Screens.Single().Directives.OfType().Single().Slots.Count().ShouldEqual(4); diff --git a/Source/Screenplay/Monaco/screenplay-editor/samples/invoicing.play b/Source/Screenplay/Monaco/screenplay-editor/samples/invoicing.play index 1e5627f..a8a052d 100644 --- a/Source/Screenplay/Monaco/screenplay-editor/samples/invoicing.play +++ b/Source/Screenplay/Monaco/screenplay-editor/samples/invoicing.play @@ -11,6 +11,9 @@ domain Sales import Customers.CustomerRegistered +import Customers.CustomerAccountClosed +import Shipping.InvoiceShipped +import Shipping.InvoiceShippingCleared concept InvoiceId : Uuid concept CustomerId : Uuid @@ -554,8 +557,7 @@ module Invoicing authorize IsAuthenticated projection InvoiceList => InvoiceListReadModel - sequence invoices - all + every lastActivityAt = $eventContext.occurred from InvoiceRegistered key invoiceId id = $eventSourceId @@ -565,7 +567,7 @@ module Invoicing status = "draft" lineCount = 0 dueDate = dueDate - channel = literal "portal" + channel = "portal" from InvoiceSent, InvoicePaid touched = true from InvoiceMarkedOverdue @@ -609,7 +611,7 @@ module Invoicing registeredForSubject = $causedBy.subject displayLabel = `${invoiceNumber} (${currency})` from InvoiceSent - set status to "sent" + status = "sent" sentAt = sentAt from InvoicePaid status = "paid" @@ -665,7 +667,6 @@ module Invoicing authorize IsAccountant projection InvoiceLineReport => InvoiceLineReportReadModel - automap from InvoiceLineItemAdded key InvoiceLineKey invoiceId = invoiceId @@ -696,11 +697,11 @@ module Invoicing from InvoicePaid decrement sentCount increment paidCount - add paidTotal by amount from InvoiceMarkedOverdue increment overdueCount from InvoiceWrittenOff - subtract paidTotal by amount + add writeOffTotal by amount + subtract outstandingTotal by amount from InvoiceCancelled decrement totalCount @@ -754,6 +755,17 @@ module Invoicing label $strings.invoices.actions.newInvoice navigate to RegisterInvoiceScreen + slice StateView SystemActivity + description "Counts every event in the system for the activity ticker" + + query GetSystemActivity => SystemActivityReadModel + authorize IsAccountant + + projection SystemActivity => SystemActivityReadModel + all + count totalEvents + lastOccurred = $eventContext.occurred + slice Automation NotifyCustomerOnInvoiceRegistered reactor NotifyCustomer From d9bba626a3cabca6e22d2e16e610bfbee1a372a8 Mon Sep 17 00:00:00 2001 From: Einar Date: Thu, 23 Jul 2026 17:40:40 +0200 Subject: [PATCH 2/2] Correct auto-map and grammar docs to match the projection parser The auto-map page showed an event-level no automap the parser rejects and join/children examples using an events line and an id shorthand that do not exist - joins take with blocks and children require identified by. The grammar page contradicted the parser in the other direction by denying the per-with automap toggle the parser supports, and its closing example used the nonexistent events line. Also add the sequence directive to the projection EBNF. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../screenplay/projections/auto-map.mdx | 56 ++++++++++--------- .../screenplay/projections/grammar.md | 13 +++-- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/Documentation/screenplay/projections/auto-map.mdx b/Documentation/screenplay/projections/auto-map.mdx index 62c6506..7b0426e 100644 --- a/Documentation/screenplay/projections/auto-map.mdx +++ b/Documentation/screenplay/projections/auto-map.mdx @@ -35,31 +35,31 @@ projection User => UserReadModel All properties must be mapped explicitly. -### Event-Level Disable +### Every- and All-Level Disable -Disable AutoMap for a specific event: +Disable AutoMap inside an `every` or `all` block: ```pdl projection User => UserReadModel - from UserRegistered + every no automap - Name = name - Email = email - - from UserUpdated - # AutoMap still enabled here - UpdatedAt = $eventContext.occurred + LastUpdated = $eventContext.occurred ``` +AutoMap cannot be toggled inside an individual `from` block — it is controlled at the projection, children, or nested level. + ### Join-Level Disable -Disable AutoMap within join blocks: +Disable AutoMap within a join's `with` blocks: ```pdl join Group on GroupId - events GroupCreated, GroupRenamed - no automap - Name = name + with GroupCreated + no automap + Name = name + with GroupRenamed + no automap + Name = name ``` ### Children-Level Disable @@ -67,7 +67,7 @@ join Group on GroupId Disable AutoMap for children collections: ```pdl -children members id userId +children members identified by userId no automap from UserAddedToGroup @@ -107,12 +107,14 @@ With default AutoMap, `Name`, `Email`, and `Age` are automatically copied. `IsAc ## Scope and Precedence 1. **Projection-level** `no automap` disables AutoMap for all events -2. **Event-level** `no automap` applies only to that specific event +2. **Every/All-level** `no automap` applies within that `every` or `all` block 3. **Explicit mappings** always override AutoMap values when both exist -4. **Join-level** `no automap` applies to joined events +4. **Join-level** `no automap` applies within the individual `with` block 5. **Children-level** `no automap` applies to child events 6. **Default is Enabled** - AutoMap works unless explicitly disabled +AutoMap cannot be toggled inside an individual `from` block. + ## Examples ### Default AutoMap @@ -145,11 +147,12 @@ projection User => UserReadModel # AutoMap enabled (default) IsActive = true - from UserProfileUpdated - no automap - # Must map everything explicitly - Name = fullName - Email = emailAddress + join Profile on ProfileId + with UserProfileUpdated + no automap + # Must map everything explicitly + Name = fullName + Email = emailAddress ``` ### Projection-Level Disable with Event-Level Override @@ -176,9 +179,12 @@ projection Order => OrderReadModel # AutoMap enabled join Customer on CustomerId - events CustomerCreated, CustomerUpdated - no automap - CustomerName = name + with CustomerCreated + no automap + CustomerName = name + with CustomerUpdated + no automap + CustomerName = name ``` ### Children with Selective Disable @@ -188,7 +194,7 @@ projection Group => GroupReadModel from GroupCreated # AutoMap enabled - children members id userId + children members identified by userId no automap from UserAddedToGroup diff --git a/Documentation/screenplay/projections/grammar.md b/Documentation/screenplay/projections/grammar.md index 0140248..ebe509f 100644 --- a/Documentation/screenplay/projections/grammar.md +++ b/Documentation/screenplay/projections/grammar.md @@ -35,6 +35,7 @@ Projection = "projection", Ident, [ "=>", TypeRef ], NL, DEDENT ] ; ProjDirective = "no", "automap", NL + | "sequence", Ident, NL | KeyDecl | CompositeKeyDecl ; @@ -78,6 +79,7 @@ JoinBlock = "join", Ident, "on", Ident, NL, WithEventBlock = "with", TypeRef, NL, [ INDENT, + [ "automap" | "no", "automap", NL ], { MappingLine }, DEDENT ] ; @@ -275,11 +277,12 @@ JoinBlock = "join", Ident, "on", Ident, NL, WithEventBlock = "with", TypeRef, NL, [ INDENT, + [ "automap" | "no", "automap", NL ], { MappingLine }, DEDENT ] ; ``` -**Note:** AutoMap for join blocks is controlled at the projection or children level, not within individual with blocks. +**Note:** Each `with` block may toggle AutoMap for its own mappings with `automap` or `no automap`. ### Children Block @@ -461,8 +464,10 @@ projection Order => OrderReadModel ShippedAt = $eventContext.occurred join Customer on CustomerId - events CustomerCreated, CustomerUpdated - CustomerName = name + with CustomerCreated + CustomerName = name + with CustomerUpdated + CustomerName = name children items identified by lineNumber every @@ -484,7 +489,7 @@ This projection uses: - Projection declaration - Every block with exclude children at the projection level - Multiple from blocks with keys -- Join block with multiple events +- Join block with multiple with blocks - Children block with: - Child every block for common mappings across all child events - Nested from and remove blocks