@cratis/pi 2.23.1 → 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.
- package/package/corpus/rules/general.md +2 -0
- package/package/corpus/rules/profiles.md +18 -1
- package/package/corpus/skills/cratis-chronicle-event-modeling/SKILL.md +8 -0
- package/package/corpus/skills/cratis-event-model-diagram/SKILL.md +7 -0
- package/package/corpus/skills/cratis-screenplay-captures-and-reactions/LICENSE +2 -0
- package/package/corpus/skills/cratis-screenplay-captures-and-reactions/SKILL.md +229 -0
- package/package/corpus/skills/cratis-screenplay-captures-and-reactions/verification.json +10 -0
- package/package/corpus/skills/cratis-screenplay-command-surface/LICENSE +2 -0
- package/package/corpus/skills/cratis-screenplay-command-surface/SKILL.md +270 -0
- package/package/corpus/skills/cratis-screenplay-command-surface/references/context.md +111 -0
- package/package/corpus/skills/cratis-screenplay-command-surface/verification.json +23 -0
- package/package/corpus/skills/cratis-screenplay-event-modeling/LICENSE +2 -0
- package/package/corpus/skills/cratis-screenplay-event-modeling/SKILL.md +204 -0
- package/package/corpus/skills/cratis-screenplay-event-modeling/references/nine-steps.md +219 -0
- package/package/corpus/skills/cratis-screenplay-event-modeling/verification.json +23 -0
- package/package/corpus/skills/cratis-screenplay-model-authoring/SKILL.md +13 -1
- package/package/corpus/skills/cratis-screenplay-projections/LICENSE +2 -0
- package/package/corpus/skills/cratis-screenplay-projections/SKILL.md +215 -0
- package/package/corpus/skills/cratis-screenplay-projections/references/pdl-grammar.md +239 -0
- package/package/corpus/skills/cratis-screenplay-projections/verification.json +11 -0
- package/package/corpus/skills/cratis-screenplay-read-surface/LICENSE +2 -0
- package/package/corpus/skills/cratis-screenplay-read-surface/SKILL.md +188 -0
- package/package/corpus/skills/cratis-screenplay-read-surface/verification.json +10 -0
- package/package/corpus/skills/cratis-screenplay-specifications/LICENSE +2 -0
- package/package/corpus/skills/cratis-screenplay-specifications/SKILL.md +199 -0
- package/package/corpus/skills/cratis-screenplay-specifications/verification.json +19 -0
- package/package/corpus/skills/cratis-screenplay-ui-composition/LICENSE +2 -0
- package/package/corpus/skills/cratis-screenplay-ui-composition/SKILL.md +231 -0
- package/package/corpus/skills/cratis-screenplay-ui-composition/verification.json +10 -0
- package/package/corpus/skills/cratis-stage-rendering-and-sandbox/SKILL.md +5 -2
- package/package/profile-catalog.json +8 -1
- package/package.json +1 -1
|
@@ -147,6 +147,8 @@ A claim is only as good as the signal behind it — a build result, a test run,
|
|
|
147
147
|
| Writing a scan, allowlist or destructive pass that cannot pass vacuously | `guards-and-fuses.md` |
|
|
148
148
|
| How the shared corpus is installed, updated and rolled back | `ai-distribution.md` |
|
|
149
149
|
| Event modeling / schema migration / calling commands from code / paging / cross-cutting metadata / multi-tenancy | the matching skills |
|
|
150
|
+
| Designing an information system, business process or information flow as a **Screenplay** `.play` model | the **cratis-screenplay-event-modeling** skill, then the per-surface `cratis-screenplay-*` skills |
|
|
151
|
+
| Rendering a settled `.play` model into an application | the **cratis-stage-rendering-and-sandbox** skill |
|
|
150
152
|
| Step-by-step recipes | `.cratis/ai/skills/` |
|
|
151
153
|
|
|
152
154
|
## Source-of-Truth Discipline
|
|
@@ -98,8 +98,25 @@ The complete list of available profiles is defined in [profile-catalog.json](../
|
|
|
98
98
|
| `cratis/studio` | Studio MCP safety guidance |
|
|
99
99
|
| `cratis/cli` | CLI operations |
|
|
100
100
|
| `cratis/lens` | Lens browser extension |
|
|
101
|
-
| `cratis/screenplay` | Screenplay
|
|
101
|
+
| `cratis/screenplay` | Event modeling and information-system design with Screenplay — the method and the whole `.play` language |
|
|
102
102
|
| `cratis/stage` | Stage rendering and sandbox |
|
|
103
|
+
| `cratis/modeling/screenplay-stage` | Screenplay + Stage together |
|
|
104
|
+
|
|
105
|
+
### Event modeling with Screenplay
|
|
106
|
+
|
|
107
|
+
`cratis/screenplay` carries the **method** and the **language**, split one skill
|
|
108
|
+
per surface so only the relevant one loads:
|
|
109
|
+
|
|
110
|
+
| Skill | Covers |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| `cratis-screenplay-event-modeling` | Domain discovery, the nine-step workflow, the four slice types, model validation |
|
|
113
|
+
| `cratis-screenplay-command-surface` | `command`, `event`, `validate`, `authorize`, `produces`, `concurrency`, `constraint`, `concept`, `$context` |
|
|
114
|
+
| `cratis-screenplay-projections` | The Projection Declaration Language and the `reducer` escape hatch |
|
|
115
|
+
| `cratis-screenplay-read-surface` | `readmodel`, `query`, `screen`, name resolution |
|
|
116
|
+
| `cratis-screenplay-ui-composition` | `layout`, templates, `form`, `contribute`, `ui profile`, `theme`, `$strings`, `file` |
|
|
117
|
+
| `cratis-screenplay-captures-and-reactions` | The Change Data Capture Language, `reaction`, `trigger` |
|
|
118
|
+
| `cratis-screenplay-specifications` | Given/when/then and the reference execution |
|
|
119
|
+
| `cratis-screenplay-model-authoring` | The compiler, the CLI, diagnostics, and the parsed-versus-admitted boundary |
|
|
103
120
|
|
|
104
121
|
## How to Use Profiles
|
|
105
122
|
|
|
@@ -155,3 +155,11 @@ remain separately authorized; a ready model authorizes none of them.
|
|
|
155
155
|
- Personal data has a decided subject, and the subject is person-level or the
|
|
156
156
|
trade-off is surfaced.
|
|
157
157
|
- The specification outline names the failures, not only the happy path.
|
|
158
|
+
|
|
159
|
+
## Route near misses
|
|
160
|
+
|
|
161
|
+
- Modeling the same system as a **Screenplay `.play` document** rather than as a
|
|
162
|
+
brief for hand-written C#: `cratis-screenplay-event-modeling`. The method is the
|
|
163
|
+
same; the artifact is a file that compiles, and the four behavior types above
|
|
164
|
+
map onto its `StateChange`, `StateView`, `Automation` and `Translate` slices.
|
|
165
|
+
- Drawing a decided model as a diagram: `cratis-event-model-diagram`.
|
|
@@ -140,3 +140,10 @@ language. Include commands that emit no event.
|
|
|
140
140
|
- The command-rules table covers every command, including those that emit
|
|
141
141
|
nothing.
|
|
142
142
|
- Cross-module flows are reflected in all three places.
|
|
143
|
+
|
|
144
|
+
## Route near misses
|
|
145
|
+
|
|
146
|
+
- Deciding the model rather than drawing one: `cratis-chronicle-event-modeling`
|
|
147
|
+
for a hand-written Chronicle implementation, or
|
|
148
|
+
`cratis-screenplay-event-modeling` when the model is authored as a Screenplay
|
|
149
|
+
`.play` document.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cratis-screenplay-captures-and-reactions
|
|
3
|
+
description: Model the reactive edges of a Cratis Screenplay `.play` model — `capture` in the Change Data Capture Language for turning external data into events, and `reaction` plus `trigger` for work that runs when something happens, on a schedule, or on an integration signal. Use when integrating an external system, translating outside data into events, or declaring automatic follow-up work in a `.play` model. Do not use for commands, projections or screens.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Captures and reactions
|
|
8
|
+
|
|
9
|
+
The two slice types that run without anyone pressing a button. A `capture` in a
|
|
10
|
+
`Translate` slice turns outside data into events; a `reaction` in an `Automation`
|
|
11
|
+
slice runs when something happens.
|
|
12
|
+
|
|
13
|
+
## Verified product sources
|
|
14
|
+
|
|
15
|
+
| Package | Version | Purpose |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `Cratis.Screenplay` | `4.12.1` | CDL parser, reaction and trigger parsers, diagnostics |
|
|
18
|
+
|
|
19
|
+
Read from the Screenplay repository at tag `v4.12.1` (commit `122eee8`), against
|
|
20
|
+
`Documentation/screenplay/{captures,captures/grammar,reactions,triggers}.md` and
|
|
21
|
+
`Source/DotNET/Screenplay/Parsing/`. Reverify before claiming another version
|
|
22
|
+
behaves the same.
|
|
23
|
+
|
|
24
|
+
⚠️ **Neither slice type reaches the reference runtime.** `SemanticSliceKind` admits
|
|
25
|
+
only `StateChange` and `StateView`, so an `Automation` or `Translate` slice is
|
|
26
|
+
rejected by ESM v1 outright. Model them when the `.play` file is the deliverable;
|
|
27
|
+
do not promise they execute.
|
|
28
|
+
|
|
29
|
+
## `capture` — the Change Data Capture Language
|
|
30
|
+
|
|
31
|
+
Captures live in `Translate` slices and are the anti-corruption layer: external
|
|
32
|
+
shapes in, domain events out.
|
|
33
|
+
|
|
34
|
+
```screenplay
|
|
35
|
+
slice Translate LegacyInvoiceSync
|
|
36
|
+
capture LegacyInvoiceCapture
|
|
37
|
+
source api
|
|
38
|
+
api LegacyInvoicingApi
|
|
39
|
+
route /invoices
|
|
40
|
+
poll 5m
|
|
41
|
+
key id
|
|
42
|
+
map
|
|
43
|
+
status = status translate
|
|
44
|
+
"utkast" => draft
|
|
45
|
+
"sendt" => sent
|
|
46
|
+
"betalt" => paid
|
|
47
|
+
append InvoiceStatusChanged
|
|
48
|
+
tag legacy
|
|
49
|
+
when status
|
|
50
|
+
invoiceId = $.id
|
|
51
|
+
status = $.status
|
|
52
|
+
changedAt = $context.occurred
|
|
53
|
+
append InvoicePaidFromSent
|
|
54
|
+
when status from "sent" to "paid"
|
|
55
|
+
invoiceId = $.id
|
|
56
|
+
children lineItems identified by lineNumber
|
|
57
|
+
append InvoiceLineItemAdded
|
|
58
|
+
when added
|
|
59
|
+
invoiceId = $.id
|
|
60
|
+
lineNumber = $.lineNumber
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**`source <kind>`** with indented settings. The documented kinds and settings are
|
|
64
|
+
`api` (`api`, `route`, `poll`), `webhook` (`path`) and `message` (`topic`).
|
|
65
|
+
⚠️ The grammar is **open** — the parser accepts any kind and any setting name as
|
|
66
|
+
free-form name/value pairs, so a typo is not caught. The three kinds are
|
|
67
|
+
convention, not enforcement.
|
|
68
|
+
|
|
69
|
+
**`key <property>`** names the source property identifying an instance.
|
|
70
|
+
|
|
71
|
+
**`map`** reshapes before events are appended: direct rename
|
|
72
|
+
(`productName = name`), a backtick template, `translate` with indented
|
|
73
|
+
`"source" => target` entries, and `split <source> by "<sep>"` with indented target
|
|
74
|
+
properties.
|
|
75
|
+
|
|
76
|
+
**Mapping sources:** `$.` for a value from the current source item, `$context.` for
|
|
77
|
+
capture context, `$env.` for an environment variable, plus literals and templates.
|
|
78
|
+
|
|
79
|
+
**`append <Event>`** with optional `tag` lines and one `when` clause:
|
|
80
|
+
|
|
81
|
+
| `when` form | Appends when |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| `when added` | an item appears in the source |
|
|
84
|
+
| `when removed` | an item disappears from the source |
|
|
85
|
+
| `when <Path>` | that property changes |
|
|
86
|
+
| `when <Path> from <v> to <v>` | that property makes that exact transition |
|
|
87
|
+
| `when <a> or <b> [or <c>]` | any of those properties change |
|
|
88
|
+
| `when <a> and <b> [and <c>]` | all of those properties change |
|
|
89
|
+
| ``when `<expression>` `` | a raw template expression is true (captured verbatim, unparsed) |
|
|
90
|
+
|
|
91
|
+
⚠️ **`or` and `and` cannot be mixed in one clause** — `when a or b and c` is a
|
|
92
|
+
compile error. Split it into two `append` blocks.
|
|
93
|
+
|
|
94
|
+
**`children <collection> identified by <path>`** and **`nested <path>`** each take
|
|
95
|
+
an optional `map` and any number of `append` blocks.
|
|
96
|
+
|
|
97
|
+
## `reaction`
|
|
98
|
+
|
|
99
|
+
```screenplay
|
|
100
|
+
slice Automation NotifyOnBuildFailure
|
|
101
|
+
reaction NotifyOnFailure
|
|
102
|
+
description "Tells the owning team when a watched build fails"
|
|
103
|
+
when BuildFinished
|
|
104
|
+
repository
|
|
105
|
+
outcome
|
|
106
|
+
invokes SendFailureNotice
|
|
107
|
+
repository = repository
|
|
108
|
+
where outcome == "failed"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
A reaction declares **at least one trigger**; everything under a trigger is
|
|
112
|
+
optional.
|
|
113
|
+
|
|
114
|
+
⚠️ **Indentation decides what is a trigger and what is an effect.** `produces`,
|
|
115
|
+
`invokes`, `file` and inline code belong **inside** the trigger block, indented
|
|
116
|
+
under `when`/`every`/`at`. Only `description` and `where` sit at reaction level.
|
|
117
|
+
Outdenting an effect gives *Expected a trigger in reaction body, got 'invokes …'*
|
|
118
|
+
(`PLAY0137`) — the compiler is looking for another trigger where the effect is.
|
|
119
|
+
|
|
120
|
+
### What sets it off
|
|
121
|
+
|
|
122
|
+
| Form | Runs |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| `when <Name>` | on that event, declared trigger, or registered trigger |
|
|
125
|
+
| `every <n> seconds\|minutes\|hours\|days` | on that interval (`n` ≥ 1) |
|
|
126
|
+
| `at HH:mm` | every day at that time |
|
|
127
|
+
| `at HH:mm on <Weekday>` | every week on that day |
|
|
128
|
+
| `at HH:mm on day <n>` | every month on that day (`n` is 1–31) |
|
|
129
|
+
|
|
130
|
+
Time is strictly `HH:mm`. Weekday is the full English name.
|
|
131
|
+
|
|
132
|
+
### The values a reaction takes
|
|
133
|
+
|
|
134
|
+
A bare name under a trigger says the reaction **uses** that value from the
|
|
135
|
+
occurrence. This is a **selection, not a declaration** — the shape belongs to the
|
|
136
|
+
event or trigger. Taking a value the occurrence does not carry is reported.
|
|
137
|
+
|
|
138
|
+
### `where` — narrowing
|
|
139
|
+
|
|
140
|
+
`where` uses the same condition grammar as `produces … when` and `require`:
|
|
141
|
+
`==`, `!=`, the ordering operators, `contains` for a substring anywhere,
|
|
142
|
+
`starts with` for one at the beginning, combined with `and`, `or` and parentheses.
|
|
143
|
+
|
|
144
|
+
⚠️ **`where` belongs to the reaction, not to one trigger** — it says which
|
|
145
|
+
occurrences are worth running for, whatever set them off. A second `where` on one
|
|
146
|
+
reaction is an error; combine with `and`/`or` instead.
|
|
147
|
+
|
|
148
|
+
### Effects — `produces` vs `invokes`
|
|
149
|
+
|
|
150
|
+
```screenplay
|
|
151
|
+
reaction Provisioner
|
|
152
|
+
when InvitationAccepted
|
|
153
|
+
workspaceId
|
|
154
|
+
produces WorkspaceProvisioned
|
|
155
|
+
for workspaceId
|
|
156
|
+
workspaceId = workspaceId
|
|
157
|
+
invokes SendWelcomeMail
|
|
158
|
+
workspaceId = workspaceId
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**The different word is the point.** `produces` appends a fact, and nothing can
|
|
162
|
+
refuse it. `invokes` asks for a command, which may still validate and reject.
|
|
163
|
+
Using `produces` for both would say those are the same kind of consequence.
|
|
164
|
+
|
|
165
|
+
Both are declarations of *what happens*, not of how — a trigger can state its
|
|
166
|
+
consequences **and** carry a `file` or inline block that implements them.
|
|
167
|
+
|
|
168
|
+
## `trigger`
|
|
169
|
+
|
|
170
|
+
```screenplay
|
|
171
|
+
trigger BuildFinished
|
|
172
|
+
description "CI reported a finished build on a watched repository"
|
|
173
|
+
repository Repository
|
|
174
|
+
outcome
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
A trigger declares two things: **that the name exists**, and **what an occurrence
|
|
178
|
+
hands the reaction**. It deliberately declares nothing about what makes one occur —
|
|
179
|
+
that belongs to whatever produces it, and keeping it out is what lets the set stay
|
|
180
|
+
open. The type on a value is optional; a bare name is already a useful statement.
|
|
181
|
+
|
|
182
|
+
**Name resolution, in order:** events the document declares or imports → triggers
|
|
183
|
+
the document declares → triggers registered with the compiler, plus the built-in
|
|
184
|
+
host signals. A registration wins over a built-in of the same name. Only when all
|
|
185
|
+
three miss is the name reported unknown — as a **warning**, like every other
|
|
186
|
+
unresolved reference.
|
|
187
|
+
|
|
188
|
+
**Built-in triggers:** `Startup` and `Shutdown`. The clock is built in too, but
|
|
189
|
+
with its own syntax rather than a name.
|
|
190
|
+
|
|
191
|
+
⚠️ A trigger body reserves `file`, so a trigger value named `file` is written
|
|
192
|
+
`@file`.
|
|
193
|
+
|
|
194
|
+
**Registration** (for a name only an integration knows) happens through the
|
|
195
|
+
compiler's language registry, either name-only or with a shape. ⚠️ Stating **no**
|
|
196
|
+
values and stating **none** are different claims: a definition with no value list
|
|
197
|
+
says the registration does not describe the shape, so what a reaction takes is left
|
|
198
|
+
alone; one with an **empty** list says an occurrence carries nothing, so taking
|
|
199
|
+
something is reported. `Startup` and `Shutdown` are registered the second way.
|
|
200
|
+
|
|
201
|
+
## Guidance, verbatim
|
|
202
|
+
|
|
203
|
+
> - Reactions that only translate events into other events belong in `Translate`
|
|
204
|
+
> slices when driven by external data (captures); event-to-event automation stays
|
|
205
|
+
> in `Automation` slices.
|
|
206
|
+
> - Keep reaction logic small; anything substantial belongs in a `file` reference
|
|
207
|
+
> where it can be tested on its own.
|
|
208
|
+
> - Describe the reaction before you implement it — a document full of `file` lines
|
|
209
|
+
> and nothing else tells a reader nothing.
|
|
210
|
+
> - A name that only an integration knows belongs in a `trigger` declaration, so the
|
|
211
|
+
> document says what the reaction is handed rather than leaving the reader to guess.
|
|
212
|
+
|
|
213
|
+
## Verify
|
|
214
|
+
|
|
215
|
+
- [ ] `screenplay <model> --warnaserror` reports zero errors and zero warnings.
|
|
216
|
+
- [ ] Each automation has all four components — occurrence, state consulted,
|
|
217
|
+
conditional logic, resulting command or event. If it always fires
|
|
218
|
+
unconditionally it is co-production, not an automation.
|
|
219
|
+
- [ ] Every reaction has a termination condition and cannot loop forever.
|
|
220
|
+
- [ ] `produces` is used for facts and `invokes` for intents, never interchangeably.
|
|
221
|
+
- [ ] No `when` clause mixes `or` and `and`.
|
|
222
|
+
- [ ] Cross-cutting infrastructure is **not** modeled as a `Translate` slice.
|
|
223
|
+
- [ ] Every name a reaction takes is carried by the event or trigger.
|
|
224
|
+
|
|
225
|
+
## Route near misses
|
|
226
|
+
|
|
227
|
+
- Deciding whether a behavior is an automation at all: `cratis-screenplay-event-modeling`.
|
|
228
|
+
- The commands a reaction invokes: `cratis-screenplay-command-surface`.
|
|
229
|
+
- Hand-written C# Chronicle reactors: `cratis-chronicle-reactor`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill": "cratis-screenplay-captures-and-reactions",
|
|
3
|
+
"input": "Translate external data into events and declare a reaction.",
|
|
4
|
+
"fixtures": [],
|
|
5
|
+
"assertions": [
|
|
6
|
+
{ "kind": "skill-contains", "value": "cannot be mixed in one clause" },
|
|
7
|
+
{ "kind": "skill-contains", "value": "`where` belongs to the reaction, not to one trigger" },
|
|
8
|
+
{ "kind": "skill-contains", "value": "Startup` and `Shutdown" }
|
|
9
|
+
]
|
|
10
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cratis-screenplay-command-surface
|
|
3
|
+
description: Write the write side of a Cratis Screenplay `.play` model — the `command` block and its `identifier`, `reads`, `validate`, `authorize`, `produces`, `handler` and `concurrency` clauses, plus `event`, `constraint`, `policy`, `persona`, `concept`, `type` and `seed`. Use when declaring or changing a command, an event shape, a validation or authorization rule, an append-time constraint, or a strongly-typed value in Screenplay. Do not use for projections, queries, screens, captures or reactions.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# The Screenplay write surface
|
|
8
|
+
|
|
9
|
+
Everything that changes the system: the `command` that expresses intent, the
|
|
10
|
+
`event` it appends, the rules that can refuse it, and the strongly-typed values
|
|
11
|
+
they are all built from.
|
|
12
|
+
|
|
13
|
+
## Verified product sources
|
|
14
|
+
|
|
15
|
+
| Package | Version | Purpose |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `Cratis.Screenplay` | `4.12.1` | Parser, validator, diagnostics |
|
|
18
|
+
|
|
19
|
+
Read from the Screenplay repository at tag `v4.12.1` (commit `122eee8`), against
|
|
20
|
+
both the documentation and `Source/DotNET/Screenplay/Parsing/`. Reverify before
|
|
21
|
+
claiming another version behaves the same.
|
|
22
|
+
|
|
23
|
+
## The command block
|
|
24
|
+
|
|
25
|
+
```screenplay
|
|
26
|
+
command RegisterInvoice
|
|
27
|
+
description "Registers a new invoice with its lines and payment terms"
|
|
28
|
+
invoiceId InvoiceId identifier
|
|
29
|
+
invoiceNumber InvoiceNumber
|
|
30
|
+
lines InvoiceLine[]
|
|
31
|
+
note String?
|
|
32
|
+
reads InvoiceList by invoiceNumber
|
|
33
|
+
authorize CanManageInvoice
|
|
34
|
+
validate
|
|
35
|
+
invoiceNumber not empty message "Invoice number is required"
|
|
36
|
+
invoiceNumber matches "^INV-[0-9]{6}$" message "Must look like INV-000000"
|
|
37
|
+
produces InvoiceRegistered
|
|
38
|
+
invoiceId = invoiceId
|
|
39
|
+
registeredAt = $context.occurred
|
|
40
|
+
concurrency
|
|
41
|
+
eventSource
|
|
42
|
+
events InvoiceRegistered, InvoiceCancelled
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Type modifiers: `<Type>[]` for a collection, `<Type>?` for optional.
|
|
46
|
+
|
|
47
|
+
⚠️ **The parser enforces no clause order.** The order above is the house
|
|
48
|
+
convention and the order a reader expects; the compiler accepts any. Keep to it.
|
|
49
|
+
|
|
50
|
+
⚠️ **`produces` and `handler` are mutually exclusive** — declaring both is an
|
|
51
|
+
error. Everything else may repeat except `description` (one) and `concurrency`
|
|
52
|
+
(one).
|
|
53
|
+
|
|
54
|
+
## `identifier` — the stream boundary decision
|
|
55
|
+
|
|
56
|
+
Exactly one command property may carry `identifier`. It names the value the
|
|
57
|
+
runtime resolves the event source id from, so **choosing it is choosing the stream
|
|
58
|
+
boundary** — the highest-consequence decision in the slice.
|
|
59
|
+
|
|
60
|
+
- A second `identifier` on the same command is an error: *only one property can be
|
|
61
|
+
the identifier*.
|
|
62
|
+
- `identifier` on an **event** property is an error: *an event never carries its
|
|
63
|
+
event source id*. It travels in the event context.
|
|
64
|
+
|
|
65
|
+
## `reads` — state the command decides against
|
|
66
|
+
|
|
67
|
+
```screenplay
|
|
68
|
+
reads InvoiceList by invoiceNumber
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`by <property>` names the command property the read model is looked up by; it is
|
|
72
|
+
optional. A command may read several read models, but **each one only once** —
|
|
73
|
+
a duplicate is an error.
|
|
74
|
+
|
|
75
|
+
This is a deliberate Cratis divergence from generic Event Modeling, which forbids
|
|
76
|
+
read-model-to-command edges. Use it for a genuine consistency boundary, and pair
|
|
77
|
+
it with `concurrency` so the decision is enforced at append time rather than
|
|
78
|
+
merely consulted. Do not use it to fetch data the caller could supply.
|
|
79
|
+
|
|
80
|
+
## `validate` — the complete rule vocabulary
|
|
81
|
+
|
|
82
|
+
Fourteen rule kinds (`ValidationRuleKind`), each taking an optional
|
|
83
|
+
`message "<text>"`:
|
|
84
|
+
|
|
85
|
+
| Rule | Example |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `not empty` | `name not empty` |
|
|
88
|
+
| `max <n>` / `min <n>` | `reason max 500`, `quantity min 1` |
|
|
89
|
+
| `> <v>` / `>= <v>` / `< <v>` / `<= <v>` | `quantity > 0`, `discountPct <= 100` |
|
|
90
|
+
| `== <v>` / `!= <v>` | `currency == "NOK"`, `status != "draft"` |
|
|
91
|
+
| `length == <n>` | `currency length == 3` |
|
|
92
|
+
| `matches <regex>` | `invoiceNumber matches "^INV-[0-9]{6}$"` |
|
|
93
|
+
| `all > <v>` / `all >= <v>` | `lines.quantity all > 0` |
|
|
94
|
+
| `rule <Name>` | `orgNumber rule BeAValidOrganizationNumber` |
|
|
95
|
+
|
|
96
|
+
`matches` accepts either a named pattern (`email matches email`) or a quoted
|
|
97
|
+
regex, which is why the reference lists it on two rows while the parser has one
|
|
98
|
+
kind for it.
|
|
99
|
+
|
|
100
|
+
A `rule <Name>` may carry an indented `file <path>` or an inline code block as its
|
|
101
|
+
body. A rule with no body is a complete statement that the rule exists.
|
|
102
|
+
|
|
103
|
+
**Whole-command rules** use `require <condition>` with an optional indented
|
|
104
|
+
`message`, sharing the condition grammar with `produces … when` and `where`.
|
|
105
|
+
|
|
106
|
+
**Put format rules on the `concept`, not the command.** A concept carries its own
|
|
107
|
+
`validate` block and every use inherits it — that is Screenplay's type system, and
|
|
108
|
+
a rule that travels is worth more than one that is repeated.
|
|
109
|
+
|
|
110
|
+
## `authorize`
|
|
111
|
+
|
|
112
|
+
```screenplay
|
|
113
|
+
authorize IsAccountant
|
|
114
|
+
or IsCustomerSelf
|
|
115
|
+
|
|
116
|
+
authorize (IsAccountant or IsFinance) and OwnsInvoice
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Policy names are PascalCase. `and` binds tighter than `or`; parentheses group.
|
|
120
|
+
A continuation line extends the clause.
|
|
121
|
+
|
|
122
|
+
⚠️ **Two adjacent policies synthesize an implicit `and`.** `authorize A B` means
|
|
123
|
+
`A and B`. Write the operator explicitly so the reader does not have to know this.
|
|
124
|
+
|
|
125
|
+
## `produces`
|
|
126
|
+
|
|
127
|
+
Every mapping source form: a command property (`= invoiceNumber`), a context value
|
|
128
|
+
(`= $context.occurred`, `= $context.identity.id`, `= $context.identity.claims.x`,
|
|
129
|
+
`= $context.causedBy.subject`, `= $context.causation.type`, `= $context.tenant`),
|
|
130
|
+
an environment variable (`= $env.REGION`), a literal (`= "draft"`, `= 0`), or a
|
|
131
|
+
template (`` = `${firstName} ${lastName}` ``).
|
|
132
|
+
|
|
133
|
+
- **`for <expression>`** says which event source the event lands on. **At most one
|
|
134
|
+
per `produces`** — *an event is appended to one event source*.
|
|
135
|
+
- **`tag`** lines apply tags to this append. Tags also exist on the `event`
|
|
136
|
+
declaration, where they apply to every append of that type.
|
|
137
|
+
- **Several unconditional `produces` blocks** are allowed — that is co-production,
|
|
138
|
+
and it is what an automation is *not*.
|
|
139
|
+
- **`produces when <condition>`** takes the event name on the next indented line:
|
|
140
|
+
|
|
141
|
+
```screenplay
|
|
142
|
+
produces when isProForma == true
|
|
143
|
+
ProFormaInvoiceIssued
|
|
144
|
+
invoiceId = invoiceId
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## `concurrency`
|
|
148
|
+
|
|
149
|
+
Five dimensions, each at most once, and at most one `concurrency` block per
|
|
150
|
+
command:
|
|
151
|
+
|
|
152
|
+
| Dimension | Scopes the check to |
|
|
153
|
+
| --- | --- |
|
|
154
|
+
| `eventSource` | the command's own event source id |
|
|
155
|
+
| `sourceType <Name>` | an event source type |
|
|
156
|
+
| `streamType <Name>` | an event stream type |
|
|
157
|
+
| `streamId <Name>` | an event stream id |
|
|
158
|
+
| `events <A>, <B>` | the listed event types |
|
|
159
|
+
|
|
160
|
+
An unknown dimension is an error naming all five.
|
|
161
|
+
|
|
162
|
+
## `constraint` — append-time invariants
|
|
163
|
+
|
|
164
|
+
Exactly one of three forms per constraint:
|
|
165
|
+
|
|
166
|
+
```screenplay
|
|
167
|
+
constraint UniqueInvoiceNumber
|
|
168
|
+
unique invoiceNumber on InvoiceRegistered
|
|
169
|
+
|
|
170
|
+
constraint OneRegistrationPerInvoice
|
|
171
|
+
unique event InvoiceRegistered
|
|
172
|
+
|
|
173
|
+
constraint InvoiceStatusTransition
|
|
174
|
+
file Constraints/InvoiceStatusTransition.cs
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
A constraint with no body is an error naming the three forms. Use a constraint
|
|
178
|
+
when two concurrent appends must not both win — a `validate` rule cannot do that.
|
|
179
|
+
|
|
180
|
+
## `concept` and `type`
|
|
181
|
+
|
|
182
|
+
```screenplay
|
|
183
|
+
concept InvoiceId : Uuid
|
|
184
|
+
concept DiscountPercentage : Decimal
|
|
185
|
+
validate
|
|
186
|
+
>= 0 message "A discount cannot be negative"
|
|
187
|
+
<= 100 message "A discount cannot exceed 100 percent"
|
|
188
|
+
concept PersonName : String @pii
|
|
189
|
+
pii reason "Billing contact name; lawful basis: contract performance."
|
|
190
|
+
concept InvoiceStatus : Enum
|
|
191
|
+
draft
|
|
192
|
+
sent
|
|
193
|
+
paid
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The seven primitives are `Uuid`, `String`, `Int`, `Decimal`, `Bool`, `Date` and
|
|
197
|
+
`DateTime`. `Enum` is **not** one of them — it is a separate concept kind, which
|
|
198
|
+
is why the compiler says *expected … or Enum* rather than listing it among them.
|
|
199
|
+
Attributes `@pii` and `@sensitive`, each with at most one `reason`; a
|
|
200
|
+
reason for an attribute the concept does not declare is an error. **Compliance is
|
|
201
|
+
inherited** — a property typed with a `@pii` concept is PII everywhere.
|
|
202
|
+
|
|
203
|
+
⚠️ **Enum trap.** A value literally named `validate` is read as an empty validate
|
|
204
|
+
block. Write `@validate` for the value; the compiler warns when it sees the
|
|
205
|
+
ambiguity.
|
|
206
|
+
|
|
207
|
+
Use `type <Name>` for a composite shape (several properties) that events and
|
|
208
|
+
commands reference; use `concept` for a single wrapped primitive.
|
|
209
|
+
|
|
210
|
+
## `policy` and `persona`
|
|
211
|
+
|
|
212
|
+
```screenplay
|
|
213
|
+
policy IsAuthenticated
|
|
214
|
+
require authenticated
|
|
215
|
+
policy CanManageInvoice
|
|
216
|
+
require role "InvoiceManager"
|
|
217
|
+
or role "Accountant"
|
|
218
|
+
policy OwnsInvoice
|
|
219
|
+
require claim "sub" matches subject
|
|
220
|
+
|
|
221
|
+
persona Accountant
|
|
222
|
+
description "Handles invoicing and collections"
|
|
223
|
+
policy IsAccountant
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Three condition forms: `authenticated`, `role "<name>"`, and
|
|
227
|
+
`claim "<name>" matches subject | <value>`. A policy must declare a `require`
|
|
228
|
+
condition or an inline code block.
|
|
229
|
+
|
|
230
|
+
## `seed`
|
|
231
|
+
|
|
232
|
+
```screenplay
|
|
233
|
+
seed
|
|
234
|
+
for "3fa85f64-5717-4562-b3fc-2c963f66afa6"
|
|
235
|
+
CustomerRegistered
|
|
236
|
+
name = "Acme Corp"
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Events append to that event source in declaration order, using the same mapping
|
|
240
|
+
expression grammar as `produces`. Several `seed` blocks accumulate.
|
|
241
|
+
|
|
242
|
+
## `$context`
|
|
243
|
+
|
|
244
|
+
Four contexts, and **what each omits is load-bearing** — read
|
|
245
|
+
[context.md](references/context.md) for the full member lists, the declarative
|
|
246
|
+
`$context.` paths, and `$causedBy` / `$env` / `$strings`.
|
|
247
|
+
|
|
248
|
+
| Context | For | Deliberately omits |
|
|
249
|
+
| --- | --- | --- |
|
|
250
|
+
| Command | a command handler | — |
|
|
251
|
+
| Query | a query performer | — |
|
|
252
|
+
| Rule | a validation rule | **`Identity`** — validation does not see roles or claims |
|
|
253
|
+
| Policy | an authorization policy | **`CausedBy`, `Causation`** |
|
|
254
|
+
|
|
255
|
+
## Verify
|
|
256
|
+
|
|
257
|
+
- [ ] `screenplay <model> --warnaserror` reports zero errors and zero warnings.
|
|
258
|
+
- [ ] Exactly one command property carries `identifier`, and no event property does.
|
|
259
|
+
- [ ] Format rules live on the `concept`; state-dependent rules are specifications.
|
|
260
|
+
- [ ] Every `authorize` combining policies writes `and`/`or` explicitly.
|
|
261
|
+
- [ ] Any `reads` that decides a rule is paired with a `concurrency` block.
|
|
262
|
+
- [ ] Personal data is `@pii` on the concept, with a reason.
|
|
263
|
+
- [ ] No event carries an optional property covering two situations.
|
|
264
|
+
|
|
265
|
+
## Route near misses
|
|
266
|
+
|
|
267
|
+
- Deciding *which* commands and events exist: `cratis-screenplay-event-modeling`.
|
|
268
|
+
- Building read models from these events: `cratis-screenplay-projections`.
|
|
269
|
+
- Pinning the rejections: `cratis-screenplay-specifications`.
|
|
270
|
+
- The admitted-versus-parsed boundary: `cratis-screenplay-model-authoring`.
|