@cratis/pi 0.0.1
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/README.md +43 -0
- package/package.json +24 -0
- package/skills/cratis-application-react-specifications/LICENSE +2 -0
- package/skills/cratis-application-react-specifications/SKILL.md +192 -0
- package/skills/cratis-application-slice-specifications/LICENSE +2 -0
- package/skills/cratis-application-slice-specifications/SKILL.md +222 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/LICENSE +2 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/SKILL.md +233 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/authentication.md +140 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/authorization.md +112 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/frontend.md +102 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/local-development.md +85 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/tenancy.md +87 -0
- package/skills/cratis-arc-command/LICENSE +2 -0
- package/skills/cratis-arc-command/SKILL.md +286 -0
- package/skills/cratis-arc-command/references/command-result.md +111 -0
- package/skills/cratis-arc-command/references/handler-shapes.md +135 -0
- package/skills/cratis-arc-command/references/proxy-generation.md +87 -0
- package/skills/cratis-arc-command/references/read-model-injection.md +97 -0
- package/skills/cratis-arc-command-execution/LICENSE +2 -0
- package/skills/cratis-arc-command-execution/SKILL.md +148 -0
- package/skills/cratis-arc-command-validation/LICENSE +2 -0
- package/skills/cratis-arc-command-validation/SKILL.md +230 -0
- package/skills/cratis-arc-ef-core-migration/LICENSE +2 -0
- package/skills/cratis-arc-ef-core-migration/SKILL.md +231 -0
- package/skills/cratis-arc-observable-query-http/LICENSE +2 -0
- package/skills/cratis-arc-observable-query-http/SKILL.md +157 -0
- package/skills/cratis-arc-query-paging/LICENSE +2 -0
- package/skills/cratis-arc-query-paging/SKILL.md +173 -0
- package/skills/cratis-arc-react-page/LICENSE +2 -0
- package/skills/cratis-arc-react-page/SKILL.md +291 -0
- package/skills/cratis-arc-react-page/references/data-page.md +125 -0
- package/skills/cratis-arc-react-page/references/data-tables.md +136 -0
- package/skills/cratis-arc-react-page/references/dialogs.md +238 -0
- package/skills/cratis-arc-react-page/references/mvvm.md +150 -0
- package/skills/cratis-arc-react-page/references/queries-and-commands.md +200 -0
- package/skills/cratis-chronicle-cli-operations/LICENSE +2 -0
- package/skills/cratis-chronicle-cli-operations/SKILL.md +199 -0
- package/skills/cratis-chronicle-client-dotnet/LICENSE +2 -0
- package/skills/cratis-chronicle-client-dotnet/SKILL.md +433 -0
- package/skills/cratis-chronicle-client-elixir/LICENSE +2 -0
- package/skills/cratis-chronicle-client-elixir/SKILL.md +429 -0
- package/skills/cratis-chronicle-client-kotlin/LICENSE +2 -0
- package/skills/cratis-chronicle-client-kotlin/SKILL.md +498 -0
- package/skills/cratis-chronicle-client-typescript/LICENSE +2 -0
- package/skills/cratis-chronicle-client-typescript/SKILL.md +396 -0
- package/skills/cratis-chronicle-compliance/LICENSE +2 -0
- package/skills/cratis-chronicle-compliance/SKILL.md +246 -0
- package/skills/cratis-chronicle-event-constraints/LICENSE +2 -0
- package/skills/cratis-chronicle-event-constraints/SKILL.md +227 -0
- package/skills/cratis-chronicle-event-modeling/LICENSE +2 -0
- package/skills/cratis-chronicle-event-modeling/SKILL.md +157 -0
- package/skills/cratis-chronicle-event-specifications/LICENSE +2 -0
- package/skills/cratis-chronicle-event-specifications/SKILL.md +200 -0
- package/skills/cratis-chronicle-event-type-migration/LICENSE +2 -0
- package/skills/cratis-chronicle-event-type-migration/SKILL.md +235 -0
- package/skills/cratis-chronicle-mcp-inspection/LICENSE +2 -0
- package/skills/cratis-chronicle-mcp-inspection/SKILL.md +79 -0
- package/skills/cratis-chronicle-mcp-inspection/references/blocked-tools.md +12 -0
- package/skills/cratis-chronicle-mcp-inspection/references/observational-tools.md +8 -0
- package/skills/cratis-chronicle-multi-tenancy/LICENSE +2 -0
- package/skills/cratis-chronicle-multi-tenancy/SKILL.md +198 -0
- package/skills/cratis-chronicle-projection/LICENSE +2 -0
- package/skills/cratis-chronicle-projection/SKILL.md +162 -0
- package/skills/cratis-chronicle-projection/references/fluent-builder.md +126 -0
- package/skills/cratis-chronicle-projection/references/model-bound-attributes.md +118 -0
- package/skills/cratis-chronicle-reactor/LICENSE +2 -0
- package/skills/cratis-chronicle-reactor/SKILL.md +344 -0
- package/skills/cratis-chronicle-read-model/LICENSE +2 -0
- package/skills/cratis-chronicle-read-model/SKILL.md +193 -0
- package/skills/cratis-chronicle-read-model/references/queries.md +142 -0
- package/skills/cratis-chronicle-read-model-specifications/LICENSE +2 -0
- package/skills/cratis-chronicle-read-model-specifications/SKILL.md +176 -0
- package/skills/cratis-chronicle-reducer/LICENSE +2 -0
- package/skills/cratis-chronicle-reducer/SKILL.md +173 -0
- package/skills/cratis-chronicle-web-workbench/LICENSE +2 -0
- package/skills/cratis-chronicle-web-workbench/SKILL.md +177 -0
- package/skills/cratis-cli-terminal-workbench/LICENSE +2 -0
- package/skills/cratis-cli-terminal-workbench/SKILL.md +156 -0
- package/skills/cratis-code-review/LICENSE +2 -0
- package/skills/cratis-code-review/SKILL.md +177 -0
- package/skills/cratis-components-accessibility/LICENSE +2 -0
- package/skills/cratis-components-accessibility/SKILL.md +176 -0
- package/skills/cratis-components-schema-editor/LICENSE +2 -0
- package/skills/cratis-components-schema-editor/SKILL.md +238 -0
- package/skills/cratis-components-stepper-command-dialog/LICENSE +2 -0
- package/skills/cratis-components-stepper-command-dialog/SKILL.md +209 -0
- package/skills/cratis-components-styling/LICENSE +2 -0
- package/skills/cratis-components-styling/SKILL.md +221 -0
- package/skills/cratis-components-toolbar/LICENSE +2 -0
- package/skills/cratis-components-toolbar/SKILL.md +259 -0
- package/skills/cratis-documentation-writing/LICENSE +2 -0
- package/skills/cratis-documentation-writing/SKILL.md +121 -0
- package/skills/cratis-event-model-diagram/LICENSE +2 -0
- package/skills/cratis-event-model-diagram/SKILL.md +142 -0
- package/skills/cratis-fundamentals-concept/LICENSE +2 -0
- package/skills/cratis-fundamentals-concept/SKILL.md +198 -0
- package/skills/cratis-fundamentals-type-discovery/LICENSE +2 -0
- package/skills/cratis-fundamentals-type-discovery/SKILL.md +152 -0
- package/skills/cratis-governed-release-methodology/LICENSE +2 -0
- package/skills/cratis-governed-release-methodology/SKILL.md +297 -0
- package/skills/cratis-lens-browser-extension/LICENSE +2 -0
- package/skills/cratis-lens-browser-extension/SKILL.md +239 -0
- package/skills/cratis-performance-review/LICENSE +2 -0
- package/skills/cratis-performance-review/SKILL.md +139 -0
- package/skills/cratis-screenplay-model-authoring/LICENSE +2 -0
- package/skills/cratis-screenplay-model-authoring/SKILL.md +231 -0
- package/skills/cratis-security-review/LICENSE +2 -0
- package/skills/cratis-security-review/SKILL.md +141 -0
- package/skills/cratis-specification-by-example/LICENSE +2 -0
- package/skills/cratis-specification-by-example/SKILL.md +188 -0
- package/skills/cratis-specifications-csharp/LICENSE +2 -0
- package/skills/cratis-specifications-csharp/SKILL.md +230 -0
- package/skills/cratis-specifications-csharp/references/application-scenarios.md +191 -0
- package/skills/cratis-specifications-csharp/references/csharp-patterns.md +142 -0
- package/skills/cratis-specifications-csharp/references/integration-specs.md +126 -0
- package/skills/cratis-specifications-typescript/LICENSE +2 -0
- package/skills/cratis-specifications-typescript/SKILL.md +202 -0
- package/skills/cratis-specifications-typescript/references/typescript-patterns.md +168 -0
- package/skills/cratis-stage-rendering-and-sandbox/LICENSE +2 -0
- package/skills/cratis-stage-rendering-and-sandbox/SKILL.md +230 -0
- package/skills/cratis-studio-mcp-safety-guidance/LICENSE +2 -0
- package/skills/cratis-studio-mcp-safety-guidance/SKILL.md +69 -0
- package/skills/cratis-studio-mcp-safety-guidance/references/blocked-tools.md +12 -0
- package/skills/cratis-studio-mcp-safety-guidance/references/observational-tools.md +8 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cratis-chronicle-event-constraints
|
|
3
|
+
description: Enforce append-time uniqueness in Chronicle with the [Unique] attribute, [RemoveConstraint], and IConstraint, and specify the violation with EventScenario. Use when a rule must hold at the event store rather than in a command, so two concurrent appends cannot both win. Do not use for ordinary command input validation and do not use to create a projection.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Chronicle event constraints
|
|
8
|
+
|
|
9
|
+
A constraint is an **append-time** invariant. Chronicle evaluates it inside the
|
|
10
|
+
append, against the event store's own index — so it is the only mechanism that
|
|
11
|
+
holds when two requests race. A command-side pre-check reads state that may
|
|
12
|
+
already be stale by the time the append lands; a constraint cannot be.
|
|
13
|
+
|
|
14
|
+
Constraints are discovered automatically. There is no registration call.
|
|
15
|
+
|
|
16
|
+
## Verified product sources
|
|
17
|
+
|
|
18
|
+
| Package | Version | Purpose |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| `Cratis.Chronicle` (client) | `16.45.2` | `[Unique]`, `[RemoveConstraint]`, `IConstraint`, `IConstraintBuilder`, `IUniqueConstraintBuilder`, `ConstraintViolation`, `IAppendResult` |
|
|
21
|
+
| `Cratis.Chronicle` code analysis | `16.45.2` | `CHR0017`, `CHR0018`, `CHR0020` |
|
|
22
|
+
| `Cratis.Chronicle.Testing` | `16.45.2` | `EventScenario` and the `IAppendResult` `Should*` assertions |
|
|
23
|
+
|
|
24
|
+
Reverify against the Chronicle repository before claiming support for another
|
|
25
|
+
version. Never translate an attribute argument order or an assertion name from
|
|
26
|
+
memory.
|
|
27
|
+
|
|
28
|
+
## Route near misses
|
|
29
|
+
|
|
30
|
+
- The rule rejects **command input** — a range, a format, a cross-field
|
|
31
|
+
condition: use `cratis-arc-command-validation`.
|
|
32
|
+
- The rule is state-dependent and belongs in the handler under concurrency (the
|
|
33
|
+
DCB pattern): also `cratis-arc-command-validation`.
|
|
34
|
+
- You are creating or changing a read model that the rule reads: use
|
|
35
|
+
`cratis-chronicle-read-model`.
|
|
36
|
+
- You are writing the specification and want the `EventScenario` mechanics in
|
|
37
|
+
depth: use `cratis-chronicle-event-specifications`.
|
|
38
|
+
|
|
39
|
+
## Step 1 — Pick the mechanism
|
|
40
|
+
|
|
41
|
+
| Situation | Use |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| One event of this type per event source | class-level `[Unique]` on the event type |
|
|
44
|
+
| A property value unique across event sources | property-level `[Unique]` on that property |
|
|
45
|
+
| Release a claimed value when something is removed | `[RemoveConstraint("name")]` on the removal event |
|
|
46
|
+
| The same value must be unique across **several** event types | `IConstraint` with one `[Unique(name: …)]` name, or a fluent `Unique(…)` |
|
|
47
|
+
| Case-insensitive matching, composite keys, or a scoped constraint | `IConstraint` |
|
|
48
|
+
|
|
49
|
+
**Per-event-source versus across-event-sources is the constraint *kind*, not a
|
|
50
|
+
setting.** Class-level `[Unique]` (and the generic `Unique<TEvent>()` builder
|
|
51
|
+
call) means "at most one such event per event source id". Property-level
|
|
52
|
+
`[Unique]` (and `Unique(unique => unique.On<T>(…))`) means "this value is unique
|
|
53
|
+
across event sources". Choosing the wrong one gives a constraint that never
|
|
54
|
+
fires rather than a compile error.
|
|
55
|
+
|
|
56
|
+
## Step 2 — `[Unique]` — the default
|
|
57
|
+
|
|
58
|
+
```csharp
|
|
59
|
+
// Event-type uniqueness — only one of this event per event source
|
|
60
|
+
[EventType]
|
|
61
|
+
[Unique(message: "A project with this name already exists.")]
|
|
62
|
+
public record ProjectRegistered(ProjectName Name);
|
|
63
|
+
|
|
64
|
+
// Property uniqueness — the value must be unique across event sources
|
|
65
|
+
[EventType]
|
|
66
|
+
public record UserRegistered([Unique(name: "UniqueEmail", message: "Email already registered.")] EmailAddress Email, DisplayName Name);
|
|
67
|
+
|
|
68
|
+
// Release a claimed value on removal — the name must match the [Unique] name exactly
|
|
69
|
+
[EventType]
|
|
70
|
+
[RemoveConstraint("UniqueEmail")]
|
|
71
|
+
public record UserRemoved;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- `UniqueAttribute`'s constructor is `(string? name = default, string? message = default)` —
|
|
75
|
+
**name first, message second**. Pass them by name, as above, so a future
|
|
76
|
+
reader cannot misread the order.
|
|
77
|
+
- The attribute targets `Class | Property`, `AllowMultiple = false`.
|
|
78
|
+
- **The name defaults to the member or type name** when omitted. Give it an
|
|
79
|
+
explicit name whenever a removal event or another event type has to refer to
|
|
80
|
+
it, because the string in `[RemoveConstraint]` must match exactly.
|
|
81
|
+
- `RemoveConstraintAttribute` is class-only with `AllowMultiple = true`. Stack
|
|
82
|
+
several on one removal event to release several claimed values at once.
|
|
83
|
+
- To enforce the same property across **multiple** event types, give each its
|
|
84
|
+
`[Unique(name: "UniqueEmail")]` with the **same name**.
|
|
85
|
+
- Several removal events may declare the same name; **any one of them releases
|
|
86
|
+
the claim on its own**.
|
|
87
|
+
|
|
88
|
+
## Step 3 — `IConstraint` — the advanced form
|
|
89
|
+
|
|
90
|
+
Reach for `IConstraint` when uniqueness spans event types with different
|
|
91
|
+
property names, needs `.IgnoreCasing()`, covers a composite of properties, or a
|
|
92
|
+
`RemovedWith` event must release it.
|
|
93
|
+
|
|
94
|
+
```csharp
|
|
95
|
+
public class UniqueProjectName : IConstraint
|
|
96
|
+
{
|
|
97
|
+
public void Define(IConstraintBuilder builder) =>
|
|
98
|
+
builder.Unique(unique =>
|
|
99
|
+
unique
|
|
100
|
+
.On<ProjectRegistered>(e => e.Name)
|
|
101
|
+
.IgnoreCasing() // case-insensitive — do NOT lowercase inside the lambda
|
|
102
|
+
.RemovedWith<ProjectRemoved>()); // omit if there is no remove event
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`IUniqueConstraintBuilder` also carries `WithName(name)`, `WithMessage(message)`,
|
|
107
|
+
and an `On(EventType, string[])` overload. `On<TEvent>` is **variadic** —
|
|
108
|
+
`On<T>(e => e.Tenant, e => e.Email)` constrains the composite of both
|
|
109
|
+
properties, not two independent values.
|
|
110
|
+
|
|
111
|
+
`IConstraintBuilder` additionally offers `PerEventSourceType()`,
|
|
112
|
+
`PerEventStreamType()`, and `PerEventStreamId()` to narrow the scope a
|
|
113
|
+
uniqueness claim is keyed by, and a `Unique<TEvent>(message, name)` overload for
|
|
114
|
+
event-type uniqueness. **That overload takes the message first and the name
|
|
115
|
+
second** — the reverse of the `[Unique]` attribute. Both parameters convert
|
|
116
|
+
implicitly from `string`, so the wrong order compiles and silently swaps them.
|
|
117
|
+
Always pass them by name.
|
|
118
|
+
|
|
119
|
+
`ConstraintScope` is a record of three optional dimensions, not an enum, and you
|
|
120
|
+
never construct it — the three `Per…` calls select which dimensions participate.
|
|
121
|
+
|
|
122
|
+
## Step 4 — Keep `Define` declarative
|
|
123
|
+
|
|
124
|
+
`Define` builds a rule; it never runs one. Three analyzers enforce that, all at
|
|
125
|
+
error severity:
|
|
126
|
+
|
|
127
|
+
| Id | Rejects |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `CHR0017` | Injecting `IEventLog` or `ICommandPipeline` into a constraint |
|
|
130
|
+
| `CHR0018` | An imperative statement in `Define` — `if`, loops, `switch`, `return`, `throw`, a local declaration, an assignment |
|
|
131
|
+
| `CHR0020` | An expression lambda that is not a pure member-access chain |
|
|
132
|
+
|
|
133
|
+
`CHR0020` is the one that bites: `e => e.Email.ToLower()` is a method call, so
|
|
134
|
+
it is rejected. Use `.IgnoreCasing()` instead. Suppressing the analyzer does not
|
|
135
|
+
make it work — the runtime path extracts an **empty** property path from a
|
|
136
|
+
method call and then fails at startup with `PropertyDoesNotExistOnEventType`,
|
|
137
|
+
which is a much harder failure to read.
|
|
138
|
+
|
|
139
|
+
## Step 5 — Treat a violation as a result, never as an exception
|
|
140
|
+
|
|
141
|
+
A violated constraint comes back on the append result, not as a throw. On
|
|
142
|
+
`IAppendResult`: `IsSuccess`, `HasConstraintViolations`,
|
|
143
|
+
`HasConcurrencyViolations`, `HasErrors`, `ConstraintViolations`, and `Errors`.
|
|
144
|
+
Each `ConstraintViolation` carries `ConstraintName`, `ConstraintType`,
|
|
145
|
+
`Message`, `Details`, the `EventTypeId` and the `SequenceNumber`. Arc surfaces
|
|
146
|
+
it as a validation error on the command result.
|
|
147
|
+
|
|
148
|
+
Note the asymmetry: `AppendResult` exposes a singular nullable
|
|
149
|
+
`ConcurrencyViolation`, while `AppendManyResult` exposes a plural
|
|
150
|
+
`ConcurrencyViolations`. Only the `HasConcurrencyViolations` flag is on the
|
|
151
|
+
interface.
|
|
152
|
+
|
|
153
|
+
⚠️ A `[PII]` property's value is encrypted before the constraint index sees it,
|
|
154
|
+
so the claimed value and the violation details hold the encrypted form.
|
|
155
|
+
Uniqueness still works; a human reading the index will not see the plaintext.
|
|
156
|
+
|
|
157
|
+
## Step 6 — Specify the violation
|
|
158
|
+
|
|
159
|
+
Specify the constraint with `EventScenario`: seed the conflicting state through
|
|
160
|
+
`Given`, append again, and assert
|
|
161
|
+
`ShouldHaveConstraintViolationFor(<Module>ConstraintNames.UniqueX)` — **the
|
|
162
|
+
constraint name, never the message**. A message is presentation text and will
|
|
163
|
+
change without the behavior changing.
|
|
164
|
+
|
|
165
|
+
```csharp
|
|
166
|
+
#if DEBUG
|
|
167
|
+
public class and_the_name_already_exists : Specification
|
|
168
|
+
{
|
|
169
|
+
EventScenario _scenario;
|
|
170
|
+
IAppendResult _result;
|
|
171
|
+
|
|
172
|
+
async Task Establish()
|
|
173
|
+
{
|
|
174
|
+
_scenario = new EventScenario();
|
|
175
|
+
await _scenario.Given
|
|
176
|
+
.ForEventSource(ProjectId.New())
|
|
177
|
+
.Events(new ProjectRegistered("Acme"));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async Task Because() =>
|
|
181
|
+
_result = await _scenario.EventLog.Append(ProjectId.New(), new ProjectRegistered("Acme"));
|
|
182
|
+
|
|
183
|
+
[Fact] void should_be_failed() => _result.ShouldBeFailed();
|
|
184
|
+
[Fact] void should_have_constraint_violation_for_unique_name() =>
|
|
185
|
+
_result.ShouldHaveConstraintViolationFor(ProjectConstraintNames.UniqueProjectName);
|
|
186
|
+
}
|
|
187
|
+
#endif
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Use per-test unique values (`$"{Guid.NewGuid():N}"`) so specifications are not
|
|
191
|
+
order-dependent. For a release, append the removal event and then assert the
|
|
192
|
+
value can be claimed again.
|
|
193
|
+
|
|
194
|
+
⚠️ **`EventScenario` disables concurrency checking.** It wires a no-op
|
|
195
|
+
concurrency-scope strategy, so `ShouldHaveConcurrencyViolations()` can never
|
|
196
|
+
become true through the in-process scenario. Constraint violations are fully
|
|
197
|
+
exercisable there; concurrency violations need the real kernel.
|
|
198
|
+
|
|
199
|
+
## What breaks
|
|
200
|
+
|
|
201
|
+
- **The constraint never fires.** You used class-level `[Unique]` for a value
|
|
202
|
+
that has to be unique across event sources, or property-level `[Unique]` for
|
|
203
|
+
a "one per event source" rule. Re-read Step 1 — the kind is the switch.
|
|
204
|
+
- **`[RemoveConstraint]` releases nothing.** The string does not match the
|
|
205
|
+
`[Unique]` name. When `[Unique]` had no explicit name the real name is the
|
|
206
|
+
member or type name, which is easy to get wrong; name it explicitly.
|
|
207
|
+
- **`PropertyDoesNotExistOnEventType` at startup.** A non-member-access lambda
|
|
208
|
+
reached the builder — the `CHR0020` case with the analyzer suppressed.
|
|
209
|
+
- **`MissingNameForUniqueConstraint` / `NoEventTypesAddedToUniqueConstraint`.**
|
|
210
|
+
The fluent builder was built without a name, or without any `On` call.
|
|
211
|
+
- **`NoUniqueEventTypeConstraintToRemove`.** `IConstraintBuilder.RemovedWith<T>()`
|
|
212
|
+
was called with no preceding `Unique<TEvent>()`. That method attaches to the
|
|
213
|
+
last event-type constraint; the fluent one on `IUniqueConstraintBuilder` is a
|
|
214
|
+
different method.
|
|
215
|
+
|
|
216
|
+
## Verify
|
|
217
|
+
|
|
218
|
+
- Every constraint is discovered without a registration call.
|
|
219
|
+
- `[Unique]` arguments are passed by name, and every name a removal event refers
|
|
220
|
+
to is explicit rather than defaulted.
|
|
221
|
+
- `Define` contains no imperative statement, no injected effect dependency, and
|
|
222
|
+
no lambda that is not a pure member-access chain — `CHR0017`, `CHR0018` and
|
|
223
|
+
`CHR0020` are all clean.
|
|
224
|
+
- Every constraint has an `EventScenario` specification that seeds the conflict
|
|
225
|
+
and asserts `ShouldHaveConstraintViolationFor(<name>)` on the name.
|
|
226
|
+
- No specification asserts on a violation message string.
|
|
227
|
+
- Build clean in Debug and Release, and the specifications pass.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cratis-chronicle-event-modeling
|
|
3
|
+
description: Design a Chronicle event model before writing code - stream boundaries and event-source identities, commands, past-tense events, read models, automations and translations, compliance subjects, and the specification outline. Use when behavior, event vocabulary, stream boundaries, or a multi-slice flow is not yet settled. Do not use to draw or update an existing model diagram, and do not use for a mechanical change to a flow that is already decided.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Chronicle event modeling
|
|
8
|
+
|
|
9
|
+
Use this **before writing code**, when behavior, event vocabulary, stream
|
|
10
|
+
boundaries, or a multi-step flow is not already settled. The output is an
|
|
11
|
+
implementation brief: which commands exist, which stream each event lands on,
|
|
12
|
+
which read models consume those events, which automations and translations react,
|
|
13
|
+
and which specifications prove the flow.
|
|
14
|
+
|
|
15
|
+
Skip it for a mechanical change where the event types and the flow already exist
|
|
16
|
+
and the request is wiring or a narrow fix.
|
|
17
|
+
|
|
18
|
+
> **Lineage.** The four behavior types and the given/when/then-per-behavior
|
|
19
|
+
> discipline follow **Event Modeling** (Adam Dymitruk; Martin Dilger,
|
|
20
|
+
> *Understanding Eventsourcing*). This skill applies that method to Chronicle.
|
|
21
|
+
|
|
22
|
+
## Verified product sources
|
|
23
|
+
|
|
24
|
+
This skill is verified against these exact sources:
|
|
25
|
+
|
|
26
|
+
| Package | Version | Purpose |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| `Cratis.Chronicle` | `16.45.2` | event types, event-source identities, subjects, `[PII]` |
|
|
29
|
+
| `Cratis.Chronicle.CodeAnalysis` | `16.45.2` | `CHR0012`, `CHR0026`, `CHR0034`, `CHR0035` |
|
|
30
|
+
|
|
31
|
+
Reverify product sources before claiming support for another version.
|
|
32
|
+
|
|
33
|
+
## The four behavior types
|
|
34
|
+
|
|
35
|
+
Classify every behavior in the model as exactly one:
|
|
36
|
+
|
|
37
|
+
| Type | What it does | Built from |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| **State change** | accepts a command, appends events | command, validation, events |
|
|
40
|
+
| **State view** | projects events into a queryable read model | read model plus a projection or a reducer |
|
|
41
|
+
| **Automation** | reacts to events and causes an external side effect | a reactor |
|
|
42
|
+
| **Translation** | reacts to events and appends follow-up events elsewhere | a reactor |
|
|
43
|
+
|
|
44
|
+
Automation and translation differ in the *result*: an automation reaches out of
|
|
45
|
+
the system, a translation stays inside it and produces new facts.
|
|
46
|
+
|
|
47
|
+
## Decide before implementing
|
|
48
|
+
|
|
49
|
+
- **Behavior name and type** for each step in the flow.
|
|
50
|
+
- **Commands** — their inputs and the authorization that gates them. Commands
|
|
51
|
+
are imperative intents.
|
|
52
|
+
- **Events** — past-tense, one-purpose facts. Names must be self-describing:
|
|
53
|
+
`<Entity><PastTenseVerb>`, never a bare `Created`.
|
|
54
|
+
- **The event-source id for every event.** Events never carry their own
|
|
55
|
+
event-source id as a payload property; it lives in the event context. Deciding
|
|
56
|
+
the id *is* deciding the stream boundary.
|
|
57
|
+
- **Event properties are non-nullable.** Analyzer `CHR0012` warns otherwise.
|
|
58
|
+
Model an optional fact as a separate event, not a nullable field.
|
|
59
|
+
- **Read models** — their consumers and their source events, and whether the
|
|
60
|
+
model is projection-backed, reducer-backed, or `[Passive]` for a
|
|
61
|
+
strongly-consistent command-side decision.
|
|
62
|
+
- **Automations and translations** — which events they react to, whether they
|
|
63
|
+
emit follow-up events or run commands, and which side effects must not repeat.
|
|
64
|
+
- **Specifications** — the happy path, the validation failures, the constraints,
|
|
65
|
+
the projections and reducers, and the reactor side effects.
|
|
66
|
+
|
|
67
|
+
Do not append an event for derived or aggregate state. Project it from the source
|
|
68
|
+
events instead; an event that records a total is a fact that can disagree with
|
|
69
|
+
its own inputs.
|
|
70
|
+
|
|
71
|
+
## Information completeness — trace every field to an event
|
|
72
|
+
|
|
73
|
+
This is the check that pays for itself, and it belongs at modeling time rather
|
|
74
|
+
than after the projection misbehaves.
|
|
75
|
+
|
|
76
|
+
- **Backward:** for each read model, walk every property back to the event that
|
|
77
|
+
carries it. A field with no source event is a **missing event or command** —
|
|
78
|
+
not a nullable column. Resolve it in the model.
|
|
79
|
+
- **Forward:** every event you define should feed at least one read model,
|
|
80
|
+
automation, or translation. An event nothing consumes is a smell: either a
|
|
81
|
+
consumer is missing or the event should not exist.
|
|
82
|
+
|
|
83
|
+
If a field can only be filled by reaching into another behavior's read model,
|
|
84
|
+
you have found a missing event or a wrong stream boundary. Fix the model; do not
|
|
85
|
+
cross-read at runtime.
|
|
86
|
+
|
|
87
|
+
## Compliance modeling
|
|
88
|
+
|
|
89
|
+
Decide this *before* choosing event and read-model shapes, because erasure
|
|
90
|
+
operates on the subject and the subject follows the stream.
|
|
91
|
+
|
|
92
|
+
- Prefer **one subject per event stream** for person-level personal data. The
|
|
93
|
+
subject then defaults correctly to the event-source id and needs no attribute.
|
|
94
|
+
- Mark inherently personal values with `[PII]` — names, email addresses, phone
|
|
95
|
+
numbers, identity-provider subjects, personal notes. Leave business metadata
|
|
96
|
+
unmarked; every marked value becomes unreadable once the subject is erased.
|
|
97
|
+
- Record the rationale with `[ComplianceDetails("...")]`. A string argument
|
|
98
|
+
passed to `[PII]` is **not** read by Chronicle.
|
|
99
|
+
- Set `[Subject]` only when the person is not the event source. A stored
|
|
100
|
+
read-model document carries one subject — do not mix several people's personal
|
|
101
|
+
data into one document.
|
|
102
|
+
- Never place `[PII]` on an event-source id: Chronicle cannot encrypt it, and
|
|
103
|
+
analyzer `CHR0034` rejects it. When the natural identifier is itself
|
|
104
|
+
sensitive, model a surrogate stream id and carry the sensitive value as a
|
|
105
|
+
`[PII]` property.
|
|
106
|
+
- Do not place `[Key]` or `[Subject]` on an `EventSourceId<T>` value — it is
|
|
107
|
+
already both, and analyzer `CHR0026` says so.
|
|
108
|
+
- Bearer tokens, magic links, and signed URLs are not durable facts. Model a
|
|
109
|
+
keyed hash or an opaque reference, never the secret. Chronicle has no attribute
|
|
110
|
+
that withholds a value from the log.
|
|
111
|
+
|
|
112
|
+
If a subject boundary cannot be made person-level without changing product
|
|
113
|
+
behavior, **stop and surface that trade-off** before implementing.
|
|
114
|
+
|
|
115
|
+
## Output shape
|
|
116
|
+
|
|
117
|
+
Write the brief in this order:
|
|
118
|
+
|
|
119
|
+
1. stream boundaries and subjects
|
|
120
|
+
2. commands and events
|
|
121
|
+
3. read models and their consumers
|
|
122
|
+
4. automations and translations
|
|
123
|
+
5. compliance notes
|
|
124
|
+
6. specifications
|
|
125
|
+
|
|
126
|
+
## The lifecycle: draft, ready, working, done
|
|
127
|
+
|
|
128
|
+
The behavior is the unit of work, and it moves through four states. The model —
|
|
129
|
+
events, commands, read models, screens, specifications — lives inside it, and
|
|
130
|
+
anyone may author or update it.
|
|
131
|
+
|
|
132
|
+
- **Draft** — being modeled; events, commands, read models, and boundaries are
|
|
133
|
+
still in flux.
|
|
134
|
+
- **Ready** — the handoff gate. The model is *information-complete*: the checks
|
|
135
|
+
above pass, and commands, authorization, compliance, and the specification
|
|
136
|
+
outline are all decided. A ready behavior can be implemented with **no further
|
|
137
|
+
modeling decisions**.
|
|
138
|
+
- **Working** — an implementer has picked it up and is running the
|
|
139
|
+
implementation workflow end to end.
|
|
140
|
+
- **Done** — every quality gate is green.
|
|
141
|
+
|
|
142
|
+
**Marking a behavior ready is the signal to implement it.** Do not batch ready
|
|
143
|
+
items behind one another. Branching, pull requests, merging, and publication
|
|
144
|
+
remain separately authorized; a ready model authorizes none of them.
|
|
145
|
+
|
|
146
|
+
## Verify the brief before handing it over
|
|
147
|
+
|
|
148
|
+
- Every behavior has exactly one type.
|
|
149
|
+
- Every event is past-tense, self-describing, single-purpose, and non-nullable.
|
|
150
|
+
- Every event has a decided event-source id, and none carries that id as a
|
|
151
|
+
property.
|
|
152
|
+
- Every read-model field traces back to an event.
|
|
153
|
+
- Every event has at least one consumer.
|
|
154
|
+
- No behavior depends on reading another behavior's read model at runtime.
|
|
155
|
+
- Personal data has a decided subject, and the subject is person-level or the
|
|
156
|
+
trade-off is surfaced.
|
|
157
|
+
- The specification outline names the failures, not only the happy path.
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cratis-chronicle-event-specifications
|
|
3
|
+
description: Specify event-append behavior with EventScenario from Cratis.Chronicle.Testing — seeding prior events through Given, appending through When or the event log, and asserting on the append result including constraint violations. Use when the behavior under specification is what reaches the event store. Do not use for command handling or for read-model projection behavior.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Chronicle event specifications
|
|
8
|
+
|
|
9
|
+
`EventScenario` runs the append path in-process. No Chronicle server, no
|
|
10
|
+
database, no network — an empty event log at sequence number zero, with the
|
|
11
|
+
application's constraints discovered and active.
|
|
12
|
+
|
|
13
|
+
## Verified product sources
|
|
14
|
+
|
|
15
|
+
| Package | Version | Purpose |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `Cratis.Chronicle.Testing` | `16.45.2` | `EventScenario`, its `Given`/`When` builders, the `IAppendResult` `Should*` assertions |
|
|
18
|
+
| `Cratis.Chronicle` | `16.45.2` | `IEventSequence.Append`/`AppendMany`, `IAppendResult`, `ConstraintViolation` |
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
dotnet add package Cratis.Chronicle.Testing
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Reverify against the Chronicle repository before claiming support for another
|
|
25
|
+
version. Never translate an assertion name from memory.
|
|
26
|
+
|
|
27
|
+
## Route near misses
|
|
28
|
+
|
|
29
|
+
- The behavior is a command running through validators, `Provide()` and
|
|
30
|
+
`Handle()`: use `cratis-application-slice-specifications`.
|
|
31
|
+
- The behavior is a projection or reducer building a read model: use
|
|
32
|
+
`cratis-chronicle-read-model-specifications`.
|
|
33
|
+
- You are deciding what the constraint should *be*: use
|
|
34
|
+
`cratis-chronicle-event-constraints`.
|
|
35
|
+
- The specification style itself — folder shape, naming, what not to specify:
|
|
36
|
+
use `cratis-specification-by-example`.
|
|
37
|
+
|
|
38
|
+
## When you need this
|
|
39
|
+
|
|
40
|
+
- A constraint must reject a duplicate or conflicting append.
|
|
41
|
+
- A batch append (`AppendMany`) must behave as one unit.
|
|
42
|
+
- The event log's own behavior is the subject, not the command that reached it.
|
|
43
|
+
|
|
44
|
+
## When you do not
|
|
45
|
+
|
|
46
|
+
- **Concurrency violations cannot be provoked here.** `EventScenario` wires a
|
|
47
|
+
no-op concurrency-scope strategy, so `HasConcurrencyViolations` is never true
|
|
48
|
+
inside the scenario however the append is shaped. Specify concurrency against
|
|
49
|
+
the real kernel with an out-of-process integration specification, and do not
|
|
50
|
+
write a scenario specification that looks like it covers it — it would pass
|
|
51
|
+
vacuously, which is worse than not having one.
|
|
52
|
+
|
|
53
|
+
## Step 1 — Create the scenario
|
|
54
|
+
|
|
55
|
+
A new `EventScenario` per specification, never shared state.
|
|
56
|
+
|
|
57
|
+
```csharp
|
|
58
|
+
var scenario = new EventScenario();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The parameterless constructor uses the default event log, an in-memory event
|
|
62
|
+
store, the `default` namespace, and **auto-discovers the constraints** declared
|
|
63
|
+
in the loaded assemblies — which is what makes a constraint specification work
|
|
64
|
+
without wiring. Pass an `ICanProvideConstraints` to the single-argument
|
|
65
|
+
constructor when the specification needs a narrower set.
|
|
66
|
+
|
|
67
|
+
## Step 2 — Seed prior state through `Given`
|
|
68
|
+
|
|
69
|
+
```csharp
|
|
70
|
+
await scenario.Given
|
|
71
|
+
.ForEventSource(authorId)
|
|
72
|
+
.Events(new AuthorRegistered("Jane Smith"), new BookAdded("Clean Code"));
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Chain a second `ForEventSource` for a different event source:
|
|
76
|
+
|
|
77
|
+
```csharp
|
|
78
|
+
await scenario.Given.ForEventSource(author1Id).Events(new AuthorRegistered("Jane Smith"));
|
|
79
|
+
await scenario.Given.ForEventSource(author2Id).Events(new AuthorRegistered("John Doe"));
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- `Given` returns a bare `Task` — it carries no result, because seeding is not
|
|
83
|
+
the action under specification.
|
|
84
|
+
- Seed **before** the act phase; seeded events take monotonically increasing
|
|
85
|
+
sequence numbers.
|
|
86
|
+
- Never put the act under test inside `Given`. Only the world as it already is
|
|
87
|
+
belongs there.
|
|
88
|
+
|
|
89
|
+
## Step 3 — Append, and hold the result
|
|
90
|
+
|
|
91
|
+
Two equivalent surfaces. `When` reads as the act phase; the event log is the
|
|
92
|
+
direct one.
|
|
93
|
+
|
|
94
|
+
```csharp
|
|
95
|
+
// The act-phase builder — returns Task<AppendResult>
|
|
96
|
+
_result = await scenario.When
|
|
97
|
+
.ForEventSource(authorId)
|
|
98
|
+
.Events(new AuthorRegistered("Jane Smith"));
|
|
99
|
+
|
|
100
|
+
// The event log directly
|
|
101
|
+
_result = await scenario.EventLog.Append(authorId, new AuthorRegistered("Jane Smith"));
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`When…Events(@event, params object[] additionalEvents)` takes the first event
|
|
105
|
+
positionally and **stops at the first non-successful append**, so a specification
|
|
106
|
+
that appends several events asserts on the first failure rather than the last.
|
|
107
|
+
|
|
108
|
+
`AppendMany` on the event log appends a batch for one event source:
|
|
109
|
+
|
|
110
|
+
```csharp
|
|
111
|
+
_result = await scenario.EventLog.AppendMany(cartId, [
|
|
112
|
+
new ItemAddedToCart(itemId1),
|
|
113
|
+
new ItemAddedToCart(itemId2)
|
|
114
|
+
]);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`Append` and `AppendMany` are members of the event sequence reached through
|
|
118
|
+
`scenario.EventLog`, not of `EventScenario` itself.
|
|
119
|
+
|
|
120
|
+
## Step 4 — Assert on the result
|
|
121
|
+
|
|
122
|
+
Every assertion below is an extension on `IAppendResult` and throws
|
|
123
|
+
`AppendResultAssertionException` on failure, with a message listing the
|
|
124
|
+
violations and errors it found.
|
|
125
|
+
|
|
126
|
+
| Method | Asserts |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| `ShouldBeSuccessful()` | No violation and no error |
|
|
129
|
+
| `ShouldBeFailed()` | Any violation or error |
|
|
130
|
+
| `ShouldHaveConstraintViolations()` | At least one constraint violation |
|
|
131
|
+
| `ShouldNotHaveConstraintViolations()` | No constraint violation |
|
|
132
|
+
| `ShouldHaveConstraintViolationFor(name)` | A violation for the named constraint |
|
|
133
|
+
| `ShouldHaveConstraintViolation(name)` | The same assertion, singular spelling |
|
|
134
|
+
| `ShouldHaveErrors()` | At least one error |
|
|
135
|
+
| `ShouldNotHaveErrors()` | No error |
|
|
136
|
+
|
|
137
|
+
`ShouldHaveConcurrencyViolations()` and `ShouldNotHaveConcurrencyViolations()`
|
|
138
|
+
also exist, but see *When you do not* above — the first can never pass inside a
|
|
139
|
+
scenario and the second passes vacuously.
|
|
140
|
+
|
|
141
|
+
The constraint name parameter is a `ConstraintName`, which converts implicitly
|
|
142
|
+
from `string`, so a literal compiles. Prefer a shared constant
|
|
143
|
+
(`<Module>ConstraintNames.UniqueX`) so a rename moves both sides at once.
|
|
144
|
+
**Assert the name, never the message.**
|
|
145
|
+
|
|
146
|
+
## Step 5 — Write the specification
|
|
147
|
+
|
|
148
|
+
Specification files live in the slice folder, wrapped in `#if DEBUG … #endif` so
|
|
149
|
+
specification code ships only in Debug.
|
|
150
|
+
|
|
151
|
+
```csharp
|
|
152
|
+
#if DEBUG
|
|
153
|
+
namespace MyApp.Library.Authors.when_registering;
|
|
154
|
+
|
|
155
|
+
public class and_the_name_already_exists : Specification
|
|
156
|
+
{
|
|
157
|
+
EventScenario _scenario;
|
|
158
|
+
IAppendResult _result;
|
|
159
|
+
|
|
160
|
+
async Task Establish()
|
|
161
|
+
{
|
|
162
|
+
_scenario = new EventScenario();
|
|
163
|
+
await _scenario.Given
|
|
164
|
+
.ForEventSource(AuthorId.New())
|
|
165
|
+
.Events(new AuthorRegistered("Jane Smith"));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async Task Because() =>
|
|
169
|
+
_result = await _scenario.EventLog.Append(AuthorId.New(), new AuthorRegistered("Jane Smith"));
|
|
170
|
+
|
|
171
|
+
[Fact] void should_be_failed() => _result.ShouldBeFailed();
|
|
172
|
+
[Fact] void should_have_constraint_violation_for_unique_name() =>
|
|
173
|
+
_result.ShouldHaveConstraintViolationFor(AuthorConstraintNames.UniqueAuthorName);
|
|
174
|
+
}
|
|
175
|
+
#endif
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Use per-specification unique values (`$"{Guid.NewGuid():N}"`) so the suite does
|
|
179
|
+
not depend on execution order. For a release, append the removal event and then
|
|
180
|
+
assert the value can be claimed again.
|
|
181
|
+
|
|
182
|
+
## What breaks
|
|
183
|
+
|
|
184
|
+
- **The duplicate append succeeds.** The constraint was not discovered — the
|
|
185
|
+
scenario was given an explicit constraint provider that excludes it, or the
|
|
186
|
+
constraint type is not in a loaded assembly.
|
|
187
|
+
- **`ShouldHaveConstraintViolationFor` fails with an empty violation list on a
|
|
188
|
+
batch append.** `When…Events` short-circuits, so the conflict may be on an
|
|
189
|
+
event the append never reached.
|
|
190
|
+
- **A concurrency specification is always green.** It is vacuous — the scenario
|
|
191
|
+
disables the check. Delete it or move it out-of-process.
|
|
192
|
+
- **The specification passes alone and fails in the suite.** A hard-coded unique
|
|
193
|
+
value collided with another specification's claim.
|
|
194
|
+
|
|
195
|
+
## How it is proven
|
|
196
|
+
|
|
197
|
+
`dotnet build` in Debug (which compiles the `#if DEBUG` specification code) and
|
|
198
|
+
`dotnet test`, both clean. A constraint specification is only meaningful if it
|
|
199
|
+
has been seen to fail: remove the seeding `Given` once and confirm the
|
|
200
|
+
specification goes red before trusting it green.
|