@cratis/pi 6.2.0 → 6.3.1
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.
- package/package/corpus/skills/cratis-screenplay-captures-and-reactions/SKILL.md +90 -15
- package/package/corpus/skills/cratis-screenplay-captures-and-reactions/verification.json +5 -1
- package/package/corpus/skills/cratis-screenplay-command-surface/SKILL.md +162 -64
- package/package/corpus/skills/cratis-screenplay-command-surface/references/context.md +20 -8
- package/package/corpus/skills/cratis-screenplay-command-surface/verification.json +33 -1
- package/package/corpus/skills/cratis-screenplay-event-modeling/SKILL.md +63 -31
- package/package/corpus/skills/cratis-screenplay-event-modeling/references/nine-steps.md +35 -13
- package/package/corpus/skills/cratis-screenplay-event-modeling/verification.json +12 -0
- package/package/corpus/skills/cratis-screenplay-model-authoring/SKILL.md +50 -9
- package/package/corpus/skills/cratis-screenplay-model-authoring/references/language-reference.md +78 -22
- package/package/corpus/skills/cratis-screenplay-model-authoring/references/mcp-tools.md +21 -5
- package/package/corpus/skills/cratis-screenplay-model-authoring/verification.json +16 -0
- package/package/corpus/skills/cratis-screenplay-projections/SKILL.md +155 -43
- package/package/corpus/skills/cratis-screenplay-projections/references/pdl-grammar.md +81 -37
- package/package/corpus/skills/cratis-screenplay-projections/verification.json +6 -1
- package/package/corpus/skills/cratis-screenplay-read-surface/SKILL.md +64 -17
- package/package/corpus/skills/cratis-screenplay-read-surface/verification.json +4 -1
- package/package/corpus/skills/cratis-screenplay-specifications/SKILL.md +201 -115
- package/package/corpus/skills/cratis-screenplay-specifications/verification.json +13 -1
- package/package/corpus/skills/cratis-screenplay-ui-composition/SKILL.md +133 -24
- package/package/corpus/skills/cratis-screenplay-ui-composition/verification.json +4 -1
- package/package.json +1 -1
|
@@ -25,31 +25,40 @@ source is the single flow model.
|
|
|
25
25
|
|
|
26
26
|
| Package | Version | Purpose |
|
|
27
27
|
| --- | --- | --- |
|
|
28
|
-
| `Cratis.Screenplay` | `4.
|
|
28
|
+
| `Cratis.Screenplay` | `4.31.0` | PDL parser, validator, diagnostics, semantic binder |
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
`Documentation/screenplay/projections/`
|
|
32
|
-
`
|
|
33
|
-
|
|
30
|
+
Checked against the Screenplay repository at tag `v4.31.0` (commit `355dffb`):
|
|
31
|
+
`Documentation/screenplay/projections/` (including `semantic-model.md`, `keys.md`
|
|
32
|
+
and `variants.md`), `readmodels.md`, `diagnostics.md` and decisions 0001 and 0002.
|
|
33
|
+
Every example below compiles with that version's compiler. Reverify before
|
|
34
|
+
claiming another version behaves the same.
|
|
35
|
+
|
|
36
|
+
**Chronicle decides what a projection means** (Screenplay decision 0001).
|
|
37
|
+
Screenplay owns the grammar; which instance an event affects, what a join may
|
|
38
|
+
create and what a removal removes follow Chronicle's lowering. Syntax that
|
|
39
|
+
compiles can still mean something other than it seems: read the warnings.
|
|
34
40
|
|
|
35
41
|
## The shape
|
|
36
42
|
|
|
43
|
+
Excerpt: the concepts and the three events (each carrying `invoiceId`) are
|
|
44
|
+
declared elsewhere in the model.
|
|
45
|
+
|
|
37
46
|
```screenplay
|
|
38
47
|
readmodel InvoiceDetailsReadModel
|
|
48
|
+
invoiceId InvoiceId
|
|
39
49
|
invoiceNumber InvoiceNumber
|
|
40
50
|
status InvoiceStatus
|
|
41
51
|
lastUpdatedAt DateTime
|
|
42
52
|
|
|
43
53
|
projection InvoiceDetails => InvoiceDetailsReadModel
|
|
44
|
-
key invoiceId
|
|
45
54
|
every
|
|
46
55
|
lastUpdatedAt = $eventContext.occurred
|
|
47
56
|
exclude children
|
|
48
57
|
from InvoiceRegistered key invoiceId
|
|
49
58
|
status = "draft"
|
|
50
|
-
from InvoiceSent
|
|
59
|
+
from InvoiceSent key invoiceId
|
|
51
60
|
status = "sent"
|
|
52
|
-
remove with InvoiceCancelled
|
|
61
|
+
remove with InvoiceCancelled key invoiceId
|
|
53
62
|
```
|
|
54
63
|
|
|
55
64
|
**The arrow always points the same way.** A read model never declares what builds
|
|
@@ -60,14 +69,18 @@ projections, each building a **different** read model.
|
|
|
60
69
|
|
|
61
70
|
## Projection-level directives
|
|
62
71
|
|
|
63
|
-
`
|
|
64
|
-
|
|
72
|
+
`no automap`, `sequence <name>`, `file <path>`. `sequence` is a realization
|
|
73
|
+
concern and does not bind to the executable model.
|
|
74
|
+
|
|
75
|
+
⚠️ **Do not write `key` on the projection.** It parses, but neither Chronicle nor
|
|
76
|
+
the executable model routes any event by it, and it is not a default for the
|
|
77
|
+
`from` blocks (warning `PLAY0381`). Key each `from` instead.
|
|
65
78
|
|
|
66
79
|
## Keys — eight ways, and the default
|
|
67
80
|
|
|
68
81
|
| Form | Example |
|
|
69
82
|
| --- | --- |
|
|
70
|
-
| Projection-level | `key invoiceId` |
|
|
83
|
+
| Projection-level | `key invoiceId` — **routes nothing** (`PLAY0381`); do not use |
|
|
71
84
|
| From-level inline | `from InvoiceRegistered key invoiceId` |
|
|
72
85
|
| From-level block | indented `key invoiceId` inside the `from` |
|
|
73
86
|
| Per event, several events | `from A key idA, B key idB, C` |
|
|
@@ -76,6 +89,12 @@ error `PLAY0059`.
|
|
|
76
89
|
| Children identity | `children lines identified by lineNumber` |
|
|
77
90
|
| **Default** | **the event source id** — `from X` ≡ `from X key $eventSourceId` |
|
|
78
91
|
|
|
92
|
+
The key resolves from the event's inline key, then the `from` block's key, then
|
|
93
|
+
the event source id. A `from` without a key does **not** inherit another
|
|
94
|
+
`from`'s key: when one event is keyed by `invoiceId` and another is unkeyed, they
|
|
95
|
+
update the same instance only if the event source id happens to equal
|
|
96
|
+
`invoiceId`.
|
|
97
|
+
|
|
79
98
|
A second key on one `from` is error `PLAY0060`. A template expression in a
|
|
80
99
|
composite key is error `PLAY0073`; an empty composite key is `PLAY0074`.
|
|
81
100
|
|
|
@@ -84,11 +103,16 @@ composite key is error `PLAY0073`; an empty composite key is `PLAY0074`.
|
|
|
84
103
|
| Block | Fires for | Use for |
|
|
85
104
|
| --- | --- | --- |
|
|
86
105
|
| `from <Event>` | that event type only | the actual mapping work |
|
|
87
|
-
| `every` | **only** the types
|
|
88
|
-
| `all` | **every event type in the system
|
|
106
|
+
| `every` | **only** the event types its level names in `from` and `join` blocks | common fields across your own events |
|
|
107
|
+
| `all` | **every event type in the system**; types no `from` names are keyed by their event source id | activity timestamps and counters per event source |
|
|
89
108
|
|
|
90
109
|
⚠️ `all` and `every` are not synonyms, and confusing them is the classic PDL
|
|
91
|
-
mistake. `every` is scoped to your `from`
|
|
110
|
+
mistake. `every` is scoped to your level's `from` and `join` events — a joined
|
|
111
|
+
event also runs the `every` mappings, so a counter in `every` counts joined
|
|
112
|
+
events too. `all` is not scoped at all. Only a projection's own level can
|
|
113
|
+
subscribe to every event type: `all` inside `children` or `nested` behaves as
|
|
114
|
+
`every` (warning `PLAY0380`). An `all` block beside removals, `children` or
|
|
115
|
+
`nested` binds, but the reference execution plan refuses it.
|
|
92
116
|
|
|
93
117
|
`every` accepts `exclude children`, so child events do not bump the parent's
|
|
94
118
|
`lastUpdatedAt`. The `every` block **inside** a `children` block does not accept
|
|
@@ -98,14 +122,25 @@ it — it is already in a children context.
|
|
|
98
122
|
|
|
99
123
|
⚠️ **AutoMap is on by default.** Matching property names are copied before your
|
|
100
124
|
explicit mappings run, and explicit mappings win. Turn it off with `no automap` at
|
|
101
|
-
**projection**, `every`/`all`, `join
|
|
102
|
-
**cannot** be toggled inside an individual `from` block.
|
|
125
|
+
**projection**, `every`/`all`, `join`, `children` or `nested` level — it
|
|
126
|
+
**cannot** be toggled inside an individual `from` block. An `automap`/`no automap`
|
|
127
|
+
under a joined event (`with …`) parses but Chronicle replaces it with the auto-map
|
|
128
|
+
of the level the join sits in (`PLAY0380`).
|
|
103
129
|
|
|
104
130
|
Mapping sources: a property path (`name`, `contactInfo.email`), a literal
|
|
105
|
-
(`true`, `"Pending"`, `42`, `null`),
|
|
106
|
-
`$
|
|
107
|
-
|
|
108
|
-
|
|
131
|
+
(`true`, `"Pending"`, `42`, `null`), `$eventSourceId`, and
|
|
132
|
+
`$eventContext.<path>` from the event-context catalog (for example `occurred`,
|
|
133
|
+
`sequenceNumber`, `correlationId`, `causedBy.subject`; an unknown path warns with
|
|
134
|
+
`PLAY0295`). Templates (`` `${first} ${last}` ``) and `$causedBy.<…>` parse but
|
|
135
|
+
do not bind to the executable model; write `$eventContext.causedBy.subject`
|
|
136
|
+
instead of `$causedBy.subject`.
|
|
137
|
+
|
|
138
|
+
⚠️ Only the event source identity (`$eventSourceId`,
|
|
139
|
+
`$eventContext.eventSourceId`) runs in the reference runner. Any other
|
|
140
|
+
`$eventContext` path in a mapping or key — `occurred` in the shape above,
|
|
141
|
+
`sequenceNumber`, `causedBy.subject` — binds, but the reference execution plan
|
|
142
|
+
refuses it, so no specification in that model runs there. Use it when the target
|
|
143
|
+
needs it, and report those specifications as needing a target.
|
|
109
144
|
|
|
110
145
|
`clear <property>` removes a value and is equivalent to `= null`; prefer `clear`.
|
|
111
146
|
It takes a dotted path (`clear Owner.Note`) and escapes reserved names
|
|
@@ -126,23 +161,33 @@ must be numeric.
|
|
|
126
161
|
|
|
127
162
|
## Joins
|
|
128
163
|
|
|
164
|
+
Excerpt, inside a `projection` whose read model has `customerId` and
|
|
165
|
+
`customerName`:
|
|
166
|
+
|
|
129
167
|
```screenplay
|
|
130
|
-
join
|
|
168
|
+
join customer on customerId
|
|
131
169
|
with CustomerCreated
|
|
132
|
-
|
|
133
|
-
CustomerName = name
|
|
170
|
+
customerName = name
|
|
134
171
|
with CustomerUpdated
|
|
135
|
-
|
|
172
|
+
customerName = name
|
|
136
173
|
```
|
|
137
174
|
|
|
138
|
-
`join <
|
|
139
|
-
`with <EventType>` blocks (error `PLAY0064` otherwise)
|
|
140
|
-
|
|
141
|
-
|
|
175
|
+
`join <label> on <property>` (error `PLAY0063` otherwise), then one or more
|
|
176
|
+
`with <EventType>` blocks (error `PLAY0064` otherwise). **A join never creates an
|
|
177
|
+
instance**: a joined event updates every existing instance whose `on` property
|
|
178
|
+
equals the joined event's event source id. Chronicle discards the label, but
|
|
179
|
+
inside `children` and `nested` the compiler's completeness check (`PLAY0284`)
|
|
180
|
+
counts the label as the field the join fills, so name it after that field there.
|
|
181
|
+
Joins work at a projection's own level and inside `children`. Inside `nested`,
|
|
182
|
+
Chronicle's engine does not wire a join, a `children` block or a
|
|
183
|
+
`remove via join`; they bind, but the reference execution plan refuses them.
|
|
184
|
+
A join cannot declare its own key or trigger removal — that is
|
|
142
185
|
`remove via join on`.
|
|
143
186
|
|
|
144
187
|
## Children and nested
|
|
145
188
|
|
|
189
|
+
Excerpt, inside a `projection`:
|
|
190
|
+
|
|
146
191
|
```screenplay
|
|
147
192
|
children lineItems identified by lineNumber
|
|
148
193
|
from InvoiceLineItemAdded key lineNumber
|
|
@@ -167,6 +212,8 @@ nested billingContact
|
|
|
167
212
|
|
|
168
213
|
## Removal
|
|
169
214
|
|
|
215
|
+
Excerpt, inside a `projection`:
|
|
216
|
+
|
|
170
217
|
```screenplay
|
|
171
218
|
remove with InvoiceCancelled key invoiceId
|
|
172
219
|
remove via join on CustomerAccountClosed
|
|
@@ -174,48 +221,113 @@ remove via join on CustomerAccountClosed
|
|
|
174
221
|
|
|
175
222
|
`remove with <Event> [key <expr>]` removes the instance the event identifies.
|
|
176
223
|
`remove via join on <Event> [key <expr>]` removes instances reached through a
|
|
177
|
-
join.
|
|
224
|
+
join. On a projection's own level, as in the second line above, it has no
|
|
225
|
+
verified meaning — Chronicle's engine wires it as a child removal — so it binds
|
|
226
|
+
but the reference execution plan refuses it. Use it inside `children`. Inside
|
|
227
|
+
`children`, both take an indented `parent <expr>` — and **only**
|
|
178
228
|
`parent`; anything else is error `PLAY0069`. Several removal conditions may
|
|
179
229
|
coexist.
|
|
180
230
|
|
|
181
|
-
##
|
|
231
|
+
## Variants — one identity, mutually exclusive read models
|
|
232
|
+
|
|
233
|
+
Excerpt: the events and the three read models (each with a keyed query) are
|
|
234
|
+
declared elsewhere. Every event about an issue is appended with the issue's
|
|
235
|
+
identifier as its event source.
|
|
182
236
|
|
|
183
237
|
```screenplay
|
|
238
|
+
projection WorkItem
|
|
239
|
+
from TitleChanged
|
|
240
|
+
title = title
|
|
241
|
+
variant BacklogItem
|
|
242
|
+
enters on IssueCreated
|
|
243
|
+
variant DevelopmentItem
|
|
244
|
+
enters on IssueStarted
|
|
245
|
+
variant PullRequestItem
|
|
246
|
+
enters on PullRequestCreated
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Each variant is its own read model; only its `enters on` events create it.
|
|
250
|
+
Everything else, including a projection-level shared handler, is update-only: it
|
|
251
|
+
becomes a join on the variant's identifier, matched against the handler's key
|
|
252
|
+
(here the event source).
|
|
253
|
+
|
|
254
|
+
⚠️ **Mutual exclusion works through the event source identity.** Entering a
|
|
255
|
+
variant removes the entity from its siblings, and that removal is always keyed by
|
|
256
|
+
the entering event's source identity, whatever key the `enters on` line declares.
|
|
257
|
+
Keep the event source equal to the entity's identifier, as above. An
|
|
258
|
+
`enters on IssueStarted key issueId` whose `issueId` differs from the event
|
|
259
|
+
source creates the development item under `issueId` but removes the backlog item
|
|
260
|
+
keyed by the event source instead, so the issue stays in both variants.
|
|
261
|
+
|
|
262
|
+
Diagnostics: no `enters on` (`PLAY0382`), a shared mapping a variant's read
|
|
263
|
+
model lacks (`PLAY0383`), duplicate variant names (`PLAY0384`), an entering
|
|
264
|
+
event claimed twice (`PLAY0385`). The executable model binds variants the way Chronicle's client SDK
|
|
265
|
+
reclassifies them; Chronicle's hosted declaration language does not lower them
|
|
266
|
+
yet (Cratis/Chronicle#4109). Use variants for genuinely different shapes, not for
|
|
267
|
+
a single `status` field.
|
|
268
|
+
|
|
269
|
+
## When PDL is not enough — the reducer
|
|
270
|
+
|
|
271
|
+
Excerpt: the events and the `AccountBalance` read model are declared elsewhere.
|
|
272
|
+
|
|
273
|
+
````screenplay
|
|
184
274
|
reducer Balance => AccountBalance
|
|
185
275
|
on AmountDeposited
|
|
186
|
-
csharp
|
|
187
|
-
```
|
|
276
|
+
```csharp
|
|
188
277
|
return context.State is null
|
|
189
278
|
? new(context.Event.amount, 1)
|
|
190
279
|
: context.State with { balance = context.State.balance + context.Event.amount };
|
|
191
280
|
```
|
|
192
281
|
on AmountWithdrawn
|
|
193
282
|
file Reducers/Withdrawn.cs
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
`
|
|
283
|
+
````
|
|
284
|
+
|
|
285
|
+
Inline code uses a tagged fence (` ```csharp `); a `csharp` line above a bare
|
|
286
|
+
fence still parses but warns (`PLAY0397`). Reach for a reducer only when the next
|
|
287
|
+
state depends on the current one — a running balance, a state machine.
|
|
288
|
+
`context.State` is **null for the first event** and is the only nullable member;
|
|
289
|
+
`Event`, `Key`, `Tenant`, `Occurred`, `SequenceNumber` and `IsFirst` are always
|
|
290
|
+
there.
|
|
291
|
+
|
|
292
|
+
What the executable model does with it (ESM v3):
|
|
293
|
+
|
|
294
|
+
- Every `on` rule needs a body, inline or `file`. Mixing bodied and body-less
|
|
295
|
+
rules is `PLAY0398`; two rules for one event is `PLAY0399`; a reducer with no
|
|
296
|
+
bodies is rejected with a hint to write a projection.
|
|
297
|
+
- The key is **always the event source id** — a reducer cannot route by an event
|
|
298
|
+
property. State starts at null; returning null deletes the instance.
|
|
299
|
+
- The body is an opaque attachment identified by a content hash, never compiled
|
|
300
|
+
or run by Screenplay. The reference runner cannot compute reducer state, so a
|
|
301
|
+
specification that reads a reducer-built read model reports unsupported and
|
|
302
|
+
never passes. A target must supply the transition.
|
|
200
303
|
|
|
201
304
|
**Prefer a projection where one will do.** A reducer is code, and code is the part
|
|
202
|
-
of a document a reader cannot check at a glance.
|
|
203
|
-
`UnsupportedSemanticSyntax` in ESM v1, so it never reaches the reference runtime.
|
|
305
|
+
of a document a reader cannot check at a glance.
|
|
204
306
|
|
|
205
|
-
Read [pdl-grammar.md](references/pdl-grammar.md) for the
|
|
206
|
-
diagnostic
|
|
307
|
+
Read [pdl-grammar.md](references/pdl-grammar.md) for the syntax grammar, the
|
|
308
|
+
projection diagnostic codes, and worked examples.
|
|
207
309
|
|
|
208
310
|
## Verify
|
|
209
311
|
|
|
210
312
|
- [ ] `screenplay <model> --warnaserror` reports zero errors and zero warnings.
|
|
211
313
|
- [ ] Each read model has **exactly one** builder.
|
|
314
|
+
- [ ] No projection-level `key`; every `from` that must address the same
|
|
315
|
+
instance is keyed on the same identity.
|
|
212
316
|
- [ ] Every read-model property traces back to an event that carries it.
|
|
213
317
|
- [ ] `every` was intended where `every` is written — not `all`.
|
|
214
318
|
- [ ] AutoMap's default-on behavior is intended, or `no automap` is declared at the
|
|
215
319
|
right level.
|
|
216
320
|
- [ ] Every `children` block's `from` declares `parent`.
|
|
217
321
|
- [ ] Every `nested` block contains at least one `from`.
|
|
218
|
-
- [ ]
|
|
322
|
+
- [ ] No `$causedBy`, template or `sequence` where the model must run.
|
|
323
|
+
- [ ] Where specifications must run in the reference runner: no `$eventContext`
|
|
324
|
+
path other than `eventSourceId`, no projection-level `remove via join`, no
|
|
325
|
+
`all` beside removals, `children` or `nested`, and no `join`, `children` or
|
|
326
|
+
`remove via join` inside `nested`.
|
|
327
|
+
- [ ] Variant entering events use the event source identity as the entity's
|
|
328
|
+
identifier.
|
|
329
|
+
- [ ] A reducer is present only because a projection genuinely could not express it,
|
|
330
|
+
and every rule has a body in a tagged fence or a `file`.
|
|
219
331
|
|
|
220
332
|
## Route near misses
|
|
221
333
|
|
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
# PDL —
|
|
1
|
+
# PDL — syntax grammar, meaning and diagnostics
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
`Source/DotNET/Screenplay/Parsing/ProjectionParser.cs` at
|
|
3
|
+
Checked against `Documentation/screenplay/projections/{grammar,semantic-model,keys,variants}.md`
|
|
4
|
+
and `Source/DotNET/Screenplay/Parsing/ProjectionParser.cs` at Screenplay tag
|
|
5
|
+
`v4.31.0` (commit `355dffb`).
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- The braces on a composite key are **optional** — a composite key with and
|
|
12
|
-
without them both compile clean.
|
|
13
|
-
- `$causedBy` is a valid expression root.
|
|
14
|
-
|
|
15
|
-
Against a Screenplay older than that fix the published grammar is the stricter of
|
|
16
|
-
the two, and this file is the accurate one.
|
|
7
|
+
The grammar says what **parses**. It does not say what an event does at runtime:
|
|
8
|
+
Chronicle's lowering decides that (Screenplay decision 0001), and the
|
|
9
|
+
[meaning table](#syntax-that-parses-but-means-something-else) lists the places
|
|
10
|
+
where the two differ. The braces on a composite key are optional, and
|
|
11
|
+
`EveryBlock` accepts a bare `automap` as well as `no automap`.
|
|
17
12
|
|
|
18
13
|
## Grammar
|
|
19
14
|
|
|
@@ -28,7 +23,14 @@ ProjDirective = "no", "automap", NL
|
|
|
28
23
|
| CompositeKeyDecl ;
|
|
29
24
|
|
|
30
25
|
Block = EveryBlock | FromAllBlock | FromEventBlock | JoinBlock
|
|
31
|
-
| ChildrenBlock | NestedBlock | RemoveWithBlock | RemoveWithJoinBlock
|
|
26
|
+
| ChildrenBlock | NestedBlock | RemoveWithBlock | RemoveWithJoinBlock
|
|
27
|
+
| VariantBlock ;
|
|
28
|
+
|
|
29
|
+
VariantBlock = "variant", Ident, NL,
|
|
30
|
+
INDENT, EntersOnDecl, { EntersOnDecl }, { ProjDirective | Block }, DEDENT ;
|
|
31
|
+
EntersOnDecl = "enters", "on", TypeRef, [ "key", Expr ], NL ;
|
|
32
|
+
|
|
33
|
+
(* A projection-level KeyDecl parses but routes no event (PLAY0381). *)
|
|
32
34
|
|
|
33
35
|
EveryBlock = "every", NL, INDENT,
|
|
34
36
|
[ "automap" | "no", "automap", NL ], { MappingLine },
|
|
@@ -93,6 +95,9 @@ LiteralKeyword = "literal", " ", Literal ; (* parsed at the Expr level *)
|
|
|
93
95
|
| Nested | `^nested\s+(@?[\w.]+)$` |
|
|
94
96
|
| Remove with | `^remove\s+with\s+(@?[\w.]+)(?:\s+key\s+(.+))?$` |
|
|
95
97
|
| Remove via join | `^remove\s+via\s+join\s+on\s+(@?[\w.]+)(?:\s+key\s+(.+))?$` |
|
|
98
|
+
| Clear with | `^clear\s+with\s+(@?[\w.]+)$` |
|
|
99
|
+
| Variant | `^variant\s+(@?[\w.]+)\s*$` |
|
|
100
|
+
| Enters on | `^enters\s+on\s+(@?[\w.]+)(?:\s+key\s+(.+))?$` |
|
|
96
101
|
| Counters | `^(increment\|decrement\|count\|clear)\s+(@?[$\w.]+)$` |
|
|
97
102
|
| Arithmetic | `^(add\|subtract)\s+(@?[$\w.]+)\s+by\s+(.+)$` |
|
|
98
103
|
| Assignment | `^(@?[$\w.@]+)\s*=(?!=\|>)\s*(.+)$` |
|
|
@@ -130,20 +135,53 @@ LiteralKeyword = "literal", " ", Literal ; (* parsed at the Expr level *)
|
|
|
130
135
|
| `PLAY0190` | A reducer rule body opens with an unknown word |
|
|
131
136
|
| `PLAY0191` | A read model is built by more than one projection or reducer |
|
|
132
137
|
| `PLAY0192` | A read model is declared more than once |
|
|
138
|
+
| `PLAY0295`–`PLAY0299` | An `$eventContext` path the event-context catalog does not have |
|
|
139
|
+
| `PLAY0380` | Chronicle drops part of a construct: `all` below the top level, or `automap` on a joined event |
|
|
140
|
+
| `PLAY0381` | A projection-level `key` routes no event |
|
|
141
|
+
| `PLAY0382`–`PLAY0385` | Variant errors: no `enters on`, shared mapping missing on a variant, duplicate variant name, entering event claimed twice |
|
|
142
|
+
| `PLAY0397` | Inline code uses a language line instead of a tagged fence |
|
|
143
|
+
| `PLAY0398` / `PLAY0399` | A reducer mixes bodied and body-less rules / observes one event twice |
|
|
144
|
+
|
|
145
|
+
## Syntax that parses but means something else
|
|
146
|
+
|
|
147
|
+
| Syntax | What Chronicle and the executable model do |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| `key` on the projection | Nothing: events route by the `from` key, then the event source id (`PLAY0381`) |
|
|
150
|
+
| `from X` with no key | Routes by the event source id, never by another `from`'s key |
|
|
151
|
+
| `join … on …` | Updates existing instances only; never creates one; the name after `join` is discarded |
|
|
152
|
+
| `automap` under a joined event | Replaced by the auto-map of the join's level (`PLAY0380`) |
|
|
153
|
+
| `all` inside `children`/`nested` | Behaves as `every` (`PLAY0380`) |
|
|
154
|
+
| `$causedBy.<x>` | Does not bind; use `$eventContext.causedBy.<x>` |
|
|
155
|
+
| Templates in mappings or keys | Do not bind |
|
|
156
|
+
| A number or Boolean literal key | Read as a property path; write `key literal "…"` |
|
|
157
|
+
| `parent` outside `children` | Ignored by Chronicle; does not bind |
|
|
158
|
+
| `sequence` | Realization concern; does not bind |
|
|
159
|
+
| Reducer rules | Opaque ESM v3 code keyed by the event source id; not computed by the reference runner |
|
|
160
|
+
| `$eventContext.<path>` other than `eventSourceId` in a mapping or key | Binds; the reference execution plan refuses it, so no specification in the model runs there |
|
|
161
|
+
| `remove via join` on a projection's own level | Chronicle's engine wires it as a child removal; binds, but the reference execution plan refuses it |
|
|
162
|
+
| `all` beside removals, `children` or `nested` | Binds, but the reference execution plan refuses it |
|
|
163
|
+
| `join`, `children` or `remove via join` inside `nested` | Not wired by Chronicle's engine; binds, but the reference execution plan refuses it |
|
|
133
164
|
|
|
134
165
|
## Worked examples
|
|
135
166
|
|
|
167
|
+
Each example is an excerpt: the events and read models it names are declared
|
|
168
|
+
elsewhere in the slice or model. Read-model properties in a `.play` model are
|
|
169
|
+
camelCase (a PascalCase property line is `PLAY0016`), so mapping targets are too.
|
|
170
|
+
Several examples map `$eventContext.occurred`, `sequenceNumber` or
|
|
171
|
+
`causedBy.subject`. Those bind, but the reference runner cannot execute a model
|
|
172
|
+
that contains them; a target must run their specifications.
|
|
173
|
+
|
|
136
174
|
### Composite key with event context
|
|
137
175
|
|
|
138
176
|
```screenplay
|
|
139
177
|
projection LineItems => LineItemReadModel
|
|
140
178
|
from LineItemAdded
|
|
141
179
|
key LineItemKey
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
180
|
+
orderId = orderId
|
|
181
|
+
lineNumber = lineNumber
|
|
182
|
+
sequenceNumber = $eventContext.sequenceNumber
|
|
183
|
+
createdBy = $eventContext.causedBy.subject
|
|
184
|
+
product = productName
|
|
147
185
|
```
|
|
148
186
|
|
|
149
187
|
### Global counter with a literal key
|
|
@@ -151,7 +189,7 @@ projection LineItems => LineItemReadModel
|
|
|
151
189
|
```screenplay
|
|
152
190
|
projection SiteStats => SiteStatsReadModel
|
|
153
191
|
from UserLoggedIn key literal "site-stats"
|
|
154
|
-
count
|
|
192
|
+
count totalLogins
|
|
155
193
|
lastLogin = $eventContext.occurred
|
|
156
194
|
```
|
|
157
195
|
|
|
@@ -168,23 +206,24 @@ projection ActivityFeed => ActivityFeedModel
|
|
|
168
206
|
recentUsers = name
|
|
169
207
|
```
|
|
170
208
|
|
|
171
|
-
`
|
|
172
|
-
on `UserRegistered`.
|
|
209
|
+
`all` subscribes to **every** event type in the system; `recentUsers` only fills
|
|
210
|
+
on `UserRegistered`. Chronicle keys the event types that only `all`
|
|
211
|
+
reaches by their **event source id**, so each event source gets its own instance
|
|
212
|
+
and its own count; `all` takes no key of its own.
|
|
173
213
|
|
|
174
214
|
### Children with a join and scoped removal
|
|
175
215
|
|
|
176
216
|
```screenplay
|
|
177
217
|
projection Group => GroupReadModel
|
|
178
218
|
from GroupCreated
|
|
179
|
-
|
|
219
|
+
name = name
|
|
180
220
|
children members identified by userId
|
|
181
221
|
from UserAddedToGroup key userId
|
|
182
222
|
parent groupId
|
|
183
|
-
|
|
184
|
-
join
|
|
223
|
+
role = role
|
|
224
|
+
join userName on userId
|
|
185
225
|
with UserCreated
|
|
186
|
-
|
|
187
|
-
UserName = name
|
|
226
|
+
userName = name
|
|
188
227
|
remove with UserRemovedFromGroup key userId
|
|
189
228
|
parent groupId
|
|
190
229
|
```
|
|
@@ -194,29 +233,30 @@ projection Group => GroupReadModel
|
|
|
194
233
|
```screenplay
|
|
195
234
|
projection Group => GroupReadModel
|
|
196
235
|
every
|
|
197
|
-
|
|
236
|
+
lastActivity = $eventContext.occurred
|
|
198
237
|
exclude children
|
|
199
238
|
from GroupCreated
|
|
200
|
-
|
|
239
|
+
name = name
|
|
201
240
|
children members identified by userId
|
|
202
|
-
from UserAdded
|
|
203
|
-
|
|
241
|
+
from UserAdded key userId
|
|
242
|
+
parent groupId
|
|
243
|
+
name = userName
|
|
204
244
|
```
|
|
205
245
|
|
|
206
|
-
Group-level events bump `
|
|
246
|
+
Group-level events bump `lastActivity`; member events do not.
|
|
207
247
|
|
|
208
248
|
### Nested nullable object
|
|
209
249
|
|
|
210
250
|
```screenplay
|
|
211
251
|
projection Slice => SliceReadModel
|
|
212
252
|
from SliceCreated
|
|
213
|
-
|
|
253
|
+
name = name
|
|
214
254
|
nested command
|
|
215
255
|
from CommandSetForSlice
|
|
216
|
-
|
|
217
|
-
|
|
256
|
+
name = commandName
|
|
257
|
+
schema = schema
|
|
218
258
|
from CommandRenamed
|
|
219
|
-
|
|
259
|
+
name = newName
|
|
220
260
|
clear with CommandClearedForSlice
|
|
221
261
|
```
|
|
222
262
|
|
|
@@ -227,6 +267,10 @@ projection Slice => SliceReadModel
|
|
|
227
267
|
|
|
228
268
|
```screenplay
|
|
229
269
|
slice StateView CustomerPortalReport
|
|
270
|
+
readmodel PortalReportReadModel
|
|
271
|
+
invitedAt DateTime
|
|
272
|
+
readmodel RevokedPortalTokenReadModel
|
|
273
|
+
revokedAt DateTime
|
|
230
274
|
query GetPortalReport => PortalReportReadModel
|
|
231
275
|
by customerId CustomerId
|
|
232
276
|
projection PortalReport => PortalReportReadModel
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
{ "kind": "skill-contains", "value": "AutoMap is on by default" },
|
|
9
9
|
{ "kind": "skill-contains", "value": "PLAY0191" },
|
|
10
10
|
{ "kind": "skill-contains", "value": "every event type in the system" },
|
|
11
|
-
{ "kind": "skill-contains", "value": "references/pdl-grammar.md" }
|
|
11
|
+
{ "kind": "skill-contains", "value": "references/pdl-grammar.md" },
|
|
12
|
+
{ "kind": "skill-contains", "value": "Do not write `key` on the projection." },
|
|
13
|
+
{ "kind": "skill-contains", "value": "**routes nothing** (`PLAY0381`)" },
|
|
14
|
+
{ "kind": "skill-contains", "value": "The key is **always the event source id**" },
|
|
15
|
+
{ "kind": "skill-contains", "value": "**A join never creates an" },
|
|
16
|
+
{ "kind": "skill-contains", "value": "Inline code uses a tagged fence" }
|
|
12
17
|
]
|
|
13
18
|
}
|