@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.
Files changed (125) hide show
  1. package/README.md +43 -0
  2. package/package.json +24 -0
  3. package/skills/cratis-application-react-specifications/LICENSE +2 -0
  4. package/skills/cratis-application-react-specifications/SKILL.md +192 -0
  5. package/skills/cratis-application-slice-specifications/LICENSE +2 -0
  6. package/skills/cratis-application-slice-specifications/SKILL.md +222 -0
  7. package/skills/cratis-arc-authentication-authorization-and-identity/LICENSE +2 -0
  8. package/skills/cratis-arc-authentication-authorization-and-identity/SKILL.md +233 -0
  9. package/skills/cratis-arc-authentication-authorization-and-identity/references/authentication.md +140 -0
  10. package/skills/cratis-arc-authentication-authorization-and-identity/references/authorization.md +112 -0
  11. package/skills/cratis-arc-authentication-authorization-and-identity/references/frontend.md +102 -0
  12. package/skills/cratis-arc-authentication-authorization-and-identity/references/local-development.md +85 -0
  13. package/skills/cratis-arc-authentication-authorization-and-identity/references/tenancy.md +87 -0
  14. package/skills/cratis-arc-command/LICENSE +2 -0
  15. package/skills/cratis-arc-command/SKILL.md +286 -0
  16. package/skills/cratis-arc-command/references/command-result.md +111 -0
  17. package/skills/cratis-arc-command/references/handler-shapes.md +135 -0
  18. package/skills/cratis-arc-command/references/proxy-generation.md +87 -0
  19. package/skills/cratis-arc-command/references/read-model-injection.md +97 -0
  20. package/skills/cratis-arc-command-execution/LICENSE +2 -0
  21. package/skills/cratis-arc-command-execution/SKILL.md +148 -0
  22. package/skills/cratis-arc-command-validation/LICENSE +2 -0
  23. package/skills/cratis-arc-command-validation/SKILL.md +230 -0
  24. package/skills/cratis-arc-ef-core-migration/LICENSE +2 -0
  25. package/skills/cratis-arc-ef-core-migration/SKILL.md +231 -0
  26. package/skills/cratis-arc-observable-query-http/LICENSE +2 -0
  27. package/skills/cratis-arc-observable-query-http/SKILL.md +157 -0
  28. package/skills/cratis-arc-query-paging/LICENSE +2 -0
  29. package/skills/cratis-arc-query-paging/SKILL.md +173 -0
  30. package/skills/cratis-arc-react-page/LICENSE +2 -0
  31. package/skills/cratis-arc-react-page/SKILL.md +291 -0
  32. package/skills/cratis-arc-react-page/references/data-page.md +125 -0
  33. package/skills/cratis-arc-react-page/references/data-tables.md +136 -0
  34. package/skills/cratis-arc-react-page/references/dialogs.md +238 -0
  35. package/skills/cratis-arc-react-page/references/mvvm.md +150 -0
  36. package/skills/cratis-arc-react-page/references/queries-and-commands.md +200 -0
  37. package/skills/cratis-chronicle-cli-operations/LICENSE +2 -0
  38. package/skills/cratis-chronicle-cli-operations/SKILL.md +199 -0
  39. package/skills/cratis-chronicle-client-dotnet/LICENSE +2 -0
  40. package/skills/cratis-chronicle-client-dotnet/SKILL.md +433 -0
  41. package/skills/cratis-chronicle-client-elixir/LICENSE +2 -0
  42. package/skills/cratis-chronicle-client-elixir/SKILL.md +429 -0
  43. package/skills/cratis-chronicle-client-kotlin/LICENSE +2 -0
  44. package/skills/cratis-chronicle-client-kotlin/SKILL.md +498 -0
  45. package/skills/cratis-chronicle-client-typescript/LICENSE +2 -0
  46. package/skills/cratis-chronicle-client-typescript/SKILL.md +396 -0
  47. package/skills/cratis-chronicle-compliance/LICENSE +2 -0
  48. package/skills/cratis-chronicle-compliance/SKILL.md +246 -0
  49. package/skills/cratis-chronicle-event-constraints/LICENSE +2 -0
  50. package/skills/cratis-chronicle-event-constraints/SKILL.md +227 -0
  51. package/skills/cratis-chronicle-event-modeling/LICENSE +2 -0
  52. package/skills/cratis-chronicle-event-modeling/SKILL.md +157 -0
  53. package/skills/cratis-chronicle-event-specifications/LICENSE +2 -0
  54. package/skills/cratis-chronicle-event-specifications/SKILL.md +200 -0
  55. package/skills/cratis-chronicle-event-type-migration/LICENSE +2 -0
  56. package/skills/cratis-chronicle-event-type-migration/SKILL.md +235 -0
  57. package/skills/cratis-chronicle-mcp-inspection/LICENSE +2 -0
  58. package/skills/cratis-chronicle-mcp-inspection/SKILL.md +79 -0
  59. package/skills/cratis-chronicle-mcp-inspection/references/blocked-tools.md +12 -0
  60. package/skills/cratis-chronicle-mcp-inspection/references/observational-tools.md +8 -0
  61. package/skills/cratis-chronicle-multi-tenancy/LICENSE +2 -0
  62. package/skills/cratis-chronicle-multi-tenancy/SKILL.md +198 -0
  63. package/skills/cratis-chronicle-projection/LICENSE +2 -0
  64. package/skills/cratis-chronicle-projection/SKILL.md +162 -0
  65. package/skills/cratis-chronicle-projection/references/fluent-builder.md +126 -0
  66. package/skills/cratis-chronicle-projection/references/model-bound-attributes.md +118 -0
  67. package/skills/cratis-chronicle-reactor/LICENSE +2 -0
  68. package/skills/cratis-chronicle-reactor/SKILL.md +344 -0
  69. package/skills/cratis-chronicle-read-model/LICENSE +2 -0
  70. package/skills/cratis-chronicle-read-model/SKILL.md +193 -0
  71. package/skills/cratis-chronicle-read-model/references/queries.md +142 -0
  72. package/skills/cratis-chronicle-read-model-specifications/LICENSE +2 -0
  73. package/skills/cratis-chronicle-read-model-specifications/SKILL.md +176 -0
  74. package/skills/cratis-chronicle-reducer/LICENSE +2 -0
  75. package/skills/cratis-chronicle-reducer/SKILL.md +173 -0
  76. package/skills/cratis-chronicle-web-workbench/LICENSE +2 -0
  77. package/skills/cratis-chronicle-web-workbench/SKILL.md +177 -0
  78. package/skills/cratis-cli-terminal-workbench/LICENSE +2 -0
  79. package/skills/cratis-cli-terminal-workbench/SKILL.md +156 -0
  80. package/skills/cratis-code-review/LICENSE +2 -0
  81. package/skills/cratis-code-review/SKILL.md +177 -0
  82. package/skills/cratis-components-accessibility/LICENSE +2 -0
  83. package/skills/cratis-components-accessibility/SKILL.md +176 -0
  84. package/skills/cratis-components-schema-editor/LICENSE +2 -0
  85. package/skills/cratis-components-schema-editor/SKILL.md +238 -0
  86. package/skills/cratis-components-stepper-command-dialog/LICENSE +2 -0
  87. package/skills/cratis-components-stepper-command-dialog/SKILL.md +209 -0
  88. package/skills/cratis-components-styling/LICENSE +2 -0
  89. package/skills/cratis-components-styling/SKILL.md +221 -0
  90. package/skills/cratis-components-toolbar/LICENSE +2 -0
  91. package/skills/cratis-components-toolbar/SKILL.md +259 -0
  92. package/skills/cratis-documentation-writing/LICENSE +2 -0
  93. package/skills/cratis-documentation-writing/SKILL.md +121 -0
  94. package/skills/cratis-event-model-diagram/LICENSE +2 -0
  95. package/skills/cratis-event-model-diagram/SKILL.md +142 -0
  96. package/skills/cratis-fundamentals-concept/LICENSE +2 -0
  97. package/skills/cratis-fundamentals-concept/SKILL.md +198 -0
  98. package/skills/cratis-fundamentals-type-discovery/LICENSE +2 -0
  99. package/skills/cratis-fundamentals-type-discovery/SKILL.md +152 -0
  100. package/skills/cratis-governed-release-methodology/LICENSE +2 -0
  101. package/skills/cratis-governed-release-methodology/SKILL.md +297 -0
  102. package/skills/cratis-lens-browser-extension/LICENSE +2 -0
  103. package/skills/cratis-lens-browser-extension/SKILL.md +239 -0
  104. package/skills/cratis-performance-review/LICENSE +2 -0
  105. package/skills/cratis-performance-review/SKILL.md +139 -0
  106. package/skills/cratis-screenplay-model-authoring/LICENSE +2 -0
  107. package/skills/cratis-screenplay-model-authoring/SKILL.md +231 -0
  108. package/skills/cratis-security-review/LICENSE +2 -0
  109. package/skills/cratis-security-review/SKILL.md +141 -0
  110. package/skills/cratis-specification-by-example/LICENSE +2 -0
  111. package/skills/cratis-specification-by-example/SKILL.md +188 -0
  112. package/skills/cratis-specifications-csharp/LICENSE +2 -0
  113. package/skills/cratis-specifications-csharp/SKILL.md +230 -0
  114. package/skills/cratis-specifications-csharp/references/application-scenarios.md +191 -0
  115. package/skills/cratis-specifications-csharp/references/csharp-patterns.md +142 -0
  116. package/skills/cratis-specifications-csharp/references/integration-specs.md +126 -0
  117. package/skills/cratis-specifications-typescript/LICENSE +2 -0
  118. package/skills/cratis-specifications-typescript/SKILL.md +202 -0
  119. package/skills/cratis-specifications-typescript/references/typescript-patterns.md +168 -0
  120. package/skills/cratis-stage-rendering-and-sandbox/LICENSE +2 -0
  121. package/skills/cratis-stage-rendering-and-sandbox/SKILL.md +230 -0
  122. package/skills/cratis-studio-mcp-safety-guidance/LICENSE +2 -0
  123. package/skills/cratis-studio-mcp-safety-guidance/SKILL.md +69 -0
  124. package/skills/cratis-studio-mcp-safety-guidance/references/blocked-tools.md +12 -0
  125. 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,2 @@
1
+ Copyright (c) Cratis. All rights reserved.
2
+ Licensed under the MIT license. See LICENSE file in the project root for full license information.
@@ -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,2 @@
1
+ Copyright (c) Cratis. All rights reserved.
2
+ Licensed under the MIT license. See LICENSE file in the project root for full license information.
@@ -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.
@@ -0,0 +1,2 @@
1
+ Copyright (c) Cratis. All rights reserved.
2
+ Licensed under the MIT license. See LICENSE file in the project root for full license information.