@cratis/pi 6.2.0 → 6.3.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.
- 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,22 +25,25 @@ 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` | CDL parser, reaction and trigger parsers, diagnostics, semantic binder |
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
`Documentation/screenplay/{captures,captures/grammar,reactions,triggers}.md`
|
|
32
|
-
|
|
33
|
-
behaves the same.
|
|
30
|
+
Checked against the Screenplay repository at tag `v4.31.0` (commit `355dffb`):
|
|
31
|
+
`Documentation/screenplay/{captures,captures/grammar,reactions,triggers,interactions,file-references,grammar,diagnostics}.md`
|
|
32
|
+
and decisions 0003, 0006 and 0009. Every example below compiles with that
|
|
33
|
+
version's compiler. Reverify before claiming another version behaves the same.
|
|
34
34
|
|
|
35
|
-
⚠️ **Neither slice type reaches the reference runtime.**
|
|
36
|
-
only `StateChange` and `StateView
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
⚠️ **Neither slice type reaches the reference runtime.** The executable model
|
|
36
|
+
admits only `StateChange` and `StateView` slices, so an `Automation` or
|
|
37
|
+
`Translate` slice fails binding with `PLAY0268` (*Slice '<name>' of type '<type>'
|
|
38
|
+
is not admitted by ESM v1*), and so do reactions and declared triggers. A
|
|
39
|
+
specification's `when append` never runs reactions either. Model them when the
|
|
40
|
+
`.play` file is the deliverable; do not promise they execute.
|
|
39
41
|
|
|
40
42
|
## `capture` — the Change Data Capture Language
|
|
41
43
|
|
|
42
44
|
Captures live in `Translate` slices and are the anti-corruption layer: external
|
|
43
|
-
shapes in, domain events out.
|
|
45
|
+
shapes in, domain events out. Excerpt: the module and feature around the slice,
|
|
46
|
+
and the events it appends, are declared elsewhere.
|
|
44
47
|
|
|
45
48
|
```screenplay
|
|
46
49
|
slice Translate LegacyInvoiceSync
|
|
@@ -79,6 +82,10 @@ convention, not enforcement.
|
|
|
79
82
|
|
|
80
83
|
**`key <property>`** names the source property identifying an instance.
|
|
81
84
|
|
|
85
|
+
Declaring an event's external origin on the `event` itself is accepted as
|
|
86
|
+
Screenplay decision 0009 but **not in the language**; a `capture` in a
|
|
87
|
+
`Translate` slice is how outside data enters a model today.
|
|
88
|
+
|
|
82
89
|
**`map`** reshapes before events are appended: direct rename
|
|
83
90
|
(`productName = name`), a backtick template, `translate` with indented
|
|
84
91
|
`"source" => target` entries, and `split <source> by "<sep>"` with indented target
|
|
@@ -107,6 +114,9 @@ an optional `map` and any number of `append` blocks.
|
|
|
107
114
|
|
|
108
115
|
## `reaction`
|
|
109
116
|
|
|
117
|
+
Excerpt: `BuildFinished` is a declared trigger and `SendFailureNotice` a command
|
|
118
|
+
elsewhere in the model.
|
|
119
|
+
|
|
110
120
|
```screenplay
|
|
111
121
|
slice Automation NotifyOnBuildFailure
|
|
112
122
|
reaction NotifyOnFailure
|
|
@@ -158,6 +168,8 @@ reaction is an error; combine with `and`/`or` instead.
|
|
|
158
168
|
|
|
159
169
|
### Effects — `produces` vs `invokes`
|
|
160
170
|
|
|
171
|
+
Excerpt, inside an `Automation` slice:
|
|
172
|
+
|
|
161
173
|
```screenplay
|
|
162
174
|
reaction Provisioner
|
|
163
175
|
when InvitationAccepted
|
|
@@ -174,7 +186,57 @@ refuse it. `invokes` asks for a command, which may still validate and reject.
|
|
|
174
186
|
Using `produces` for both would say those are the same kind of consequence.
|
|
175
187
|
|
|
176
188
|
Both are declarations of *what happens*, not of how — a trigger can state its
|
|
177
|
-
consequences **and** carry a `file` or inline block
|
|
189
|
+
consequences **and** carry a `file` or an inline block in a tagged fence
|
|
190
|
+
(` ```csharp `) that implements them. That code is an implementation attachment:
|
|
191
|
+
the model lists it (role, owner, content hash) but never runs it.
|
|
192
|
+
|
|
193
|
+
### What it decides from — trigger `reads`
|
|
194
|
+
|
|
195
|
+
An automation decides from state. A trigger names the views it consults with
|
|
196
|
+
`reads <View> [as <alias>] [by <trigger value>]` — the same word and meaning as
|
|
197
|
+
a command's `reads`: the state the behavior decides from. Excerpt, inside a
|
|
198
|
+
`feature`: the events, the command and the two read models (each built by a
|
|
199
|
+
projection) are declared elsewhere.
|
|
200
|
+
|
|
201
|
+
```screenplay
|
|
202
|
+
slice Automation ChaseOverdueInvoices
|
|
203
|
+
reaction RemindOnDueDate
|
|
204
|
+
description "Reminds the customer when an invoice falls due unpaid"
|
|
205
|
+
when InvoiceFellDue
|
|
206
|
+
invoiceId
|
|
207
|
+
reads InvoiceBalance as balance by invoiceId
|
|
208
|
+
invokes SendPaymentReminder
|
|
209
|
+
invoiceId = invoiceId
|
|
210
|
+
file Reactions/RemindOnDueDate.cs
|
|
211
|
+
reaction SweepOverdueInvoices
|
|
212
|
+
description "Re-checks every overdue invoice each morning"
|
|
213
|
+
at 08:00
|
|
214
|
+
reads OverdueInvoice
|
|
215
|
+
file Reactions/SweepOverdueInvoices.cs
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
- `reads` sits **under a trigger**, beside the values it takes and its effects.
|
|
219
|
+
Each trigger declares its own.
|
|
220
|
+
- **`by` names a value that trigger takes** (`invoiceId` above). Naming anything
|
|
221
|
+
else is warning `PLAY0442`.
|
|
222
|
+
- **Clock triggers read whole views.** `every` and `at` take no values, so
|
|
223
|
+
`reads <View>` works under them but `by` is error `PLAY0443`.
|
|
224
|
+
- **Aliases follow the command rules.** A view read more than once needs an
|
|
225
|
+
alias on every read (`PLAY0410`), aliases are unique per trigger (`PLAY0411`),
|
|
226
|
+
and an alias must not match a trigger value (`PLAY0412`). A view no projection
|
|
227
|
+
in the document produces is warning `PLAY0177`.
|
|
228
|
+
- **`reads` is a directive in a trigger body.** A trigger value named `reads` is
|
|
229
|
+
written `@reads`. A bare `reads` line is error `PLAY0175`, and
|
|
230
|
+
`reads <PrimitiveType>` is warning `PLAY0444`; both messages name the escape.
|
|
231
|
+
- `for each <View>` is reserved for a future view-driven trigger. It is not a
|
|
232
|
+
trigger today (`PLAY0137`).
|
|
233
|
+
|
|
234
|
+
⚠️ **Declared, not enforced.** Reactions still do not bind in the executable
|
|
235
|
+
model (`PLAY0268`), so trigger `reads` gives no runtime protection, and command
|
|
236
|
+
`reads` give none yet either (`PLAY0271`, decision 0003). A reaction that
|
|
237
|
+
`invokes` a command leaves the decision, and any protection, to that command's
|
|
238
|
+
own reads. Decision 0006 leaves `where` over read paths out of scope: keep the
|
|
239
|
+
logic that uses the state in the implementation, as both reactions above do.
|
|
178
240
|
|
|
179
241
|
## `trigger`
|
|
180
242
|
|
|
@@ -193,14 +255,23 @@ open. The type on a value is optional; a bare name is already a useful statement
|
|
|
193
255
|
**Name resolution, in order:** events the document declares or imports → triggers
|
|
194
256
|
the document declares → triggers registered with the compiler, plus the built-in
|
|
195
257
|
host signals. A registration wins over a built-in of the same name. Only when all
|
|
196
|
-
three miss is the name reported unknown —
|
|
197
|
-
|
|
258
|
+
three miss is the name reported unknown — a **warning** (`PLAY0248`), because the
|
|
259
|
+
name may be known outside the document. A malformed reaction body is an error.
|
|
198
260
|
|
|
199
261
|
**Built-in triggers:** `Startup` and `Shutdown`. The clock is built in too, but
|
|
200
262
|
with its own syntax rather than a name.
|
|
201
263
|
|
|
202
|
-
⚠️
|
|
203
|
-
|
|
264
|
+
⚠️ **An application trigger is not a UI event.** What a button click, a form
|
|
265
|
+
submit or entering a screen does is an *interaction*: an `on click` / `on submit`
|
|
266
|
+
/ `on enter` clause or a named `behavior`, never a declared `trigger` (see
|
|
267
|
+
`cratis-screenplay-ui-composition`). The two meet in two places: an interaction
|
|
268
|
+
can observe a declared trigger with `on <Trigger>` and fire one with
|
|
269
|
+
`raise <Trigger>`. A trigger named after a built-in interaction kind (`click`,
|
|
270
|
+
`submit`, `enter`, …) is an error (`PLAY0326`).
|
|
271
|
+
|
|
272
|
+
⚠️ Under a reaction trigger, `description`, `file`, `produces`, `invokes` and
|
|
273
|
+
`reads` are directives, so a value with one of those names is written with `@`
|
|
274
|
+
(`@file`, `@reads`).
|
|
204
275
|
|
|
205
276
|
**Registration** (for a name only an integration knows) happens through the
|
|
206
277
|
compiler's language registry, either name-only or with a shape. ⚠️ Stating **no**
|
|
@@ -232,6 +303,10 @@ something is reported. `Startup` and `Shutdown` are registered the second way.
|
|
|
232
303
|
- [ ] No `when` clause mixes `or` and `and`.
|
|
233
304
|
- [ ] Cross-cutting infrastructure is **not** modeled as a `Translate` slice.
|
|
234
305
|
- [ ] Every name a reaction takes is carried by the event or trigger.
|
|
306
|
+
- [ ] Each trigger `reads` the views its automation decides from; `by` names a
|
|
307
|
+
value the trigger takes, and clock triggers read without `by`.
|
|
308
|
+
- [ ] Nobody reports trigger or command `reads` as protected; UI clicks are
|
|
309
|
+
interactions, not triggers.
|
|
235
310
|
|
|
236
311
|
## Route near misses
|
|
237
312
|
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
{ "kind": "skill-contains", "value": "`cratis ai install` manages `.cratis/ai/`, not `.cratis/screenplay/`" },
|
|
8
8
|
{ "kind": "skill-contains", "value": "cannot be mixed in one clause" },
|
|
9
9
|
{ "kind": "skill-contains", "value": "`where` belongs to the reaction, not to one trigger" },
|
|
10
|
-
{ "kind": "skill-contains", "value": "Startup` and `Shutdown" }
|
|
10
|
+
{ "kind": "skill-contains", "value": "Startup` and `Shutdown" },
|
|
11
|
+
{ "kind": "skill-contains", "value": "`reads <View> [as <alias>] [by <trigger value>]`" },
|
|
12
|
+
{ "kind": "skill-contains", "value": "Clock triggers read whole views." },
|
|
13
|
+
{ "kind": "skill-contains", "value": "Declared, not enforced." },
|
|
14
|
+
{ "kind": "skill-contains", "value": "An application trigger is not a UI event" }
|
|
11
15
|
]
|
|
12
16
|
}
|
|
@@ -25,14 +25,22 @@ 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` | Parser, validator, diagnostics, semantic binder |
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
Checked against the Screenplay repository at tag `v4.31.0` (commit `355dffb`):
|
|
31
|
+
`Documentation/screenplay/{commands,constraints,policies,context,concepts,diagnostics}.md`
|
|
32
|
+
and decisions 0001 and 0003. Every example below compiles with that version's
|
|
33
|
+
compiler. Reverify before claiming another version behaves the same.
|
|
34
|
+
|
|
35
|
+
"Parses" and "runs" are different claims. The executable profile, and what
|
|
36
|
+
each construct binds to, is in the `cratis-screenplay-model-authoring` language
|
|
37
|
+
reference.
|
|
33
38
|
|
|
34
39
|
## The command block
|
|
35
40
|
|
|
41
|
+
Excerpt: the concepts, the `InvoiceLine` type, the policy and the event are
|
|
42
|
+
declared elsewhere in the model.
|
|
43
|
+
|
|
36
44
|
```screenplay
|
|
37
45
|
command RegisterInvoice
|
|
38
46
|
description "Registers a new invoice with its lines and payment terms"
|
|
@@ -40,33 +48,33 @@ command RegisterInvoice
|
|
|
40
48
|
invoiceNumber InvoiceNumber
|
|
41
49
|
lines InvoiceLine[]
|
|
42
50
|
note String?
|
|
43
|
-
reads InvoiceList by invoiceNumber
|
|
44
51
|
authorize CanManageInvoice
|
|
45
52
|
validate
|
|
46
53
|
invoiceNumber not empty message "Invoice number is required"
|
|
47
|
-
invoiceNumber matches "^INV-[0-9]{6}$" message "Must look like INV-000000"
|
|
54
|
+
invoiceNumber matches "^INV-[0-9]{6}$" severity warning message "Must look like INV-000000"
|
|
48
55
|
produces InvoiceRegistered
|
|
49
56
|
invoiceId = invoiceId
|
|
50
57
|
registeredAt = $context.occurred
|
|
51
|
-
concurrency
|
|
52
|
-
eventSource
|
|
53
|
-
events InvoiceRegistered, InvoiceCancelled
|
|
54
58
|
```
|
|
55
59
|
|
|
56
60
|
Type modifiers: `<Type>[]` for a collection, `<Type>?` for optional.
|
|
57
61
|
|
|
58
|
-
⚠️ **The parser enforces no clause order.** The order
|
|
59
|
-
|
|
62
|
+
⚠️ **The parser enforces no clause order.** The house order is description,
|
|
63
|
+
properties, `reads`, `authorize`, `validate`, `produces`/`handler`, `concurrency`.
|
|
64
|
+
Keep to it.
|
|
60
65
|
|
|
61
66
|
⚠️ **`produces` and `handler` are mutually exclusive** — declaring both is an
|
|
62
67
|
error. Everything else may repeat except `description` (one) and `concurrency`
|
|
63
|
-
(one).
|
|
68
|
+
(one). A `handler` parses but does not bind to the executable model yet
|
|
69
|
+
(`PLAY0268`); prefer `produces` when the model must run.
|
|
64
70
|
|
|
65
71
|
## `identifier` — the stream boundary decision
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
runtime
|
|
69
|
-
boundary** — the highest-consequence decision in the slice.
|
|
73
|
+
At most one command property may carry `identifier`. It names the value the
|
|
74
|
+
runtime uses as the event source id, so **choosing it is choosing the stream
|
|
75
|
+
boundary** — the highest-consequence decision in the slice. Leave it out only
|
|
76
|
+
when the runtime should allocate a fresh `Uuid` (a command that creates
|
|
77
|
+
something whose identity the caller does not supply).
|
|
70
78
|
|
|
71
79
|
- A second `identifier` on the same command is an error: *only one property can be
|
|
72
80
|
the identifier*.
|
|
@@ -75,44 +83,82 @@ boundary** — the highest-consequence decision in the slice.
|
|
|
75
83
|
|
|
76
84
|
## `reads` — state the command decides against
|
|
77
85
|
|
|
86
|
+
Excerpt: the `Account` read model, its projection and its keyed query are
|
|
87
|
+
declared elsewhere.
|
|
88
|
+
|
|
78
89
|
```screenplay
|
|
79
|
-
|
|
90
|
+
command TransferFunds
|
|
91
|
+
sourceId AccountId
|
|
92
|
+
destinationId AccountId
|
|
93
|
+
amount Decimal
|
|
94
|
+
reads Account as source by sourceId
|
|
95
|
+
reads Account as destination by destinationId
|
|
96
|
+
validate
|
|
97
|
+
require source.balance >= amount
|
|
98
|
+
message "The source account does not cover the transfer"
|
|
80
99
|
```
|
|
81
100
|
|
|
82
|
-
`by <property>` names the command property the read model is looked up by
|
|
83
|
-
|
|
84
|
-
|
|
101
|
+
`by <property>` names the command property the read model is looked up by.
|
|
102
|
+
`as <alias>` names one read: a command that reads a view more than once needs an
|
|
103
|
+
alias on **every** read (`PLAY0410`), aliases are unique (`PLAY0411`) and must not
|
|
104
|
+
match a command property (`PLAY0412`). A `require` path uses the alias, or the
|
|
105
|
+
view name when that view is read once.
|
|
85
106
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
107
|
+
⚠️ **`reads` is not a protected read.** It documents the read-model-to-command
|
|
108
|
+
arrow of the event model; nothing checks at append time that the state is still
|
|
109
|
+
current. The executable model rejects every `reads` and `concurrency` with
|
|
110
|
+
`PLAY0271`, and `require` over a read-model path with `PLAY0268`, until
|
|
111
|
+
decision-consistent reads exist (Screenplay #129, decision 0003). Adding a
|
|
112
|
+
`concurrency` block does not fix this: its scope is not the read's watermark, and
|
|
113
|
+
decision 0003 plans to make the combination an error once protected reads ship.
|
|
114
|
+
When a rule depends on state:
|
|
90
115
|
|
|
91
|
-
|
|
116
|
+
- if it is uniqueness, declare a `unique` constraint (below);
|
|
117
|
+
- otherwise state the rule, and say in review that the target implementation
|
|
118
|
+
must enforce it consistently. Do not claim the model guarantees it.
|
|
92
119
|
|
|
93
|
-
|
|
94
|
-
|
|
120
|
+
Do not use `reads` to fetch data the caller could supply.
|
|
121
|
+
|
|
122
|
+
## `validate`
|
|
123
|
+
|
|
124
|
+
Each rule takes an optional `severity information|warning|error` and then an
|
|
125
|
+
optional `message "<text>"` (or `$strings.<key>`). The default severity is `error`.
|
|
95
126
|
|
|
96
127
|
| Rule | Example |
|
|
97
128
|
| --- | --- |
|
|
98
129
|
| `not empty` | `name not empty` |
|
|
99
|
-
| `max <n>` / `min <n>` | `reason max 500
|
|
130
|
+
| `max <n>` / `min <n>` | `reason max 500` (length on text), `quantity min 1` (value on numbers) |
|
|
100
131
|
| `> <v>` / `>= <v>` / `< <v>` / `<= <v>` | `quantity > 0`, `discountPct <= 100` |
|
|
101
|
-
| `== <v>` / `!= <v>` | `currency == "NOK"`, `status !=
|
|
132
|
+
| `== <v>` / `!= <v>` | `currency == "NOK"`, `status != draft` |
|
|
102
133
|
| `length == <n>` | `currency length == 3` |
|
|
103
|
-
| `matches
|
|
134
|
+
| `matches email` | the only named pattern; any other name is `PLAY0366` |
|
|
135
|
+
| `matches "<regex>"` | ECMAScript; matches **any substring** unless anchored with `^…$`; an invalid pattern is `PLAY0367` |
|
|
104
136
|
| `all > <v>` / `all >= <v>` | `lines.quantity all > 0` |
|
|
105
137
|
| `rule <Name>` | `orgNumber rule BeAValidOrganizationNumber` |
|
|
106
138
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
139
|
+
⚠️ **Validation severity is not compiler severity.** Every failed rule and
|
|
140
|
+
`require` rejects the command, at `information` and `warning` too; severity only
|
|
141
|
+
tells the UI how to present the failure.
|
|
110
142
|
|
|
111
|
-
|
|
112
|
-
|
|
143
|
+
**Whole-command rules** use `require <condition>` with an indented `message` and
|
|
144
|
+
optional `severity`, sharing the condition grammar with `produces … when`:
|
|
145
|
+
`and` binds tighter than `or`, parentheses group. A conditional rule is an
|
|
146
|
+
implication: `require isExtension == false or newEndDate > endDate`.
|
|
113
147
|
|
|
114
|
-
**
|
|
115
|
-
|
|
148
|
+
**Rules whose logic is code.** A bare `rule <Name>` records that a rule exists but
|
|
149
|
+
has no portable meaning (`PLAY0268`). Give it a body when the logic can live in the
|
|
150
|
+
model — a `file` or a tagged ` ```csharp ` fence indented under the rule, or a
|
|
151
|
+
fenced `validate` block for cross-field rules. Excerpt, inside a command:
|
|
152
|
+
|
|
153
|
+
```screenplay
|
|
154
|
+
validate
|
|
155
|
+
orgNumber rule BeAValidOrganizationNumber message "Must be a valid organization number"
|
|
156
|
+
file Validations/BeAValidOrganizationNumber.cs
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
A bodied rule or fenced block binds as opaque code (ESM v3): the reference runner
|
|
160
|
+
reports it unsupported, and a target must supply the implementation. Prefer a
|
|
161
|
+
declarative rule when one can say it.
|
|
116
162
|
|
|
117
163
|
**Put format rules on the `concept`, not the command.** A concept carries its own
|
|
118
164
|
`validate` block and every use inherits it — that is Screenplay's type system, and
|
|
@@ -120,6 +166,8 @@ a rule that travels is worth more than one that is repeated.
|
|
|
120
166
|
|
|
121
167
|
## `authorize`
|
|
122
168
|
|
|
169
|
+
Excerpt: the policies are declared at the top of the model.
|
|
170
|
+
|
|
123
171
|
```screenplay
|
|
124
172
|
authorize IsAccountant
|
|
125
173
|
or IsCustomerSelf
|
|
@@ -133,21 +181,34 @@ A continuation line extends the clause.
|
|
|
133
181
|
⚠️ **Two adjacent policies synthesize an implicit `and`.** `authorize A B` means
|
|
134
182
|
`A and B`. Write the operator explicitly so the reader does not have to know this.
|
|
135
183
|
|
|
136
|
-
|
|
184
|
+
- Several `authorize` lines on one command or query combine with AND, in authored
|
|
185
|
+
order. (Before v4.29.0 all but the last were silently dropped.)
|
|
186
|
+
- `authorize` on an enclosing `module` or `feature` is ANDed with the command's
|
|
187
|
+
own gate; an `or` inside one gate never bypasses another gate.
|
|
188
|
+
- Evaluation short-circuits left to right, module → feature → command. If it
|
|
189
|
+
reaches a policy implemented in code, the reference runner reports the outcome
|
|
190
|
+
unsupported; it never guesses allow or deny.
|
|
137
191
|
|
|
138
|
-
|
|
139
|
-
(`= $context.occurred`, `= $context.identity.id`, `= $context.identity.claims.x`,
|
|
140
|
-
`= $context.causedBy.subject`, `= $context.causation.type`, `= $context.tenant`),
|
|
141
|
-
an environment variable (`= $env.REGION`), a literal (`= "draft"`, `= 0`), or a
|
|
142
|
-
template (`` = `${firstName} ${lastName}` ``).
|
|
192
|
+
## `produces`
|
|
143
193
|
|
|
144
|
-
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
194
|
+
- **Mapping sources** that bind to the executable model: a command property
|
|
195
|
+
(`= invoiceNumber`), a literal (`= "draft"`, `= 0`), `$context.occurred`, and
|
|
196
|
+
the caller's audit identity (`$context.identity.id`/`.name`/`.userName`, the
|
|
197
|
+
same values as `$context.causedBy.*`). The last two select ESM v2.
|
|
198
|
+
`$context.tenant`, claims, roles, causation, `$env.` values, templates and
|
|
199
|
+
computed expressions parse but block binding (`PLAY0268`).
|
|
200
|
+
- **`for <identifier>`** on an indented line names the event source the event is
|
|
201
|
+
appended to. At most one per `produces`. To bind, it must name the command's
|
|
202
|
+
`identifier` property (`PLAY0273` otherwise); fanning out to another event
|
|
203
|
+
source parses but does not run today.
|
|
204
|
+
- **`tag`** lines apply literal tags to this append. Tags also exist on the
|
|
205
|
+
`event` declaration, where they apply to every append of that type.
|
|
148
206
|
- **Several unconditional `produces` blocks** are allowed — that is co-production,
|
|
149
207
|
and it is what an automation is *not*.
|
|
150
|
-
- **`produces when <condition>`** takes the event name on the next indented line
|
|
208
|
+
- **`produces when <condition>`** takes the event name on the next indented line.
|
|
209
|
+
Conditions over command properties and constants bind; each is evaluated
|
|
210
|
+
independently, and when all are false the command is accepted with no events.
|
|
211
|
+
Excerpt:
|
|
151
212
|
|
|
152
213
|
```screenplay
|
|
153
214
|
produces when isProForma == true
|
|
@@ -158,7 +219,9 @@ produces when isProForma == true
|
|
|
158
219
|
## `concurrency`
|
|
159
220
|
|
|
160
221
|
Five dimensions, each at most once, and at most one `concurrency` block per
|
|
161
|
-
command
|
|
222
|
+
command. It mirrors Chronicle's `ConcurrencyScope` for a target implementation;
|
|
223
|
+
the executable model does not bind it (`PLAY0271`), and it does not protect a
|
|
224
|
+
`reads` decision.
|
|
162
225
|
|
|
163
226
|
| Dimension | Scopes the check to |
|
|
164
227
|
| --- | --- |
|
|
@@ -168,25 +231,43 @@ command:
|
|
|
168
231
|
| `streamId <Name>` | an event stream id |
|
|
169
232
|
| `events <A>, <B>` | the listed event types |
|
|
170
233
|
|
|
171
|
-
An unknown dimension is an error
|
|
234
|
+
An empty block or an unknown dimension is an error.
|
|
172
235
|
|
|
173
|
-
## `constraint` — append
|
|
236
|
+
## `constraint` — uniqueness at append time
|
|
174
237
|
|
|
175
|
-
|
|
238
|
+
Chronicle's constraints enforce **uniqueness only**. Excerpt: the events are
|
|
239
|
+
declared in the same model.
|
|
176
240
|
|
|
177
241
|
```screenplay
|
|
178
242
|
constraint UniqueInvoiceNumber
|
|
179
243
|
unique invoiceNumber on InvoiceRegistered
|
|
244
|
+
released by InvoiceCancelled
|
|
245
|
+
ignore casing
|
|
246
|
+
message "That invoice number is already in use"
|
|
180
247
|
|
|
181
248
|
constraint OneRegistrationPerInvoice
|
|
182
249
|
unique event InvoiceRegistered
|
|
183
|
-
|
|
184
|
-
constraint InvoiceStatusTransition
|
|
185
|
-
file Constraints/InvoiceStatusTransition.cs
|
|
186
250
|
```
|
|
187
251
|
|
|
188
|
-
|
|
189
|
-
|
|
252
|
+
- `unique <p>[, <p>…] on <Event>` — a value (or composite value, in order) held by
|
|
253
|
+
one event source is unavailable to every other. Repeat the line for other
|
|
254
|
+
events sharing the claim. The same event source may re-claim its own value; a
|
|
255
|
+
null value claims nothing. The property must be declared directly on the event
|
|
256
|
+
(`PLAY0391`).
|
|
257
|
+
- `unique event <Event>` — the event occurs at most once per event source.
|
|
258
|
+
- `released by <Event>` (repeatable) frees the claim; `ignore casing` applies to
|
|
259
|
+
property rules only (`PLAY0393`); `message` replaces the default violation text —
|
|
260
|
+
never put the colliding value in it.
|
|
261
|
+
- **The name is the identity.** It is unique across the application (`PLAY0392`),
|
|
262
|
+
and renaming a constraint starts a new, empty index.
|
|
263
|
+
- Chronicle updates the uniqueness index after the append commits; do not describe
|
|
264
|
+
it as an atomic index-and-append guarantee.
|
|
265
|
+
|
|
266
|
+
⚠️ **Do not use `constraint … file <Path>` for other rules.** It can only name a
|
|
267
|
+
hand-written Chronicle `IConstraint`, which can only declare uniqueness; it warns
|
|
268
|
+
(`PLAY0396`) and the executable model rejects it. A state transition rule
|
|
269
|
+
belongs in `validate`/`require`. Use a constraint when two concurrent appends must
|
|
270
|
+
not both win — a `validate` rule cannot do that.
|
|
190
271
|
|
|
191
272
|
## `concept` and `type`
|
|
192
273
|
|
|
@@ -209,7 +290,9 @@ The seven primitives are `Uuid`, `String`, `Int`, `Decimal`, `Bool`, `Date` and
|
|
|
209
290
|
is why the compiler says *expected … or Enum* rather than listing it among them.
|
|
210
291
|
Attributes `@pii` and `@sensitive`, each with at most one `reason`; a
|
|
211
292
|
reason for an attribute the concept does not declare is an error. **Compliance is
|
|
212
|
-
inherited** — a property typed with a `@pii` concept is PII everywhere.
|
|
293
|
+
inherited** — a property typed with a `@pii` concept is PII everywhere. The
|
|
294
|
+
executable model does not bind compliance attributes yet (`PLAY0268`); keep them,
|
|
295
|
+
because the classification is the point.
|
|
213
296
|
|
|
214
297
|
⚠️ **Enum trap.** A value literally named `validate` is read as an empty validate
|
|
215
298
|
block. Write `@validate` for the value; the compiler warns when it sees the
|
|
@@ -223,11 +306,15 @@ commands reference; use `concept` for a single wrapped primitive.
|
|
|
223
306
|
```screenplay
|
|
224
307
|
policy IsAuthenticated
|
|
225
308
|
require authenticated
|
|
309
|
+
policy IsAccountant
|
|
310
|
+
require role "Accountant"
|
|
226
311
|
policy CanManageInvoice
|
|
227
312
|
require role "InvoiceManager"
|
|
228
313
|
or role "Accountant"
|
|
229
314
|
policy OwnsInvoice
|
|
230
315
|
require claim "sub" matches subject
|
|
316
|
+
policy IsAdultCustomer
|
|
317
|
+
file Policies/IsAdultCustomer.cs
|
|
231
318
|
|
|
232
319
|
persona Accountant
|
|
233
320
|
description "Handles invoicing and collections"
|
|
@@ -235,11 +322,18 @@ persona Accountant
|
|
|
235
322
|
```
|
|
236
323
|
|
|
237
324
|
Three condition forms: `authenticated`, `role "<name>"`, and
|
|
238
|
-
`claim "<name>" matches subject | <value>`. A policy
|
|
239
|
-
condition
|
|
325
|
+
`claim "<name>" matches subject | "<value>" | <path>`. A policy has **exactly one**
|
|
326
|
+
`require` line — continue the condition on deeper-indented lines instead of adding
|
|
327
|
+
a second (`PLAY0441`) — **or** one implementation (a tagged ` ```csharp ` block or
|
|
328
|
+
`file`), never both (`PLAY0440`). Declarative policies run in the reference
|
|
329
|
+
runner; a code policy binds as opaque ESM v3 and needs a target to evaluate it.
|
|
330
|
+
A `persona` is documentation for people; it blocks executable binding
|
|
331
|
+
(`PLAY0268`).
|
|
240
332
|
|
|
241
333
|
## `seed`
|
|
242
334
|
|
|
335
|
+
Excerpt: `CustomerRegistered` is declared in a slice.
|
|
336
|
+
|
|
243
337
|
```screenplay
|
|
244
338
|
seed
|
|
245
339
|
for "3fa85f64-5717-4562-b3fc-2c963f66afa6"
|
|
@@ -248,7 +342,8 @@ seed
|
|
|
248
342
|
```
|
|
249
343
|
|
|
250
344
|
Events append to that event source in declaration order, using the same mapping
|
|
251
|
-
expression grammar as `produces`. Several `seed` blocks accumulate.
|
|
345
|
+
expression grammar as `produces`. Several `seed` blocks accumulate. Seeding is
|
|
346
|
+
operational metadata, not part of executable behavior (`PLAY0270`).
|
|
252
347
|
|
|
253
348
|
## `$context`
|
|
254
349
|
|
|
@@ -266,10 +361,13 @@ Four contexts, and **what each omits is load-bearing** — read
|
|
|
266
361
|
## Verify
|
|
267
362
|
|
|
268
363
|
- [ ] `screenplay <model> --warnaserror` reports zero errors and zero warnings.
|
|
269
|
-
- [ ]
|
|
364
|
+
- [ ] At most one command property carries `identifier`, and no event property does.
|
|
270
365
|
- [ ] Format rules live on the `concept`; state-dependent rules are specifications.
|
|
271
366
|
- [ ] Every `authorize` combining policies writes `and`/`or` explicitly.
|
|
272
|
-
- [ ]
|
|
367
|
+
- [ ] No `reads` or `concurrency` is described as protecting a decision; the gap
|
|
368
|
+
is stated.
|
|
369
|
+
- [ ] Constraints are `unique` forms; no `file` constraint stands in for another rule.
|
|
370
|
+
- [ ] Each policy has one `require` or one implementation, not both.
|
|
273
371
|
- [ ] Personal data is `@pii` on the concept, with a reason.
|
|
274
372
|
- [ ] No event carries an optional property covering two situations.
|
|
275
373
|
|
|
@@ -277,5 +375,5 @@ Four contexts, and **what each omits is load-bearing** — read
|
|
|
277
375
|
|
|
278
376
|
- Deciding *which* commands and events exist: `cratis-screenplay-event-modeling`.
|
|
279
377
|
- Building read models from these events: `cratis-screenplay-projections`.
|
|
280
|
-
- Pinning the rejections: `cratis-screenplay-specifications`.
|
|
281
|
-
- The
|
|
378
|
+
- Pinning the rejections and denials: `cratis-screenplay-specifications`.
|
|
379
|
+
- The parsed/bound/executed boundary: `cratis-screenplay-model-authoring`.
|
|
@@ -32,8 +32,13 @@ public record PolicyContext(
|
|
|
32
32
|
| **Policy** | a `policy` code block | **`CausedBy`, `Causation`** | a decision about the caller, not an audit record |
|
|
33
33
|
|
|
34
34
|
`RuleContext` carries `Artifact` (the whole thing under validation), `Value` (the
|
|
35
|
-
value the rule is declared on) and `Property` (where it sits — empty for a
|
|
36
|
-
|
|
35
|
+
value the rule is declared on) and `Property` (where it sits — empty for a fenced
|
|
36
|
+
`validate` block). A `require` condition is declarative and gets no code context.
|
|
37
|
+
|
|
38
|
+
Beside every `dynamic` member there is a typed accessor so LINQ binds at compile
|
|
39
|
+
time: `CommandAs<T>()`, `ArgumentsAs<T>()`, `ArtifactAs<T>()`, `ValueAs<T>()`, and
|
|
40
|
+
`StateAs<T>()`/`EventAs<T>()` on the reducer context. A payload of another type
|
|
41
|
+
throws `ContextPayloadTypeMismatch`.
|
|
37
42
|
|
|
38
43
|
## The values they carry
|
|
39
44
|
|
|
@@ -50,8 +55,12 @@ with an appended event.
|
|
|
50
55
|
|
|
51
56
|
## Reaching the context declaratively
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
`append` mappings, query `from` parameters
|
|
58
|
+
Parsed wherever a mapping source is — `produces` mappings, `seed` values, capture
|
|
59
|
+
`append` mappings, query `from` parameters. **Only a narrow subset binds to the
|
|
60
|
+
executable model in `produces`:** `$context.occurred` (the occurrence time, not a
|
|
61
|
+
guaranteed append time) and the audit identity `$context.identity.id`/`.name`/
|
|
62
|
+
`.userName` (equal to `$context.causedBy.subject`/`.name`/`.userName`). Tenant,
|
|
63
|
+
roles, claims and causation report `PLAY0268`.
|
|
55
64
|
|
|
56
65
|
| Path | Yields |
|
|
57
66
|
| --- | --- |
|
|
@@ -76,13 +85,15 @@ An unknown `$context.` path is reported, so a typo does not silently become null
|
|
|
76
85
|
| `$env.<VAR_NAME>` | mapping sources | an environment variable |
|
|
77
86
|
| `$eventContext.<property>` | **projections only** | `occurred`, `sequenceNumber`, `correlationId`, `eventSourceId` |
|
|
78
87
|
| `$eventSourceId` | **projections only** | shorthand for `$eventContext.eventSourceId` |
|
|
79
|
-
| `$causedBy.<property>` | **projections only** | `subject`, `name`, `userName
|
|
88
|
+
| `$causedBy.<property>` | **projections only** | `subject`, `name`, `userName`; parses, but does not bind (`PLAY0268`) — write `$eventContext.causedBy.subject` instead |
|
|
80
89
|
| `$.` | **captures only** | a value from the current source item |
|
|
81
90
|
| `$strings.<dotted.key>` | labels, titles, messages | a localized string from a `.strings` file |
|
|
82
91
|
|
|
83
92
|
## Templates and literals
|
|
84
93
|
|
|
85
|
-
A template is backticked with `${}` substitutions
|
|
94
|
+
A template is backticked with `${}` substitutions. It parses in mappings but does
|
|
95
|
+
not bind to the executable model in `produces` or projections. Excerpt, one
|
|
96
|
+
mapping line inside a `produces` block:
|
|
86
97
|
|
|
87
98
|
```screenplay
|
|
88
99
|
fullName = `${firstName} ${lastName}`
|
|
@@ -90,11 +101,12 @@ fullName = `${firstName} ${lastName}`
|
|
|
90
101
|
|
|
91
102
|
Literals are `true` / `false`, `"quoted text"`, numbers (`42`, `-3.14`), and
|
|
92
103
|
`null`. In a projection, `literal <value>` forces a value to be read as a literal
|
|
93
|
-
rather than a property path — that is how a constant key is written
|
|
104
|
+
rather than a property path — that is how a constant key is written. Excerpt,
|
|
105
|
+
inside a `projection`:
|
|
94
106
|
|
|
95
107
|
```screenplay
|
|
96
108
|
from UserLoggedIn key literal "site-stats"
|
|
97
|
-
count
|
|
109
|
+
count totalLogins
|
|
98
110
|
```
|
|
99
111
|
|
|
100
112
|
⚠️ A template expression is **not allowed in a composite key**, and a composite
|