@cratis/pi 6.2.0 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package/corpus/skills/cratis-screenplay-captures-and-reactions/SKILL.md +90 -15
- package/package/corpus/skills/cratis-screenplay-captures-and-reactions/verification.json +5 -1
- package/package/corpus/skills/cratis-screenplay-command-surface/SKILL.md +162 -64
- package/package/corpus/skills/cratis-screenplay-command-surface/references/context.md +20 -8
- package/package/corpus/skills/cratis-screenplay-command-surface/verification.json +33 -1
- package/package/corpus/skills/cratis-screenplay-event-modeling/SKILL.md +63 -31
- package/package/corpus/skills/cratis-screenplay-event-modeling/references/nine-steps.md +35 -13
- package/package/corpus/skills/cratis-screenplay-event-modeling/verification.json +12 -0
- package/package/corpus/skills/cratis-screenplay-model-authoring/SKILL.md +50 -9
- package/package/corpus/skills/cratis-screenplay-model-authoring/references/language-reference.md +78 -22
- package/package/corpus/skills/cratis-screenplay-model-authoring/references/mcp-tools.md +21 -5
- package/package/corpus/skills/cratis-screenplay-model-authoring/verification.json +16 -0
- package/package/corpus/skills/cratis-screenplay-projections/SKILL.md +155 -43
- package/package/corpus/skills/cratis-screenplay-projections/references/pdl-grammar.md +81 -37
- package/package/corpus/skills/cratis-screenplay-projections/verification.json +6 -1
- package/package/corpus/skills/cratis-screenplay-read-surface/SKILL.md +64 -17
- package/package/corpus/skills/cratis-screenplay-read-surface/verification.json +4 -1
- package/package/corpus/skills/cratis-screenplay-specifications/SKILL.md +201 -115
- package/package/corpus/skills/cratis-screenplay-specifications/verification.json +13 -1
- package/package/corpus/skills/cratis-screenplay-ui-composition/SKILL.md +133 -24
- package/package/corpus/skills/cratis-screenplay-ui-composition/verification.json +4 -1
- package/package.json +1 -1
|
@@ -24,15 +24,17 @@ source is the single flow model.
|
|
|
24
24
|
|
|
25
25
|
| Package | Version | Purpose |
|
|
26
26
|
| --- | --- | --- |
|
|
27
|
-
| `Cratis.Screenplay` | `4.
|
|
27
|
+
| `Cratis.Screenplay` | `4.31.0` | Parser, validator, diagnostics, semantic binder |
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
`Documentation/screenplay/{queries,readmodels,screens}.md`
|
|
31
|
-
|
|
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[]`
|
|
99
|
-
without the marker a query is one-shot, which is the default and what
|
|
100
|
-
are. The marker qualifies only *how* the result arrives, so it composes
|
|
101
|
-
`?`, `by`, `filter`, `authorize` and a `performer
|
|
102
|
-
exactly as it binds to a one-shot one
|
|
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
|
|
108
|
-
query's counterpart to a command's
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
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.
|
|
28
|
+
| `Cratis.Screenplay` | `4.31.0` | Parser, semantic binder, reference execution |
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
`Documentation/screenplay/specifications.md
|
|
32
|
-
|
|
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
|
|
40
|
-
| `given readmodel <ReadModelType>` | prior read-model state, established directly |
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `then
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `then
|
|
48
|
-
| `then error` | a rejection, for
|
|
49
|
-
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
|
188
|
+
## Rejections and denials say different things
|
|
73
189
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
151
|
-
|
|
152
|
-
events, read-model state,
|
|
153
|
-
internal function describes a unit, not a slice acceptance
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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`
|
|
187
|
-
- [ ] `
|
|
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
|
|
194
|
-
- [ ] `then readmodel`
|
|
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
|
-
- [ ]
|
|
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": "
|
|
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
|
}
|