@cratis/pi 6.1.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.
Files changed (22) hide show
  1. package/package/corpus/skills/cratis-screenplay-captures-and-reactions/SKILL.md +90 -15
  2. package/package/corpus/skills/cratis-screenplay-captures-and-reactions/verification.json +5 -1
  3. package/package/corpus/skills/cratis-screenplay-command-surface/SKILL.md +162 -64
  4. package/package/corpus/skills/cratis-screenplay-command-surface/references/context.md +20 -8
  5. package/package/corpus/skills/cratis-screenplay-command-surface/verification.json +33 -1
  6. package/package/corpus/skills/cratis-screenplay-event-modeling/SKILL.md +63 -31
  7. package/package/corpus/skills/cratis-screenplay-event-modeling/references/nine-steps.md +35 -13
  8. package/package/corpus/skills/cratis-screenplay-event-modeling/verification.json +12 -0
  9. package/package/corpus/skills/cratis-screenplay-model-authoring/SKILL.md +50 -9
  10. package/package/corpus/skills/cratis-screenplay-model-authoring/references/language-reference.md +78 -22
  11. package/package/corpus/skills/cratis-screenplay-model-authoring/references/mcp-tools.md +21 -5
  12. package/package/corpus/skills/cratis-screenplay-model-authoring/verification.json +16 -0
  13. package/package/corpus/skills/cratis-screenplay-projections/SKILL.md +155 -43
  14. package/package/corpus/skills/cratis-screenplay-projections/references/pdl-grammar.md +81 -37
  15. package/package/corpus/skills/cratis-screenplay-projections/verification.json +6 -1
  16. package/package/corpus/skills/cratis-screenplay-read-surface/SKILL.md +64 -17
  17. package/package/corpus/skills/cratis-screenplay-read-surface/verification.json +4 -1
  18. package/package/corpus/skills/cratis-screenplay-specifications/SKILL.md +201 -115
  19. package/package/corpus/skills/cratis-screenplay-specifications/verification.json +13 -1
  20. package/package/corpus/skills/cratis-screenplay-ui-composition/SKILL.md +133 -24
  21. package/package/corpus/skills/cratis-screenplay-ui-composition/verification.json +4 -1
  22. 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.12.1` | CDL parser, reaction and trigger parsers, diagnostics |
28
+ | `Cratis.Screenplay` | `4.31.0` | CDL parser, reaction and trigger parsers, diagnostics, semantic binder |
29
29
 
30
- Read from the Screenplay repository at tag `v4.12.1` (commit `122eee8`), against
31
- `Documentation/screenplay/{captures,captures/grammar,reactions,triggers}.md` and
32
- `Source/DotNET/Screenplay/Parsing/`. Reverify before claiming another version
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.** `SemanticSliceKind` admits
36
- only `StateChange` and `StateView`, so an `Automation` or `Translate` slice is
37
- rejected by ESM v1 outright. Model them when the `.play` file is the deliverable;
38
- do not promise they execute.
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 that implements them.
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 — as a **warning**, like every other
197
- unresolved reference.
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
- ⚠️ A trigger body reserves `file`, so a trigger value named `file` is written
203
- `@file`.
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.12.1` | Parser, validator, diagnostics |
28
+ | `Cratis.Screenplay` | `4.31.0` | Parser, validator, diagnostics, semantic binder |
29
29
 
30
- Read from the Screenplay repository at tag `v4.12.1` (commit `122eee8`), against
31
- both the documentation and `Source/DotNET/Screenplay/Parsing/`. Reverify before
32
- claiming another version behaves the same.
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 above is the house
59
- convention and the order a reader expects; the compiler accepts any. Keep to it.
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
- Exactly one command property may carry `identifier`. It names the value the
68
- runtime resolves the event source id from, so **choosing it is choosing the stream
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
- reads InvoiceList by invoiceNumber
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; it is
83
- optional. A command may read several read models, but **each one only once** —
84
- a duplicate is an error.
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
- This is a deliberate Cratis divergence from generic Event Modeling, which forbids
87
- read-model-to-command edges. Use it for a genuine consistency boundary, and pair
88
- it with `concurrency` so the decision is enforced at append time rather than
89
- merely consulted. Do not use it to fetch data the caller could supply.
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
- ## `validate` — the complete rule vocabulary
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
- Fourteen rule kinds (`ValidationRuleKind`), each taking an optional
94
- `message "<text>"`:
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`, `quantity min 1` |
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 != "draft"` |
132
+ | `== <v>` / `!= <v>` | `currency == "NOK"`, `status != draft` |
102
133
  | `length == <n>` | `currency length == 3` |
103
- | `matches <regex>` | `invoiceNumber matches "^INV-[0-9]{6}$"` |
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
- `matches` accepts either a named pattern (`email matches email`) or a quoted
108
- regex, which is why the reference lists it on two rows while the parser has one
109
- kind for it.
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
- A `rule <Name>` may carry an indented `file <path>` or an inline code block as its
112
- body. A rule with no body is a complete statement that the rule exists.
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
- **Whole-command rules** use `require <condition>` with an optional indented
115
- `message`, sharing the condition grammar with `produces … when` and `where`.
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
- ## `produces`
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
- Every mapping source form: a command property (`= invoiceNumber`), a context value
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
- - **`for <expression>`** says which event source the event lands on. **At most one
145
- per `produces`** — *an event is appended to one event source*.
146
- - **`tag`** lines apply tags to this append. Tags also exist on the `event`
147
- declaration, where they apply to every append of that type.
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 naming all five.
234
+ An empty block or an unknown dimension is an error.
172
235
 
173
- ## `constraint` — append-time invariants
236
+ ## `constraint` — uniqueness at append time
174
237
 
175
- Exactly one of three forms per constraint:
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
- A constraint with no body is an error naming the three forms. Use a constraint
189
- when two concurrent appends must not both win — a `validate` rule cannot do that.
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 must declare a `require`
239
- condition or an inline code block.
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
- - [ ] Exactly one command property carries `identifier`, and no event property does.
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
- - [ ] Any `reads` that decides a rule is paired with a `concurrency` block.
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 admitted-versus-parsed boundary: `cratis-screenplay-model-authoring`.
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
- whole-command `require`).
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
- Usable wherever a mapping source is — `produces` mappings, `seed` values, capture
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` — an unknown one is an error naming all three |
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 TotalLogins
109
+ count totalLogins
98
110
  ```
99
111
 
100
112
  ⚠️ A template expression is **not allowed in a composite key**, and a composite