@cratis/pi 2.23.0 → 2.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/package/corpus/harnesses/pi/extensions/cratis-rules/index.ts +233 -10
  2. package/package/corpus/prompts/ship-changes.prompt.md +4 -0
  3. package/package/corpus/rules/ai-distribution.md +3 -1
  4. package/package/corpus/rules/application-profile.md +73 -0
  5. package/package/corpus/rules/general.md +5 -68
  6. package/package/corpus/rules/guards-and-fuses.md +24 -10
  7. package/package/corpus/rules/local-work-artifacts.md +6 -0
  8. package/package/corpus/rules/profiles.md +24 -1
  9. package/package/corpus/rules/rtk.md +13 -3
  10. package/package/corpus/rules/verification-discipline.md +7 -1
  11. package/package/corpus/skills/cratis-chronicle-event-modeling/SKILL.md +8 -0
  12. package/package/corpus/skills/cratis-event-model-diagram/SKILL.md +7 -0
  13. package/package/corpus/skills/cratis-screenplay-captures-and-reactions/LICENSE +2 -0
  14. package/package/corpus/skills/cratis-screenplay-captures-and-reactions/SKILL.md +229 -0
  15. package/package/corpus/skills/cratis-screenplay-captures-and-reactions/verification.json +10 -0
  16. package/package/corpus/skills/cratis-screenplay-command-surface/LICENSE +2 -0
  17. package/package/corpus/skills/cratis-screenplay-command-surface/SKILL.md +270 -0
  18. package/package/corpus/skills/cratis-screenplay-command-surface/references/context.md +111 -0
  19. package/package/corpus/skills/cratis-screenplay-command-surface/verification.json +23 -0
  20. package/package/corpus/skills/cratis-screenplay-event-modeling/LICENSE +2 -0
  21. package/package/corpus/skills/cratis-screenplay-event-modeling/SKILL.md +204 -0
  22. package/package/corpus/skills/cratis-screenplay-event-modeling/references/nine-steps.md +219 -0
  23. package/package/corpus/skills/cratis-screenplay-event-modeling/verification.json +23 -0
  24. package/package/corpus/skills/cratis-screenplay-model-authoring/SKILL.md +13 -1
  25. package/package/corpus/skills/cratis-screenplay-projections/LICENSE +2 -0
  26. package/package/corpus/skills/cratis-screenplay-projections/SKILL.md +215 -0
  27. package/package/corpus/skills/cratis-screenplay-projections/references/pdl-grammar.md +239 -0
  28. package/package/corpus/skills/cratis-screenplay-projections/verification.json +11 -0
  29. package/package/corpus/skills/cratis-screenplay-read-surface/LICENSE +2 -0
  30. package/package/corpus/skills/cratis-screenplay-read-surface/SKILL.md +188 -0
  31. package/package/corpus/skills/cratis-screenplay-read-surface/verification.json +10 -0
  32. package/package/corpus/skills/cratis-screenplay-specifications/LICENSE +2 -0
  33. package/package/corpus/skills/cratis-screenplay-specifications/SKILL.md +199 -0
  34. package/package/corpus/skills/cratis-screenplay-specifications/verification.json +19 -0
  35. package/package/corpus/skills/cratis-screenplay-ui-composition/LICENSE +2 -0
  36. package/package/corpus/skills/cratis-screenplay-ui-composition/SKILL.md +231 -0
  37. package/package/corpus/skills/cratis-screenplay-ui-composition/verification.json +10 -0
  38. package/package/corpus/skills/cratis-stage-rendering-and-sandbox/SKILL.md +5 -2
  39. package/package/profile-catalog.json +8 -1
  40. package/package.json +1 -1
  41. package/package/corpus/rules/terminal-commands.md +0 -19
@@ -0,0 +1,215 @@
1
+ ---
2
+ name: cratis-screenplay-projections
3
+ description: Build a Cratis Screenplay read model with the Projection Declaration Language (PDL) — keys, `from`/`every`/`all`, property mapping and AutoMap, joins, children and nested objects, removal, arithmetic counters, and the reducer escape hatch. Use when declaring or changing how events become read-model state in a `.play` model, or when a projection does not populate what was expected. Use `cratis-chronicle-projection` instead for hand-written C# Chronicle projections.
4
+ license: MIT
5
+ ---
6
+
7
+ # Projections — the Screenplay PDL
8
+
9
+ A `projection` folds events into a read model. Its body is the **Projection
10
+ Declaration Language**, an embedded sub-grammar with its own parser — not free
11
+ text. This is half of every event model, and the half most easily got wrong.
12
+
13
+ ## Verified product sources
14
+
15
+ | Package | Version | Purpose |
16
+ | --- | --- | --- |
17
+ | `Cratis.Screenplay` | `4.12.1` | PDL parser, validator, diagnostics |
18
+
19
+ Read from the Screenplay repository at tag `v4.12.1` (commit `122eee8`), against
20
+ `Documentation/screenplay/projections/` and
21
+ `Source/DotNET/Screenplay/Parsing/ProjectionParser.cs`. Reverify before claiming
22
+ another version behaves the same.
23
+
24
+ ## The shape
25
+
26
+ ```screenplay
27
+ readmodel InvoiceDetailsReadModel
28
+ invoiceNumber InvoiceNumber
29
+ status InvoiceStatus
30
+ lastUpdatedAt DateTime
31
+
32
+ projection InvoiceDetails => InvoiceDetailsReadModel
33
+ key invoiceId
34
+ every
35
+ lastUpdatedAt = $eventContext.occurred
36
+ exclude children
37
+ from InvoiceRegistered key invoiceId
38
+ status = "draft"
39
+ from InvoiceSent
40
+ status = "sent"
41
+ remove with InvoiceCancelled
42
+ ```
43
+
44
+ **The arrow always points the same way.** A read model never declares what builds
45
+ it; the projection or reducer points at it with `=>`. **Exactly one thing may
46
+ build a read model** — two builders is error `PLAY0191`: *a projection or a
47
+ reducer builds it, and only one of them*. A slice may still declare several
48
+ projections, each building a **different** read model.
49
+
50
+ ## Projection-level directives
51
+
52
+ `key <expr>`, `no automap`, `sequence <name>`, `file <path>`. A second `key` is
53
+ error `PLAY0059`.
54
+
55
+ ## Keys — eight ways, and the default
56
+
57
+ | Form | Example |
58
+ | --- | --- |
59
+ | Projection-level | `key invoiceId` |
60
+ | From-level inline | `from InvoiceRegistered key invoiceId` |
61
+ | From-level block | indented `key invoiceId` inside the `from` |
62
+ | Per event, several events | `from A key idA, B key idB, C` |
63
+ | Composite | `key OrderKey` with indented `<property> = <expr>` parts |
64
+ | Literal (constant) | `key literal "site-stats"` — every event updates the **same** instance |
65
+ | Children identity | `children lines identified by lineNumber` |
66
+ | **Default** | **the event source id** — `from X` ≡ `from X key $eventSourceId` |
67
+
68
+ A second key on one `from` is error `PLAY0060`. A template expression in a
69
+ composite key is error `PLAY0073`; an empty composite key is `PLAY0074`.
70
+
71
+ ## `from` vs `every` vs `all`
72
+
73
+ | Block | Fires for | Use for |
74
+ | --- | --- | --- |
75
+ | `from <Event>` | that event type only | the actual mapping work |
76
+ | `every` | **only** the types this projection subscribes to via `from` | common fields across your own events |
77
+ | `all` | **every event type in the system** | system-wide audit logs, global counters |
78
+
79
+ ⚠️ `all` and `every` are not synonyms, and confusing them is the classic PDL
80
+ mistake. `every` is scoped to your `from` blocks; `all` is not scoped at all.
81
+
82
+ `every` accepts `exclude children`, so child events do not bump the parent's
83
+ `lastUpdatedAt`. The `every` block **inside** a `children` block does not accept
84
+ it — it is already in a children context.
85
+
86
+ ## Property mapping and AutoMap
87
+
88
+ ⚠️ **AutoMap is on by default.** Matching property names are copied before your
89
+ explicit mappings run, and explicit mappings win. Turn it off with `no automap` at
90
+ **projection**, `every`/`all`, `join … with`, `children` or `nested` level — it
91
+ **cannot** be toggled inside an individual `from` block.
92
+
93
+ Mapping sources: a property path (`name`, `contactInfo.email`), a literal
94
+ (`true`, `"Pending"`, `42`, `null`), a template (`` `${first} ${last}` ``),
95
+ `$eventSourceId`, `$eventContext.<occurred|sequenceNumber|correlationId|eventSourceId>`,
96
+ and `$causedBy.<subject|name|userName>` (an unknown one is an error naming all
97
+ three).
98
+
99
+ `clear <property>` removes a value and is equivalent to `= null`; prefer `clear`.
100
+ It takes a dotted path (`clear Owner.Note`) and escapes reserved names
101
+ (`clear @with`).
102
+
103
+ ## Arithmetic
104
+
105
+ | Line | Effect |
106
+ | --- | --- |
107
+ | `count <prop>` | +1, expressing *counting occurrences* |
108
+ | `increment <prop>` | +1, expressing *modifying state* |
109
+ | `decrement <prop>` | −1 |
110
+ | `add <prop> by <expr>` | += expression |
111
+ | `subtract <prop> by <expr>` | −= expression |
112
+
113
+ `count` and `increment` behave identically; the difference is intent. The target
114
+ must be numeric.
115
+
116
+ ## Joins
117
+
118
+ ```screenplay
119
+ join Customer on CustomerId
120
+ with CustomerCreated
121
+ no automap
122
+ CustomerName = name
123
+ with CustomerUpdated
124
+ CustomerName = name
125
+ ```
126
+
127
+ `join <property> on <key>` (error `PLAY0063` otherwise), then one or more
128
+ `with <EventType>` blocks (error `PLAY0064` otherwise), each able to toggle
129
+ `automap` / `no automap` for itself. Joins work at projection level and inside
130
+ `children`. A join cannot declare its own key or trigger removal — that is
131
+ `remove via join on`.
132
+
133
+ ## Children and nested
134
+
135
+ ```screenplay
136
+ children lineItems identified by lineNumber
137
+ from InvoiceLineItemAdded key lineNumber
138
+ parent invoiceId
139
+ quantity = quantity
140
+ remove with InvoiceLineItemRemoved key lineNumber
141
+ parent invoiceId
142
+
143
+ nested billingContact
144
+ from BillingContactSet
145
+ email = email
146
+ clear with BillingContactCleared
147
+ ```
148
+
149
+ - **`children <collection> identified by <expr>`** — a collection with its own
150
+ lifecycle. `parent <expr>` ties a child to its parent instance; it accepts an
151
+ event property, `$eventContext.eventSourceId`, or a nested path. Children nest
152
+ arbitrarily deep and may contain `join`, `remove`, `nested` and `every`.
153
+ - **`nested <property>`** — a single nullable object. It **must** contain at least
154
+ one `from` (error `PLAY0067`), and `clear with <Event>` drops the whole object
155
+ back to null. To clear one property instead, use a `clear <property>` mapping.
156
+
157
+ ## Removal
158
+
159
+ ```screenplay
160
+ remove with InvoiceCancelled key invoiceId
161
+ remove via join on CustomerAccountClosed
162
+ ```
163
+
164
+ `remove with <Event> [key <expr>]` removes the instance the event identifies.
165
+ `remove via join on <Event> [key <expr>]` removes instances reached through a
166
+ join. Inside `children`, both take an indented `parent <expr>` — and **only**
167
+ `parent`; anything else is error `PLAY0069`. Several removal conditions may
168
+ coexist.
169
+
170
+ ## When PDL is not enough — the reducer
171
+
172
+ ```screenplay
173
+ reducer Balance => AccountBalance
174
+ on AmountDeposited
175
+ csharp
176
+ ```
177
+ return context.State is null
178
+ ? new(context.Event.amount, 1)
179
+ : context.State with { balance = context.State.balance + context.Event.amount };
180
+ ```
181
+ on AmountWithdrawn
182
+ file Reducers/Withdrawn.cs
183
+ ```
184
+
185
+ Reach for a reducer only when the next state depends on the current one — a
186
+ running balance, a state machine. `context.State` is **null for the first event**
187
+ and is the only nullable member; `Event`, `Key`, `Tenant`, `Occurred`,
188
+ `SequenceNumber` and `IsFirst` are always there.
189
+
190
+ **Prefer a projection where one will do.** A reducer is code, and code is the part
191
+ of a document a reader cannot check at a glance. A reducer binds to
192
+ `UnsupportedSemanticSyntax` in ESM v1, so it never reaches the reference runtime.
193
+
194
+ Read [pdl-grammar.md](references/pdl-grammar.md) for the complete EBNF, every
195
+ diagnostic code, and the full worked examples.
196
+
197
+ ## Verify
198
+
199
+ - [ ] `screenplay <model> --warnaserror` reports zero errors and zero warnings.
200
+ - [ ] Each read model has **exactly one** builder.
201
+ - [ ] Every read-model property traces back to an event that carries it.
202
+ - [ ] `every` was intended where `every` is written — not `all`.
203
+ - [ ] AutoMap's default-on behavior is intended, or `no automap` is declared at the
204
+ right level.
205
+ - [ ] Every `children` block's `from` declares `parent`.
206
+ - [ ] Every `nested` block contains at least one `from`.
207
+ - [ ] A reducer is present only because a projection genuinely could not express it.
208
+
209
+ ## Route near misses
210
+
211
+ - Deciding which read models exist at all: `cratis-screenplay-event-modeling`.
212
+ - Read-model shape, queries and screens: `cratis-screenplay-read-surface`.
213
+ - Specifying projection behavior: `cratis-screenplay-specifications`.
214
+ - Hand-written C# Chronicle projections: `cratis-chronicle-projection`.
215
+ - Hand-written C# Chronicle reducers: `cratis-chronicle-reducer`.
@@ -0,0 +1,239 @@
1
+ # PDL — complete grammar and diagnostics
2
+
3
+ Verified against `Documentation/screenplay/projections/grammar.md` and
4
+ `Source/DotNET/Screenplay/Parsing/ProjectionParser.cs` at commit `122eee8`.
5
+
6
+ Two places below are deliberately **looser than the published EBNF**, because the
7
+ parser accepts more than that EBNF describes and both forms were confirmed by
8
+ compiling them:
9
+
10
+ - `EveryBlock` accepts a bare `automap` as well as `no automap`. The published
11
+ grammar lists only `no automap`, but `from-every.md` shows `automap` in an
12
+ example and `ParseMappingBlock` handles both.
13
+ - The braces on a composite key are **optional**. The published grammar writes
14
+ them as required; a composite key with and without them both compile clean.
15
+
16
+ ## Grammar
17
+
18
+ ```ebnf
19
+ Projection = "projection", Ident, [ "=>", TypeRef ], NL,
20
+ [ INDENT, { ProjDirective | Block }, DEDENT ] ;
21
+
22
+ ProjDirective = "no", "automap", NL
23
+ | "sequence", Ident, NL
24
+ | "file", FilePath, NL
25
+ | KeyDecl
26
+ | CompositeKeyDecl ;
27
+
28
+ Block = EveryBlock | FromAllBlock | FromEventBlock | JoinBlock
29
+ | ChildrenBlock | NestedBlock | RemoveWithBlock | RemoveWithJoinBlock ;
30
+
31
+ EveryBlock = "every", NL, INDENT,
32
+ [ "automap" | "no", "automap", NL ], { MappingLine },
33
+ [ "exclude", "children", NL ], DEDENT ;
34
+
35
+ FromAllBlock = "all", NL,
36
+ [ INDENT, [ "automap" | "no", "automap", NL ], { MappingLine }, DEDENT ] ;
37
+
38
+ FromEventBlock = "from", EventSpec, { ",", EventSpec }, NL,
39
+ [ INDENT, [ ParentDecl ], { MappingLine | KeyDecl | CompositeKeyDecl }, DEDENT ] ;
40
+ EventSpec = TypeRef, [ "key", Expr ] ;
41
+
42
+ JoinBlock = "join", Ident, "on", Ident, NL, INDENT, { WithEventBlock }, DEDENT ;
43
+ WithEventBlock = "with", TypeRef, NL,
44
+ [ INDENT, [ "automap" | "no", "automap", NL ], { MappingLine }, DEDENT ] ;
45
+
46
+ ChildrenBlock = "children", Ident, "identified", "by", Expr, NL,
47
+ INDENT, [ "no", "automap", NL ], { ChildBlock }, DEDENT ;
48
+ ChildBlock = ChildEveryBlock | FromEventBlock | JoinBlock | RemoveWithBlock
49
+ | RemoveWithJoinBlock | ChildrenBlock | NestedBlock | ClearWithBlock ;
50
+ ChildEveryBlock = "every", NL, INDENT, [ "no", "automap", NL ], { MappingLine }, DEDENT ;
51
+
52
+ NestedBlock = "nested", Ident, NL,
53
+ INDENT, [ "automap" | "no", "automap", NL ],
54
+ { ProjDirective | Block | NestedBlock | ClearWithBlock }, DEDENT ;
55
+ ClearWithBlock = "clear", "with", TypeRef, NL ;
56
+
57
+ RemoveWithBlock = "remove", "with", TypeRef, [ "key", Expr ], NL,
58
+ [ INDENT, [ ParentDecl ], DEDENT ] ;
59
+ RemoveWithJoinBlock = "remove", "via", "join", "on", TypeRef, [ "key", Expr ], NL ;
60
+
61
+ KeyDecl = "key", Expr, NL ;
62
+ CompositeKeyDecl= "key", TypeRef, [ "{" ], NL,
63
+ INDENT, KeyPart, { NL, KeyPart }, DEDENT, [ "}", NL ] ;
64
+ KeyPart = Ident, "=", Expr ;
65
+
66
+ MappingLine = Assignment | ClearLine | IncLine | DecLine | CountLine | AddLine | SubLine ;
67
+ Assignment = Ident, "=", Expr, NL ;
68
+ ClearLine = "clear", Path, NL ;
69
+ IncLine = "increment", Ident, NL ;
70
+ DecLine = "decrement", Ident, NL ;
71
+ CountLine = "count", Ident, NL ;
72
+ AddLine = "add", Ident, "by", Expr, NL ;
73
+ SubLine = "subtract", Ident, "by", Expr, NL ;
74
+
75
+ Expr = Template | Literal | DollarExpr | Path ;
76
+ DollarExpr = "$eventSourceId" | "$eventContext", ".", Ident | "$causedBy", ".", Ident ;
77
+ Template = "`", { TemplateChar | "${", Expr, "}" }, "`" ;
78
+ Literal = BoolLiteral | StringLiteral | NumberLiteral | NullLiteral ;
79
+ LiteralKeyword = "literal", " ", Literal ; (* parsed at the Expr level *)
80
+ ```
81
+
82
+ ## Parser regexes
83
+
84
+ | Construct | Regex |
85
+ | --- | --- |
86
+ | Projection header | `^projection\s+(@?[\w.]+)\s*(?:=>\s*([\w.]+))?$` |
87
+ | Event spec | `^(@?[\w.]+)(?:\s+key\s+(.+))?$` |
88
+ | Join | `^join\s+(@?[\w.]+)\s+on\s+(@?[\w.]+)$` |
89
+ | With | `^with\s+(@?[\w.]+)$` |
90
+ | Children | `^children\s+(@?[\w.]+)\s+identified\s+by\s+(.+)$` |
91
+ | Nested | `^nested\s+(@?[\w.]+)$` |
92
+ | Remove with | `^remove\s+with\s+(@?[\w.]+)(?:\s+key\s+(.+))?$` |
93
+ | Remove via join | `^remove\s+via\s+join\s+on\s+(@?[\w.]+)(?:\s+key\s+(.+))?$` |
94
+ | Counters | `^(increment\|decrement\|count\|clear)\s+(@?[$\w.]+)$` |
95
+ | Arithmetic | `^(add\|subtract)\s+(@?[$\w.]+)\s+by\s+(.+)$` |
96
+ | Assignment | `^(@?[$\w.@]+)\s*=(?!=\|>)\s*(.+)$` |
97
+
98
+ ## Diagnostics
99
+
100
+ | Code | Meaning |
101
+ | --- | --- |
102
+ | `PLAY0054` | A projection document's top-level line does not open a `projection` |
103
+ | `PLAY0055` | A projection document declares no projection |
104
+ | `PLAY0056` | A `projection` line is not `projection <Name> [=> <ReadModel>]` |
105
+ | `PLAY0057` | A projection declares no directives, so it builds nothing |
106
+ | `PLAY0058` | A projection body line opens with an unknown word |
107
+ | `PLAY0059` | A projection declares more than one key |
108
+ | `PLAY0060` | A `from` block declares more than one key |
109
+ | `PLAY0061` | A `from` line names no event |
110
+ | `PLAY0062` | An event reference is not a readable name |
111
+ | `PLAY0063` | A `join` line is not `join <property> on <key>` |
112
+ | `PLAY0064` | A join block holds a line that is not `with <EventType>` |
113
+ | `PLAY0065` | A `children` line is malformed |
114
+ | `PLAY0066` | A `nested` line is not `nested <property>` |
115
+ | `PLAY0067` | A nested block reads from no event, so nothing ever fills it |
116
+ | `PLAY0068` | A `remove` line is neither `remove with` nor `remove via join on` |
117
+ | `PLAY0069` | A remove block holds a line other than `parent` |
118
+ | `PLAY0070` | A `clear` line is not `clear with <EventType>` |
119
+ | `PLAY0071` | `clear with` is written where there is nothing to clear |
120
+ | `PLAY0072` | A composite key part is not `<property> = <expression>` |
121
+ | `PLAY0073` | A composite key part is a template expression, which a key cannot be |
122
+ | `PLAY0074` | A composite key declares no parts |
123
+ | `PLAY0075` | A projection mapping line is unreadable |
124
+ | `PLAY0186` | A `readmodel` line is not `readmodel <Name>` |
125
+ | `PLAY0187` | A `reducer` line is not `reducer <Name> => <ReadModel>` |
126
+ | `PLAY0188` | A reducer body line is not `on <EventType>` |
127
+ | `PLAY0189` | A reducer declares no rules |
128
+ | `PLAY0190` | A reducer rule body opens with an unknown word |
129
+ | `PLAY0191` | A read model is built by more than one projection or reducer |
130
+ | `PLAY0192` | A read model is declared more than once |
131
+
132
+ ## Worked examples
133
+
134
+ ### Composite key with event context
135
+
136
+ ```screenplay
137
+ projection LineItems => LineItemReadModel
138
+ from LineItemAdded
139
+ key LineItemKey
140
+ OrderId = orderId
141
+ LineNumber = lineNumber
142
+ SequenceNumber = $eventContext.sequenceNumber
143
+ CreatedBy = $causedBy.subject
144
+ Product = productName
145
+ ```
146
+
147
+ ### Global counter with a literal key
148
+
149
+ ```screenplay
150
+ projection SiteStats => SiteStatsReadModel
151
+ from UserLoggedIn key literal "site-stats"
152
+ count TotalLogins
153
+ lastLogin = $eventContext.occurred
154
+ ```
155
+
156
+ Every `UserLoggedIn` updates the same instance, whatever its event source.
157
+
158
+ ### System-wide audit with `all` alongside `from`
159
+
160
+ ```screenplay
161
+ projection ActivityFeed => ActivityFeedModel
162
+ all
163
+ count totalSystemEvents
164
+ lastActivity = $eventContext.occurred
165
+ from UserRegistered
166
+ recentUsers = name
167
+ ```
168
+
169
+ `totalSystemEvents` counts **every** event in the system; `recentUsers` only fills
170
+ on `UserRegistered`.
171
+
172
+ ### Children with a join and scoped removal
173
+
174
+ ```screenplay
175
+ projection Group => GroupReadModel
176
+ from GroupCreated
177
+ Name = name
178
+ children members identified by userId
179
+ from UserAddedToGroup key userId
180
+ parent groupId
181
+ Role = role
182
+ join User on UserId
183
+ with UserCreated
184
+ no automap
185
+ UserName = name
186
+ remove with UserRemovedFromGroup key userId
187
+ parent groupId
188
+ ```
189
+
190
+ ### `every` that ignores child activity
191
+
192
+ ```screenplay
193
+ projection Group => GroupReadModel
194
+ every
195
+ LastActivity = $eventContext.occurred
196
+ exclude children
197
+ from GroupCreated
198
+ Name = name
199
+ children members identified by userId
200
+ from UserAdded
201
+ Name = userName
202
+ ```
203
+
204
+ Group-level events bump `LastActivity`; member events do not.
205
+
206
+ ### Nested nullable object
207
+
208
+ ```screenplay
209
+ projection Slice => SliceReadModel
210
+ from SliceCreated
211
+ Name = name
212
+ nested command
213
+ from CommandSetForSlice
214
+ Name = commandName
215
+ Schema = schema
216
+ from CommandRenamed
217
+ Name = newName
218
+ clear with CommandClearedForSlice
219
+ ```
220
+
221
+ `command` is null until `CommandSetForSlice`, updated in place by
222
+ `CommandRenamed`, and set back to null by `CommandClearedForSlice`.
223
+
224
+ ### Several projections in one slice
225
+
226
+ ```screenplay
227
+ slice StateView CustomerPortalReport
228
+ query GetPortalReport => PortalReportReadModel
229
+ by customerId CustomerId
230
+ projection PortalReport => PortalReportReadModel
231
+ from PortalInvitationSent
232
+ invitedAt = $eventContext.occurred
233
+ projection RevokedPortalToken => RevokedPortalTokenReadModel
234
+ from PortalTokenRevoked
235
+ revokedAt = $eventContext.occurred
236
+ ```
237
+
238
+ Two read models, one behavior. Splitting them across two slices would say the
239
+ system has two behaviors where it has one.
@@ -0,0 +1,11 @@
1
+ {
2
+ "skill": "cratis-screenplay-projections",
3
+ "input": "Build a read model from events with the Projection Declaration Language.",
4
+ "fixtures": [],
5
+ "assertions": [
6
+ { "kind": "skill-contains", "value": "AutoMap is on by default" },
7
+ { "kind": "skill-contains", "value": "PLAY0191" },
8
+ { "kind": "skill-contains", "value": "every event type in the system" },
9
+ { "kind": "skill-contains", "value": "references/pdl-grammar.md" }
10
+ ]
11
+ }
@@ -0,0 +1,2 @@
1
+ Copyright (c) Cratis. All rights reserved.
2
+ Licensed under the MIT license. See LICENSE file in the project root for full license information.
@@ -0,0 +1,188 @@
1
+ ---
2
+ name: cratis-screenplay-read-surface
3
+ description: Declare the read side of a Cratis Screenplay `.play` model — the `readmodel` shape, `query` with `by`/`filter`/`scoped to`/`observable`/`performer`, and `screen` at its three levels of detail, plus how a bare name resolves across slices. Use when adding or changing a query, a read-model shape, or a screen in a `.play` model, or when deciding what a caller may narrow a result by. Do not use for how events fill the read model, and do not use for layouts, templates or forms.
4
+ license: MIT
5
+ ---
6
+
7
+ # The Screenplay read surface
8
+
9
+ What the system can be asked, and what a user sees. The `readmodel` declares the
10
+ shape, the `query` is the entry point, and the `screen` renders it.
11
+
12
+ ## Verified product sources
13
+
14
+ | Package | Version | Purpose |
15
+ | --- | --- | --- |
16
+ | `Cratis.Screenplay` | `4.12.1` | Parser, validator, diagnostics |
17
+
18
+ Read from the Screenplay repository at tag `v4.12.1` (commit `122eee8`), against
19
+ `Documentation/screenplay/{queries,readmodels,screens}.md` and
20
+ `Source/DotNET/Screenplay/Parsing/`. Reverify before claiming another version
21
+ behaves the same.
22
+
23
+ ## `readmodel` — shape only
24
+
25
+ ```screenplay
26
+ readmodel AccountBalance
27
+ description "What the account is worth right now"
28
+ balance Decimal
29
+ movements Int
30
+ note String?
31
+ ```
32
+
33
+ A read model declares what it **is** — nothing about what builds it. Whatever
34
+ builds it points at it with `=>`, and **exactly one thing may**: two builders is
35
+ error `PLAY0191`. See `cratis-screenplay-projections` for the builder side.
36
+
37
+ ## `query`
38
+
39
+ ```screenplay
40
+ query ListInvoices => InvoiceListReadModel[]
41
+ description "Every invoice the caller may see, narrowed by status and customer"
42
+ filter status InvoiceStatus?
43
+ filter customerId CustomerId?
44
+ filter tenantId TenantId from $context.tenant
45
+ authorize IsAuthenticated
46
+ ```
47
+
48
+ Return-type forms after `=>`: `ReadModel`, `ReadModel?`, `ReadModel[]`, each
49
+ optionally prefixed `observable`.
50
+
51
+ ### `by` vs `filter` — and why it is a security decision
52
+
53
+ | Clause | Meaning |
54
+ | --- | --- |
55
+ | `by` | the **identifying** parameter — the query returns the instance it identifies |
56
+ | `filter` | an optional parameter narrowing the result set; usually typed `?` |
57
+ | `from <source>` | fills the parameter **from the context** instead of from the caller |
58
+
59
+ ⚠️ **Anything the caller must not be able to choose — the tenant, the caller's own
60
+ subject — belongs on a `from` parameter, never on a `filter` the UI supplies.**
61
+ Any mapping source works as a `from` source, so `$context.`, `$env.` and constants
62
+ are all available.
63
+
64
+ ### `scoped to`
65
+
66
+ ```screenplay
67
+ query Mine => Timesheet[]
68
+ scoped to identity
69
+ query Everyones => Timesheet[]
70
+ scoped to global
71
+ ```
72
+
73
+ ⚠️ **The tenant is the default and stays unstated.** A query is scoped to the
74
+ tenant it runs for unless it says otherwise, so `scoped to global` is how a query
75
+ *opts out* and reaches past the tenant. As the documentation puts it: *"Making the
76
+ narrow case the default means the dangerous case is the one you have to write
77
+ down, rather than the one you get by forgetting."*
78
+
79
+ **The scope is a name, not a closed set.** `identity` and `global` are the two the
80
+ language documents, but the grammar accepts any name — what scopes exist follows
81
+ the identity model of whatever runs the document. A query declares at most one.
82
+
83
+ Treat every `scoped to global` in a review as a question to answer, not a detail.
84
+
85
+ ### `observable`
86
+
87
+ `=> observable OverdueInvoicesReadModel[]` is a live read that keeps pushing;
88
+ without the marker a query is one-shot, which is the default and what most reads
89
+ are. The marker qualifies only *how* the result arrives, so it composes with `[]`,
90
+ `?`, `by`, `filter`, `authorize` and a `performer`. A screen binds to a live query
91
+ exactly as it binds to a one-shot one and gets updates for free.
92
+
93
+ ### `performer`
94
+
95
+ A query is **complete without a performer** — it is realization metadata, not a
96
+ precondition. It takes a `file` reference or an inline code block, and is the
97
+ query's counterpart to a command's `handler`. `csharp` and `sql` are the two that
98
+ make sense here; the parser accepts any registered inline language
99
+ (`csharp`, `typescript`, `react`, `html`, `sql` are built in) and does not
100
+ reject a nonsensical one, so the choice is yours to get right.
101
+
102
+ ## `screen` — three levels
103
+
104
+ **Level 1 — intent.** Data and actions; the tool generates the component.
105
+
106
+ ```screenplay
107
+ screen InvoiceList
108
+ data InvoiceListReadModel[] via query ListInvoices
109
+ action RegisterInvoice
110
+ navigate to RegisterInvoiceScreen
111
+ action CancelInvoice
112
+ ```
113
+
114
+ **Level 2 — structure.** Named sections, tables and summaries filling a template's
115
+ slots.
116
+
117
+ ```screenplay
118
+ screen InvoiceDetails
119
+ template MasterDetail
120
+ sidebar
121
+ data InvoiceDetailsReadModel via query GetInvoice by invoiceId
122
+ summary InvoiceDetailsReadModel
123
+ field invoiceNumber label "Invoice #"
124
+ section actions
125
+ action CancelInvoice
126
+ main
127
+ section lineItems
128
+ table lineItems
129
+ column lineNumber label "#"
130
+ column quantity label "Qty"
131
+ on row-click navigate to InvoiceLineDetail by lineNumber
132
+ ```
133
+
134
+ **Level 3 — inline code.** The surrounding Screenplay context supplies the typed
135
+ data contract; the inline block receives it as `Props`. Languages: `react`,
136
+ `typescript`, `html`, `csharp`.
137
+
138
+ Constructs: `title`, `data … via query … [by <param>]`, `action <Command>` with
139
+ `label` and `navigate to <Screen> [by <param>]`, `section <name>`,
140
+ `table <target>` with `column <property> [label]` and
141
+ `on row-click navigate to <Screen> [by <param>]`, `summary <ReadModel>` with
142
+ `field <property> label`, and `template <Name>` with slot bodies.
143
+
144
+ ## How a bare name resolves
145
+
146
+ **Inside out:** the slice, then the enclosing feature, then the module, then the
147
+ document. The innermost match wins.
148
+
149
+ That rule exists because a generated document cannot make every name unique — one
150
+ real application declares 76 queries under 37 distinct names, with `All` appearing
151
+ 21 times. Two sibling slices can each declare `All`, and each screen gets its own.
152
+
153
+ Reach across slices by qualifying with **any trailing part** of the scope:
154
+
155
+ ```screenplay
156
+ screen OverviewScreen
157
+ data QueueReadModel[] via query Queue.All
158
+ data DeviationReadModel[] via query Preparation.Deviations.All
159
+ ```
160
+
161
+ Use the shortest unambiguous form. If a bare name matches two declarations equally
162
+ well, the compiler **warns and names the candidates** rather than picking one:
163
+
164
+ ```text
165
+ Ambiguous query 'All' - it matches 2 declarations equally well
166
+ (Invoicing.Preparation.Queue, Invoicing.Preparation.Deviations); qualify it to say which
167
+ ```
168
+
169
+ ⚠️ Unresolved and ambiguous references are **warnings, not errors**, because a
170
+ name may resolve to something outside the document. Run with `--warnaserror` or a
171
+ screen can navigate to a screen that does not exist and the build stays green.
172
+
173
+ ## Verify
174
+
175
+ - [ ] `screenplay <model> --warnaserror` reports zero errors and zero warnings.
176
+ - [ ] Every value the caller must not choose is a `from` parameter, not a `filter`.
177
+ - [ ] Every `scoped to global` is deliberate and defensible.
178
+ - [ ] `observable` is present exactly where the caller should see changes without
179
+ asking again.
180
+ - [ ] Each read model has exactly one builder and every field traces to an event.
181
+ - [ ] No screen reference is left ambiguous or unresolved.
182
+
183
+ ## Route near misses
184
+
185
+ - How events fill the read model: `cratis-screenplay-projections`.
186
+ - Layouts, templates, forms, contributions, themes: `cratis-screenplay-ui-composition`.
187
+ - Asserting query results: `cratis-screenplay-specifications`.
188
+ - Deciding which read models exist: `cratis-screenplay-event-modeling`.
@@ -0,0 +1,10 @@
1
+ {
2
+ "skill": "cratis-screenplay-read-surface",
3
+ "input": "Declare a Screenplay query and the screen that binds to it.",
4
+ "fixtures": [],
5
+ "assertions": [
6
+ { "kind": "skill-contains", "value": "The tenant is the default and stays unstated" },
7
+ { "kind": "skill-contains", "value": "belongs on a `from` parameter, never on a `filter`" },
8
+ { "kind": "skill-contains", "value": "warnings, not errors" }
9
+ ]
10
+ }
@@ -0,0 +1,2 @@
1
+ Copyright (c) Cratis. All rights reserved.
2
+ Licensed under the MIT license. See LICENSE file in the project root for full license information.