@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
@@ -24,15 +24,17 @@ source is the single flow model.
24
24
 
25
25
  | Package | Version | Purpose |
26
26
  | --- | --- | --- |
27
- | `Cratis.Screenplay` | `4.12.1` | Parser, validator, diagnostics |
27
+ | `Cratis.Screenplay` | `4.31.0` | Parser, validator, diagnostics, semantic binder |
28
28
 
29
- Read from the Screenplay repository at tag `v4.12.1` (commit `122eee8`), against
30
- `Documentation/screenplay/{queries,readmodels,screens}.md` and
31
- `Source/DotNET/Screenplay/Parsing/`. Reverify before claiming another version
32
- behaves the same.
29
+ Checked against the Screenplay repository at tag `v4.31.0` (commit `355dffb`):
30
+ `Documentation/screenplay/{queries,readmodels,screens,policies,specifications,interactions}.md`
31
+ and decision 0010. Every example below compiles with that version's compiler.
32
+ Reverify before claiming another version behaves the same.
33
33
 
34
34
  ## `readmodel` — shape only
35
35
 
36
+ Excerpt, inside a `StateView` slice:
37
+
36
38
  ```screenplay
37
39
  readmodel AccountBalance
38
40
  description "What the account is worth right now"
@@ -45,8 +47,20 @@ A read model declares what it **is** — nothing about what builds it. Whatever
45
47
  builds it points at it with `=>`, and **exactly one thing may**: two builders is
46
48
  error `PLAY0191`. See `cratis-screenplay-projections` for the builder side.
47
49
 
50
+ Do not mark a read-model property `identifier`. The executable model infers the
51
+ instance identifier from the `by` property of the keyed queries that return the
52
+ read model in its own slice
53
+ (`query InvoiceById => InvoiceSummary? by invoiceId InvoiceId`). Several queries
54
+ over the same `by` property are fine; each must still have the shape the
55
+ executable model admits, `=> <ReadModel>?` with one caller-supplied `by`. A read
56
+ model with no keyed query, or with keyed queries over different `by` properties,
57
+ has no unambiguous identifier and does not bind (`PLAY0268`), and specifications
58
+ cannot select an instance of it.
59
+
48
60
  ## `query`
49
61
 
62
+ Excerpt: the concepts, the read model and the policy are declared elsewhere.
63
+
50
64
  ```screenplay
51
65
  query ListInvoices => InvoiceListReadModel[]
52
66
  description "Every invoice the caller may see, narrowed by status and customer"
@@ -70,10 +84,13 @@ optionally prefixed `observable`.
70
84
  ⚠️ **Anything the caller must not be able to choose — the tenant, the caller's own
71
85
  subject — belongs on a `from` parameter, never on a `filter` the UI supplies.**
72
86
  Any mapping source works as a `from` source, so `$context.`, `$env.` and constants
73
- are all available.
87
+ are all available. The `from` states the contract; the runtime that realizes the
88
+ query enforces it. A compiling query is not evidence that access control works.
74
89
 
75
90
  ### `scoped to`
76
91
 
92
+ Excerpt, inside a `StateView` slice:
93
+
77
94
  ```screenplay
78
95
  query Mine => Timesheet[]
79
96
  scoped to identity
@@ -95,23 +112,41 @@ Treat every `scoped to global` in a review as a question to answer, not a detail
95
112
 
96
113
  ### `observable`
97
114
 
98
- `=> observable OverdueInvoicesReadModel[]` is a live read that keeps pushing;
99
- without the marker a query is one-shot, which is the default and what most reads
100
- are. The marker qualifies only *how* the result arrives, so it composes with `[]`,
101
- `?`, `by`, `filter`, `authorize` and a `performer`. A screen binds to a live query
102
- exactly as it binds to a one-shot one and gets updates for free.
115
+ `=> observable OverdueInvoicesReadModel[]` declares a live read that keeps
116
+ pushing; without the marker a query is one-shot, which is the default and what
117
+ most reads are. The marker qualifies only *how* the result arrives, so it composes
118
+ with `[]`, `?`, `by`, `filter`, `authorize` and a `performer`, and a screen binds
119
+ to a live query exactly as it binds to a one-shot one. The delivery itself is the
120
+ target runtime's: the executable model does not bind observable queries.
121
+
122
+ Paging, sorting and change-set delivery for queries are accepted as Screenplay
123
+ decision 0010 but **not in the language yet**. Do not invent syntax for them.
103
124
 
104
125
  ### `performer`
105
126
 
106
127
  A query is **complete without a performer** — it is realization metadata, not a
107
- precondition. It takes a `file` reference or an inline code block, and is the
108
- query's counterpart to a command's `handler`. `csharp` and `sql` are the two that
109
- make sense here; the parser accepts any registered inline language
110
- (`csharp`, `typescript`, `react`, `html`, `sql` are built in) and does not
111
- reject a nonsensical one, so the choice is yours to get right.
128
+ precondition. It takes a `file` reference or an inline block in a tagged fence
129
+ (` ```csharp `, ` ```sql `), and is the query's counterpart to a command's
130
+ `handler`. The parser accepts any registered inline language and does not reject
131
+ a nonsensical one, so the choice is yours to get right. Performer code is opaque:
132
+ Screenplay never runs it.
133
+
134
+ ### What runs
135
+
136
+ In the executable model today, only the keyed snapshot shape binds:
137
+ `=> <ReadModel>?` with one caller-supplied `by` argument and no `observable`,
138
+ `filter`, `scoped to` or `performer`. Anything else, including `=> <ReadModel>`
139
+ and `=> <ReadModel>[]`, reports `PLAY0268`. Its `authorize` gate runs before
140
+ the lookup, ANDed with module and feature gates; a denied caller gets
141
+ `Unauthorized`. Pin that with a `given caller` fixture and `then denied` (see
142
+ `cratis-screenplay-specifications`). Model the richer query shapes when the
143
+ application needs them; just do not claim the reference runner exercised them.
112
144
 
113
145
  ## `screen` — three levels
114
146
 
147
+ The examples below are excerpts: each screen sits in a slice, and the queries,
148
+ commands and screens it names are declared elsewhere.
149
+
115
150
  **Level 1 — intent.** Data and actions; the tool generates the component.
116
151
 
117
152
  ```screenplay
@@ -152,6 +187,13 @@ Constructs: `title`, `data … via query … [by <param>]`, `action <Command>` w
152
187
  `on row-click navigate to <Screen> [by <param>]`, `summary <ReadModel>` with
153
188
  `field <property> label`, and `template <Name>` with slot bodies.
154
189
 
190
+ `on row-click navigate to …` is the one-line table navigation form. What a click,
191
+ selection, submit or screen entry *does* beyond that — confirm, execute, refresh,
192
+ open a dialog, branch on success or failure — is an interaction: an inline `on`
193
+ block or a named `behavior` attached with `uses`. See
194
+ `cratis-screenplay-ui-composition`. Screens and interactions are deferred from the
195
+ executable model (information `PLAY0269`); they never block binding.
196
+
155
197
  ## How a bare name resolves
156
198
 
157
199
  **Inside out:** the slice, then the enclosing feature, then the module, then the
@@ -161,7 +203,8 @@ That rule exists because a generated document cannot make every name unique —
161
203
  real application declares 76 queries under 37 distinct names, with `All` appearing
162
204
  21 times. Two sibling slices can each declare `All`, and each screen gets its own.
163
205
 
164
- Reach across slices by qualifying with **any trailing part** of the scope:
206
+ Reach across slices by qualifying with **any trailing part** of the scope.
207
+ Excerpt: `Queue` and `Deviations` are sibling slices that each declare `All`.
165
208
 
166
209
  ```screenplay
167
210
  screen OverviewScreen
@@ -180,6 +223,8 @@ Ambiguous query 'All' - it matches 2 declarations equally well
180
223
  ⚠️ Unresolved and ambiguous references are **warnings, not errors**, because a
181
224
  name may resolve to something outside the document. Run with `--warnaserror` or a
182
225
  screen can navigate to a screen that does not exist and the build stays green.
226
+ This is about references only: malformed clauses, duplicate declarations and
227
+ binding failures are errors.
183
228
 
184
229
  ## Verify
185
230
 
@@ -188,6 +233,8 @@ screen can navigate to a screen that does not exist and the build stays green.
188
233
  - [ ] Every `scoped to global` is deliberate and defensible.
189
234
  - [ ] `observable` is present exactly where the caller should see changes without
190
235
  asking again.
236
+ - [ ] Every authorized query has a specification with `given caller`, including a
237
+ `then denied` case.
191
238
  - [ ] Each read model has exactly one builder and every field traces to an event.
192
239
  - [ ] No screen reference is left ambiguous or unresolved.
193
240
 
@@ -7,6 +7,9 @@
7
7
  { "kind": "skill-contains", "value": "`cratis ai install` manages `.cratis/ai/`, not `.cratis/screenplay/`" },
8
8
  { "kind": "skill-contains", "value": "The tenant is the default and stays unstated" },
9
9
  { "kind": "skill-contains", "value": "belongs on a `from` parameter, never on a `filter`" },
10
- { "kind": "skill-contains", "value": "warnings, not errors" }
10
+ { "kind": "skill-contains", "value": "warnings, not errors" },
11
+ { "kind": "skill-contains", "value": "the executable model does not bind observable queries" },
12
+ { "kind": "skill-contains", "value": "A compiling query is not evidence that access control works" },
13
+ { "kind": "skill-contains", "value": "decision 0010 but **not in the language yet**" }
11
14
  ]
12
15
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: cratis-screenplay-specifications
3
- description: Pin behavior in a Cratis Screenplay `.play` model with given/when/then `specification` blocks — prior events and read-model state, the command under test, expected events, read-model state, query results and rejections, plus what the reference execution actually runs. Use when writing acceptance criteria for a slice, specifying a rejection, or deciding whether a rule belongs in a specification or in the type system. Do not use for C# or TypeScript test code.
3
+ description: Pin behavior in a Cratis Screenplay `.play` model with given/when/then `specification` blocks — prior events and read-model state, the caller, the command or appended event under test, expected events, read-model state, query results, rejections and denials, plus what the reference execution actually runs. Use when writing acceptance criteria for a slice, specifying a rejection or an authorization denial, or deciding whether a rule belongs in a specification or in the type system. Do not use for C# or TypeScript test code.
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -25,106 +25,177 @@ source is the single flow model.
25
25
 
26
26
  | Package | Version | Purpose |
27
27
  | --- | --- | --- |
28
- | `Cratis.Screenplay` | `4.12.1` | Parser, semantic binder, reference execution |
28
+ | `Cratis.Screenplay` | `4.31.0` | Parser, semantic binder, reference execution |
29
29
 
30
- Read from the Screenplay repository at tag `v4.12.1` (commit `122eee8`), against
31
- `Documentation/screenplay/specifications.md` and
32
- `Source/DotNET/Screenplay/Parsing/SpecificationParser.cs`. Reverify before
30
+ Checked against the Screenplay repository at tag `v4.31.0` (commit `355dffb`):
31
+ `Documentation/screenplay/{specifications,policies,constraints,readmodels,diagnostics}.md`.
32
+ The worked example below compiles with zero diagnostics, binds to ESM v2, and
33
+ all six specifications pass the reference runner at that tag. Reverify before
33
34
  claiming another version behaves the same.
34
35
 
35
36
  ## The vocabulary
36
37
 
37
38
  | Construct | Meaning |
38
39
  | --- | --- |
39
- | `given <EventType>` | prior state, established by replaying events before the command runs |
40
- | `given readmodel <ReadModelType>` | prior read-model state, established directly |
41
- | `when <CommandType>` | the command under test — **at most one**; a second is a compile error |
42
- | `then <EventType>` | an event expected to be produced |
43
- | `then readmodel <ReadModelType>` | the read-model state expected afterwards |
44
- | `then query <Query>` | ordered query results for explicit arguments |
45
- | `arguments` | the values supplied to the query |
46
- | `result` | one expected query result; repeat for many |
47
- | `then error "<message>"` | a rejection, for the named reason |
48
- | `then error` | a rejection, for a reason this specification does not name |
49
- | `<property> = <value>` | a property value, using the `produces` expression grammar |
50
-
51
- `given`, `then`, `then readmodel`, `then query` and `then error` are each zero or
52
- more.
53
-
54
- ## Command specifications
40
+ | `given <EventType>` | prior state, established by replaying events before the action |
41
+ | `given readmodel <ReadModelType>` | prior read-model state, established directly — a **complete** instance including its identifier |
42
+ | `given caller` | the caller: `authenticated`, `role "<r>"`, repeatable `claim "<type>" = "<value>"`; empty means unauthenticated |
43
+ | `when <CommandType>` | run a command |
44
+ | `when append <EventType>` | append one event: constraints and projections run, the command and reactions do not |
45
+ | `then <EventType>` | an expected new event |
46
+ | `then events in any order` | compare the new events without regard to order |
47
+ | `then readmodel <ReadModelType> [exactly]` | read-model state afterwards — must state the identifier |
48
+ | `then query <Query> [exactly]` | query results for explicit `arguments`; one `result` per row; none means empty |
49
+ | `then error "<message>"` | a validation or constraint rejection, for that reason |
50
+ | `then error` | a validation or constraint rejection, reason unnamed |
51
+ | `then denied` | an authorization denial (`Unauthorized`) |
52
+ | `for <value>` | the event source of a `given`/`then` event, or of the `when` command or appended event |
53
+ | `<property> = <value>` | a literal, `null`, or a one-line JSON-shaped object or list: `lines = [{"sku":"A-1","quantity":2}]` |
54
+
55
+ Rules the binder enforces:
56
+
57
+ - **At most one action** — `when` or `when append` (`PLAY0358`). Without an
58
+ action, assert only `then readmodel` or `then query` (`PLAY0352`).
59
+ - **`then` contains either events or an error — never both.** A rejection
60
+ specification has exactly one rejection and no success outcome.
61
+ - **`then denied` stands alone**: not with events, errors or state assertions
62
+ (`PLAY0388`). For a query, pair it with one `then query` that has `arguments`
63
+ and no `result`.
64
+ - **Authorized commands and queries need `given caller`** (`PLAY0389`). The runner
65
+ never invents a caller.
66
+ - **`null` only in optional read-model values.** A `null` in a command or event
67
+ value is `PLAY0350`: an optional fact is a separate event.
68
+
69
+ `PLAY0358` is a syntax error. `PLAY0350`, `PLAY0352`, `PLAY0388`, `PLAY0389` and
70
+ the one-rejection rule (`PLAY0273`) are reported only when the model binds, so
71
+ `screenplay --warnaserror` does not show them. Check executable diagnostics
72
+ (the MCP authoring tools) as well.
73
+
74
+ ## How outcomes are compared
75
+
76
+ - **Events:** the complete set of new events, in authored order, unless
77
+ `then events in any order` is stated. The count is always exact.
78
+ - **Read models and query rows:** only the asserted properties must match
79
+ (subset); add `exactly` to require every property. Row count and order are
80
+ always exact. A missing property does not match an asserted `null`.
81
+ - **Rejections:** `then error "<message>"` matches the message whatever the rule's
82
+ validation severity; no form asserts severity. Bare `then error` never matches
83
+ a denial. Quote a localized key: `then error "$strings.invoices.reasonRequired"`.
84
+
85
+ ## Worked example
86
+
87
+ A command with an authorization gate, a validation rule and a uniqueness
88
+ constraint, and a read model fed by its event. The `for` values select ESM v2.
55
89
 
56
90
  ```screenplay
57
- specification RegisteringADraftInvoice
58
- given CustomerRegistered
59
- customerId = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
60
- name = "Acme Corp"
61
- when RegisterInvoice
62
- invoiceId = "9c858901-8a57-4791-81fe-4c455b099bc9"
63
- customerId = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
64
- invoiceNumber = "INV-000123"
65
- then InvoiceRegistered
66
- invoiceId = "9c858901-8a57-4791-81fe-4c455b099bc9"
67
- invoiceNumber = "INV-000123"
91
+ concept InvoiceId : Uuid
92
+ concept InvoiceNumber : String
93
+ policy IsAccountant
94
+ require role "Accountant"
95
+ module Invoicing
96
+ feature Registration
97
+ slice StateChange RegisterInvoice
98
+ command RegisterInvoice
99
+ invoiceId InvoiceId identifier
100
+ invoiceNumber InvoiceNumber
101
+ authorize IsAccountant
102
+ validate
103
+ invoiceNumber not empty message "Invoice number is required"
104
+ produces InvoiceRegistered
105
+ for invoiceId
106
+ invoiceNumber = invoiceNumber
107
+ event InvoiceRegistered
108
+ invoiceNumber InvoiceNumber
109
+ constraint UniqueInvoiceNumber
110
+ unique invoiceNumber on InvoiceRegistered
111
+ specification RegisteringAnInvoice
112
+ given caller
113
+ authenticated
114
+ role "Accountant"
115
+ when RegisterInvoice
116
+ invoiceId = "9c858901-8a57-4791-81fe-4c455b099bc9"
117
+ invoiceNumber = "INV-000123"
118
+ then InvoiceRegistered
119
+ for "9c858901-8a57-4791-81fe-4c455b099bc9"
120
+ invoiceNumber = "INV-000123"
121
+ then readmodel InvoiceSummary
122
+ invoiceId = "9c858901-8a57-4791-81fe-4c455b099bc9"
123
+ invoiceNumber = "INV-000123"
124
+ specification RejectingAnEmptyInvoiceNumber
125
+ given caller
126
+ authenticated
127
+ role "Accountant"
128
+ when RegisterInvoice
129
+ invoiceId = "9c858901-8a57-4791-81fe-4c455b099bc9"
130
+ invoiceNumber = ""
131
+ then error "Invoice number is required"
132
+ specification RejectingANumberAnotherInvoiceHolds
133
+ given caller
134
+ authenticated
135
+ role "Accountant"
136
+ given InvoiceRegistered
137
+ for "0f5f5f7f-0f6f-4f47-9f39-5c1f2f0a1a9f"
138
+ invoiceNumber = "INV-000123"
139
+ when RegisterInvoice
140
+ invoiceId = "9c858901-8a57-4791-81fe-4c455b099bc9"
141
+ invoiceNumber = "INV-000123"
142
+ then error
143
+ specification DenyingACallerWithoutTheRole
144
+ given caller
145
+ authenticated
146
+ when RegisterInvoice
147
+ invoiceId = "9c858901-8a57-4791-81fe-4c455b099bc9"
148
+ invoiceNumber = "INV-000123"
149
+ then denied
150
+ specification ProjectingAnAppendedInvoice
151
+ when append InvoiceRegistered
152
+ for "9c858901-8a57-4791-81fe-4c455b099bc9"
153
+ invoiceNumber = "INV-000123"
154
+ then query InvoiceById
155
+ arguments
156
+ invoiceId = "9c858901-8a57-4791-81fe-4c455b099bc9"
157
+ result
158
+ invoiceNumber = "INV-000123"
159
+ slice StateView InvoiceLookup
160
+ readmodel InvoiceSummary
161
+ invoiceId InvoiceId
162
+ invoiceNumber InvoiceNumber
163
+ query InvoiceById => InvoiceSummary?
164
+ by invoiceId InvoiceId
165
+ projection InvoiceSummaries => InvoiceSummary
166
+ from InvoiceRegistered
167
+ invoiceId = $eventSourceId
168
+ invoiceNumber = invoiceNumber
169
+ specification LookingUpAnExistingInvoice
170
+ given readmodel InvoiceSummary
171
+ invoiceId = "9c858901-8a57-4791-81fe-4c455b099bc9"
172
+ invoiceNumber = "INV-000001"
173
+ then query InvoiceById
174
+ arguments
175
+ invoiceId = "9c858901-8a57-4791-81fe-4c455b099bc9"
176
+ result
177
+ invoiceNumber = "INV-000001"
68
178
  ```
69
179
 
70
- **`then` contains either events or an error — never both.**
180
+ - `RejectingANumberAnotherInvoiceHolds` needs `for`: without it the `given`
181
+ event lands on the command's own event source, and re-claiming your own value
182
+ is not a violation.
183
+ - `ProjectingAnAppendedInvoice` exercises the projection without running the
184
+ command. Put `for`-bearing specifications in the slice whose command produces
185
+ the event; placed in the view slice, binding fails with `PLAY0273`.
186
+ - `LookingUpAnExistingInvoice` has no action: it checks established state only.
71
187
 
72
- ## Rejections — two forms, and they say different things
188
+ ## Rejections and denials say different things
73
189
 
74
- ```screenplay
75
- specification RejectingAnInvoiceWithNoLines
76
- when RegisterInvoice
77
- invoiceId = "9c858901-8a57-4791-81fe-4c455b099bc9"
78
- then error "An invoice must have at least one line"
79
- ```
80
-
81
- `then error "<message>"` says **rejected, for this reason** — pinning a constraint
82
- violation or a validation message down deliberately.
83
-
84
- ```screenplay
85
- specification RejectingAnInvoiceWhoseNumberIsAlreadyTaken
86
- given InvoiceRegistered
87
- invoiceNumber = "INV-000123"
88
- when RegisterInvoice
89
- invoiceNumber = "INV-000123"
90
- then error
91
- ```
92
-
93
- Bare `then error` says **rejected, for a reason this specification does not name**.
94
- Most specifications are this kind — the reason lives in the specification's name.
95
-
96
- ## View specifications
190
+ `then error "<message>"` says **rejected, for this reason** — pin it when the
191
+ message is the point. Bare `then error` says **rejected, for a reason this
192
+ specification does not name**; the reason lives in the specification's name.
193
+ `then denied` says **this caller may not do this**, which is decided before any
194
+ validation runs. Write the bare form rather than `then error ""`.
97
195
 
98
- ```screenplay
99
- specification SendingADraftInvoice
100
- given readmodel InvoiceListReadModel
101
- invoiceId = "9c858901-8a57-4791-81fe-4c455b099bc9"
102
- status = "draft"
103
- when ChangeInvoiceStatus
104
- status = "sent"
105
- then readmodel InvoiceListReadModel
106
- status = "sent"
107
- ```
108
-
109
- **Views cannot reject events.** There are no error cases for a projection.
110
-
111
- ## Query specifications
112
-
113
- ```screenplay
114
- specification LookingUpARegisteredProject
115
- when RegisterProject
116
- projectId = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
117
- name = "Screenplay"
118
- then query ProjectById
119
- arguments
120
- projectId = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
121
- result
122
- projectId = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
123
- name = "Screenplay"
124
- ```
125
-
126
- Results are compared **in order**. `then query` with no `result` block asserts the
127
- query returns nothing.
196
+ **Views cannot reject events.** A projection never refuses an event; there are no
197
+ error cases for one. (A `when append` can still be refused by an append-time
198
+ constraint — that is the constraint speaking, not the view.)
128
199
 
129
200
  ## Business rule or type system?
130
201
 
@@ -147,31 +218,41 @@ the concept.
147
218
 
148
219
  ## Application-boundary coverage
149
220
 
150
- Every slice should carry at least one specification whose `when` is the command a
151
- real caller issues and whose `then` is observable at that same boundary — produced
152
- events, read-model state, or query results. A specification satisfiable only by an
153
- internal function describes a unit, not a slice acceptance criterion.
221
+ Every slice should carry at least one specification whose action is what a real
222
+ caller does and whose `then` is observable at that same boundary — produced
223
+ events, read-model state, query results, a rejection or a denial. A specification
224
+ satisfiable only by an internal function describes a unit, not a slice acceptance
225
+ criterion.
154
226
 
155
227
  ## Reference execution — what actually runs
156
228
 
157
- Screenplay supplies a framework-neutral reference path: no Arc, no Chronicle, no
158
- database, no filesystem, no network. It executes against an immutable in-memory
159
- world so every downstream target has one normalized behavior to match.
160
-
161
- > The minimum evaluator currently admits the RegisterProject-style vertical:
162
- > `not empty` validation, unconditional event production, one affected read-model
163
- > instance, optional snapshot lookup, and exact ordered specification results.
164
- > Unsupported reachable capabilities **block plan creation** rather than producing
165
- > a partial or stubbed execution.
166
-
167
- A rejected execution returns the unchanged world. An accepted one commits its
168
- facts and projected state once, then evaluates the requested queries against that
169
- state.
170
-
171
- ⚠️ **`for <event-source-value>` is reserved for ESM v2.** The parser, printer and
172
- syntax tree preserve it, but ESM v1 binding reports blocking diagnostic
173
- `PLAY0268`. It cannot execute or render silently. Avoid it in a model that must
174
- run today.
229
+ Screenplay's reference runner executes specifications against an immutable
230
+ in-memory world: no Arc, no Chronicle, no database, no network. Every downstream
231
+ target has one normalized behavior to match.
232
+
233
+ - It runs the capabilities the execution plan admits: declarative validation
234
+ and `require` over command properties, conditional production, literal tags,
235
+ declarative policies, `unique` constraints, and projections as Chronicle
236
+ lowers them. The `cratis-screenplay-model-authoring` language reference lists
237
+ what binds and what the plan admits.
238
+ - Binding is not admission. These projection constructs bind, but the plan
239
+ refuses them: a projection-level `remove via join`; `all` beside removals,
240
+ `children` or `nested`; a `join`, `children` or `remove via join` inside
241
+ `nested`; and any `$eventContext.<path>` other than `eventSourceId`, such as
242
+ `$eventContext.occurred`. The limits apply at every ESM version.
243
+ - A specification that needs **opaque code** — a bodied reducer, a rule with a
244
+ body, a fenced `validate` block, a code policy — returns **unsupported** and
245
+ never passes. Authorization is evaluated first, so a `then denied` case still
246
+ runs when a portable policy decides it. Other specifications in the model run
247
+ normally.
248
+ - Unsupported reachable declarative constructs block the whole execution plan
249
+ rather than running partially: no specification in the model runs, including
250
+ the ones that never touch the refused construct.
251
+ - Reactions never run, not even after `when append`.
252
+ - A rejection leaves the world unchanged; an accepted action commits once, then
253
+ the read models and queries are compared.
254
+
255
+ Unsupported is not passed. Report it as "needs a target", not as green.
175
256
 
176
257
  ## Quality checklist
177
258
 
@@ -183,24 +264,29 @@ For every specification:
183
264
 
184
265
  For command specifications:
185
266
 
186
- - [ ] `given` contains only events, each with all its fields.
187
- - [ ] `when` contains exactly one command with all its inputs.
267
+ - [ ] Every `given` event and the `when` command state all required fields.
268
+ - [ ] `given caller` is present whenever the command is authorized, with a
269
+ `then denied` case for a caller who must be refused.
188
270
  - [ ] `then` contains **either** events **or** an error, never both.
271
+ - [ ] A collision between two event sources uses `for` on the `given` event.
189
272
  - [ ] Error cases test business rules, not format validation.
190
273
 
191
274
  For view specifications:
192
275
 
193
- - [ ] `given readmodel` is the complete state before.
194
- - [ ] `then readmodel` is the complete state after.
276
+ - [ ] `given readmodel` is a complete instance with its identifier.
277
+ - [ ] `then readmodel` states the identifier and the properties that matter; add
278
+ `exactly` only when extra properties must fail the assertion.
195
279
  - [ ] No error cases — views cannot reject.
196
280
 
197
281
  ## Verify
198
282
 
199
283
  - [ ] `screenplay <model> --warnaserror` reports zero errors and zero warnings.
284
+ - [ ] Executable diagnostics are clean too: `PLAY0350`, `PLAY0352`, `PLAY0388`,
285
+ `PLAY0389` and `PLAY0273` are only reported at binding.
200
286
  - [ ] Every slice has at least one boundary specification.
201
- - [ ] The rejections are specified, not only the happy path.
287
+ - [ ] The rejections and denials are specified, not only the happy path.
202
288
  - [ ] No `given`/`when`/`then` clause names an element the model does not declare.
203
- - [ ] No `for <event-source-value>` in a model that must reach a runtime.
289
+ - [ ] An unsupported run is reported as unsupported, never as passing.
204
290
 
205
291
  ## Route near misses
206
292
 
@@ -21,7 +21,19 @@
21
21
  },
22
22
  {
23
23
  "kind": "skill-contains",
24
- "value": "PLAY0268"
24
+ "value": "**Authorized commands and queries need `given caller`**"
25
+ },
26
+ {
27
+ "kind": "skill-contains",
28
+ "value": "only the asserted properties must match"
29
+ },
30
+ {
31
+ "kind": "skill-contains",
32
+ "value": "Unsupported is not passed."
33
+ },
34
+ {
35
+ "kind": "skill-contains",
36
+ "value": "Bare `then error` never matches\n a denial"
25
37
  }
26
38
  ]
27
39
  }