@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,344 @@
1
+ ---
2
+ name: cratis-chronicle-reactor
3
+ description: Implement a Chronicle IReactor - an automation that causes an external side effect, or a translation that appends follow-up events. Covers handler dispatch and parameter resolution, every supported return type, targeting another event source, replay handling with [OnceOnly] and [Replay], event filtering, failure and quarantine behavior, and the analyzer rules. Use for event-driven side effects; do not use to populate a read model.
4
+ license: MIT
5
+ ---
6
+
7
+ # Chronicle reactors
8
+
9
+ A reactor observes events and **does things**: calls an external system, or
10
+ appends follow-up events. A projection or reducer builds state; a reactor causes
11
+ effects. If the answer is "populate a queryable model", it is not a reactor.
12
+
13
+ | Need | Use |
14
+ | --- | --- |
15
+ | populate a queryable read model from events | a projection |
16
+ | a current-state-plus-event transition | a reducer |
17
+ | trigger a side effect outside the system | a reactor (**automation**) |
18
+ | append follow-up events elsewhere in the system | a reactor (**translation**) |
19
+ | both a model and an effect | one projection *and* one reactor |
20
+
21
+ ## Verified product sources
22
+
23
+ This skill is verified against these exact sources:
24
+
25
+ | Package | Version | Purpose |
26
+ | --- | --- | --- |
27
+ | `Cratis.Chronicle` | `16.45.2` | `Cratis.Chronicle.Reactors`, `Cratis.Chronicle.Reactors.SideEffects`, `Cratis.Chronicle.EventSequences`, `Cratis.Chronicle.Events` |
28
+ | `Cratis.Chronicle.CodeAnalysis` | `16.45.2` | `CHR0004`, `CHR0005`, `CHR0008`, `CHR0013`, `CHR0022`, `CHR0031`, `CHR0032` |
29
+
30
+ Reverify product sources before claiming support for another version.
31
+
32
+ > **There is no `ReactorSideEffect` type.** It does not exist in Chronicle, in
33
+ > any version. The real return types are listed below; the per-event metadata
34
+ > control it was imagined to provide is what `EventForEventSourceId` actually
35
+ > gives you. Never write it into code or an example.
36
+
37
+ ## Write the reactor
38
+
39
+ ```csharp
40
+ using Cratis.Chronicle.Events;
41
+ using Cratis.Chronicle.Reactors;
42
+
43
+ /// <summary>
44
+ /// <what this automation does and why>.
45
+ /// </summary>
46
+ public class <ReactorName>(<IServiceType> <serviceName>) : IReactor
47
+ {
48
+ /// <summary>Reacts to <see cref="<EventName>"/>.</summary>
49
+ public async Task <MethodName>(<EventName> @event, EventContext context) =>
50
+ await <serviceName>.<Operation>(@event.<Property>);
51
+ }
52
+ ```
53
+
54
+ `Cratis.Chronicle.Reactors.IReactor` is a **marker interface with no members**.
55
+ Reactors are discovered by that interface — the `[Reactor]` attribute is optional
56
+ and is not what makes a class a reactor. Generic types are not discovered.
57
+
58
+ ## Dispatch and parameters
59
+
60
+ **Dispatch is by the first parameter's type.** The method name is for the reader
61
+ and plays no part. When two methods claim the same event type, the winner is
62
+ chosen by public before non-public, then more parameters before fewer, then
63
+ ordinal name order — so do not rely on it; write one handler per event type.
64
+
65
+ A handler takes **at least one** parameter. There is no upper limit. Everything
66
+ after the first is resolved by type, position-independently:
67
+
68
+ | Parameter type | Resolved as |
69
+ | --- | --- |
70
+ | `EventContext` | the event context |
71
+ | `ReactorDelivery` | reactor id, event store, namespace, event sequence, partition, sequence number |
72
+ | a type that has a projection or a reducer | that **read model**, materialized for this event |
73
+ | anything else | from the service provider |
74
+
75
+ An unresolvable parameter throws at invocation, and analyzer **CHR0004**
76
+ (warning) flags it at build time: after the event, a reactor method may take the
77
+ event context, a read model, or a service — not a primitive, a value type, or a
78
+ string.
79
+
80
+ The first parameter's type must carry `[EventType]`, or analyzer **CHR0005**
81
+ (error) rejects it.
82
+
83
+ ### Injecting a read model
84
+
85
+ A read-model parameter is materialized using the event context's event-source id
86
+ by default. When the model's key differs from the triggering event's source —
87
+ the event carries a related entity's id — implement `ICanResolveReadModelKey` on
88
+ the reactor:
89
+
90
+ ```csharp
91
+ public class <ReactorName> : IReactor, ICanResolveReadModelKey
92
+ {
93
+ public ReadModelKey Resolve(object @event, EventContext context) =>
94
+ ((<EventName>)@event).<RelatedIdProperty>;
95
+
96
+ public Task <MethodName>(<EventName> @event, <ReadModelName> <readModel>) =>
97
+ Task.CompletedTask;
98
+ }
99
+ ```
100
+
101
+ The resolved key applies to every read-model parameter across all of the
102
+ reactor's handlers.
103
+
104
+ A **materialized** read model is read from its sink, so it is eventually
105
+ consistent — as current as its own observer. Mark the read model `[Passive]` when
106
+ the reactor needs it strongly consistent: a passive model has no sink and is
107
+ computed on demand at the point the reactor runs.
108
+
109
+ Prefer the event's own data. Analyzer **CHR0032** (warning) rejects injecting a
110
+ storage primitive such as `IMongoCollection<T>` directly — read keyed state
111
+ through a read-model parameter or `IReadModels.GetInstanceById` instead.
112
+
113
+ ## Return types
114
+
115
+ | Return | Effect |
116
+ | --- | --- |
117
+ | `void`, `Task` | no side-effect append |
118
+ | `TEvent`, `Task<TEvent>` | append one event, with reactor-level metadata |
119
+ | `IEnumerable<object>`, `Task<IEnumerable<object>>` | append several, in one transaction |
120
+ | `EventForEventSourceId`, `Task<...>` | append one event to an explicit event source |
121
+ | `IEnumerable<EventForEventSourceId>`, `Task<...>` | append to several event sources, in one transaction |
122
+ | `EventsWithConcurrencyScopes`, `Task<...>` | append with explicit concurrency scopes |
123
+
124
+ A collection may **mix** bare events and `EventForEventSourceId` items; each is
125
+ appended with its own metadata, all in one transaction.
126
+
127
+ A custom side-effect type handled by your own `IReactorSideEffectHandler` is
128
+ only recognised when returned as `Task<T>`.
129
+
130
+ ### Returning events instead of injecting `IEventLog`
131
+
132
+ Return the events. Do not inject `Cratis.Chronicle.EventSequences.IEventLog` into
133
+ a reactor. **This is a convention, not a framework contract** — no analyzer
134
+ enforces it — but returning the events keeps the reactor free of the event store
135
+ and keeps the append inside Chronicle's own transaction.
136
+
137
+ ```csharp
138
+ // One event, on the triggering event's own event source.
139
+ public Task<<ResultEvent>> <MethodName>(<EventName> @event, EventContext context) =>
140
+ Task.FromResult(new <ResultEvent>(@event.<Property>));
141
+
142
+ // Several events on that same event source.
143
+ public Task<IEnumerable<object>> <MethodName>(<EventName> @event, EventContext context) =>
144
+ Task.FromResult<IEnumerable<object>>([new <ResultEvent>(), new <OtherEvent>()]);
145
+ ```
146
+
147
+ ### Targeting another event source
148
+
149
+ `Cratis.Chronicle.EventSequences.EventForEventSourceId` is the cross-stream
150
+ wrapper:
151
+
152
+ ```csharp
153
+ public record EventForEventSourceId(EventSourceId EventSourceId, object Event, Causation? Causation = default)
154
+ {
155
+ public Subject? Subject { get; init; }
156
+ public EventStreamType EventStreamType { get; init; }
157
+ public EventStreamId EventStreamId { get; init; }
158
+ public EventSourceType EventSourceType { get; init; }
159
+ public DateTimeOffset? Occurred { get; init; }
160
+ public IEnumerable<string> Tags { get; init; }
161
+ }
162
+ ```
163
+
164
+ ```csharp
165
+ public Task<IEnumerable<EventForEventSourceId>> <MethodName>(<EventName> @event, EventContext context) =>
166
+ Task.FromResult<IEnumerable<EventForEventSourceId>>(
167
+ [
168
+ new(@event.<RelatedId>, new <ResultEvent>(@event.<Property>)),
169
+ new(@event.<OtherId>, new <OtherEvent>()) { EventStreamType = new("<stream-type>") },
170
+ ]);
171
+ ```
172
+
173
+ It does **not** carry an event sequence id; side effects are appended to the
174
+ event log.
175
+
176
+ ### Where the metadata for a bare event comes from
177
+
178
+ For a **bare** event return, Chronicle resolves the append metadata from the
179
+ reactor itself: the `[EventSourceType]`, `[EventStreamType]`, and
180
+ `[EventStreamId]` attributes, and the `ICanProvideEventSourceId`,
181
+ `ICanProvideEventStreamId`, and `ICanProvideSubject` interfaces (all in
182
+ `Cratis.Chronicle.Events`, each with one method).
183
+
184
+ An `EventForEventSourceId` is **self-describing** — reactor-level metadata is not
185
+ applied to it. In a mixed collection, bare items get the reactor metadata and
186
+ wrapper items keep their own.
187
+
188
+ ## Replay
189
+
190
+ A reactor sees the same event twice for different reasons: when it happens, and
191
+ again when its observer is replayed.
192
+
193
+ `Cratis.Chronicle.Reactors.OnceOnlyAttribute` (`[OnceOnly]`) targets a **class or
194
+ a method** and means **replay exclusion**:
195
+
196
+ - on the class, the observer is marked not replayable and the replay never
197
+ starts;
198
+ - on a method, that handler is skipped while the delivery is a replay.
199
+
200
+ **`[OnceOnly]` is not per-event-source deduplication.** There is no ledger of
201
+ which events a handler has already seen. Recovering a failed partition
202
+ re-delivers the event as an ordinary observation, and a `[OnceOnly]` handler runs
203
+ again. Design the side effect to be idempotent regardless — an external
204
+ idempotency key, an upsert, a conditional write.
205
+
206
+ Analyzer **CHR0022** (warning) requires `[OnceOnly]` on a handler that returns
207
+ event side effects, because a replay would otherwise append duplicates.
208
+
209
+ `Cratis.Chronicle.Reactors.ReplayAttribute` (`[Replay]`) is the other half, and
210
+ targets a method only. Mark a **second** handler for the same event type with it
211
+ when replay needs to do something *different* rather than nothing:
212
+
213
+ ```csharp
214
+ public Task <MethodName>(<EventName> @event) => <serviceName>.<Operation>(@event.<Property>);
215
+
216
+ [Replay]
217
+ public Task <MethodName>DuringReplay(<EventName> @event) => Task.CompletedTask;
218
+ ```
219
+
220
+ With a `[Replay]` handler present, only it runs during replay. Without one, the
221
+ ordinary handler runs during replay as before. An event type handled *only* by a
222
+ `[Replay]` handler is still subscribed to.
223
+
224
+ ## Identity and event sequence
225
+
226
+ `Cratis.Chronicle.Reactors.ReactorAttribute` takes two optional arguments:
227
+
228
+ ```csharp
229
+ public sealed class ReactorAttribute(string id = "", string? eventSequence = default) : Attribute
230
+ ```
231
+
232
+ **There is no `isActive` argument** — that belongs to `[Reducer]`. Without an
233
+ explicit `id`, the reactor's identity is its full type name.
234
+
235
+ The source sequence resolves in this order: an `[EventStore]` attribute (which
236
+ selects the inbox for that store), then `[EventSequence]`/`[EventLog]`, then
237
+ `Reactor(eventSequence: ...)`, then the event store inferred from the handled
238
+ event types, then the event log.
239
+
240
+ **`[EventStore]` cannot be combined** with `[EventSequence]`, `[EventLog]`, or
241
+ `Reactor(eventSequence: ...)` — analyzer **CHR0013** (error), and a runtime
242
+ exception. All of a reactor's event types must come from one event store —
243
+ analyzer **CHR0008** (error).
244
+
245
+ ### Cross-service events
246
+
247
+ Cross-service facts route through the outbox and inbox rather than a shared log.
248
+ The producer appends its public contract event to the outbox sequence; the
249
+ consuming observer listens on the implicit inbox sequence for the source store.
250
+ Chronicle creates or reuses that subscription from the observer's `[EventStore]`
251
+ metadata, or from `[EventStore]` on the event type or its assembly. Put
252
+ `[assembly: EventStore("<source-store>")]` in a contracts project when every
253
+ event there originates from one service.
254
+
255
+ ## Filtering which events reach the reactor
256
+
257
+ | Attribute | Effect |
258
+ | --- | --- |
259
+ | `[FilterEventsByTag("<tag>")]` | **filters** by appended tag; repeatable, and any one match admits the event |
260
+ | `[EventSourceType("<value>")]` | **filters** by the appended event source type |
261
+ | `[EventStreamType("<value>")]` | **filters** by the appended event stream type |
262
+ | `[EventStreamId("<value>")]` | **does not filter**; it is append metadata for side effects |
263
+ | `[Tag]`, `[Tags]` | **label** the reactor; they never filter |
264
+
265
+ Filter categories combine with AND; tags within the tag category combine with OR.
266
+ These correspond to metadata supplied at append time:
267
+
268
+ ```csharp
269
+ await eventStore.EventLog.Append(
270
+ <eventSourceId>,
271
+ new <EventName>(<arguments>),
272
+ eventStreamType: "<stream-type>",
273
+ eventSourceType: "<source-type>",
274
+ tags: ["<tag>"]);
275
+ ```
276
+
277
+ A **projection cannot be filtered at all** — if you need metadata-based
278
+ selection, a reactor or a reducer is where it happens.
279
+
280
+ ## Translation via a command
281
+
282
+ A translation that adapts one area's events into another's intent runs a command
283
+ rather than appending directly. `ICommandPipeline` is an **Arc** type
284
+ (`Cratis.Arc.Commands`), not a Chronicle one:
285
+
286
+ ```csharp
287
+ using Cratis.Arc.Commands;
288
+
289
+ public class <ReactorName>(ICommandPipeline commandPipeline) : IReactor
290
+ {
291
+ public async Task <MethodName>(<EventName> @event, EventContext context) =>
292
+ await commandPipeline.Execute(new <CommandName>(@event.<Property>));
293
+ }
294
+ ```
295
+
296
+ ## Failure behavior
297
+
298
+ If a handler throws, or a returned side-effect event fails to append — a
299
+ constraint violation, a concurrency violation, or an error — the failing
300
+ event-source partition **pauses** until the cause is resolved. Repeated failures
301
+ can **quarantine** the observer, which stops retries and suppresses automatic
302
+ recovery. **A quarantined observer does not resume on reconnect**; an operator
303
+ must clear the quarantine explicitly.
304
+
305
+ Do not throw to reject a malformed inbound event. A reactor is not a data-quality
306
+ gate; invalid payloads belong at the command or append site. When a malformed
307
+ cross-service fact arrives, append a clear failure or dead-letter event, or
308
+ surface it through the operational failure path, and skip the partial side
309
+ effect. Throwing just to reject it pauses the partition and can quarantine the
310
+ whole observer.
311
+
312
+ ## Keep the reactor stateless
313
+
314
+ Inject collaborators through the primary constructor; hold no mutable state on
315
+ the class. Analyzer **CHR0031** (warning) flags mutable reactor state. Keep each
316
+ reactor focused on one automation concern; several handlers in one class are fine
317
+ when they serve that concern.
318
+
319
+ ## Specifications
320
+
321
+ Drive events through the reactor scenario helper with a service provider of
322
+ substitutes, and assert on those substitutes for non-event side effects. For
323
+ handlers that return events, assert the resulting appends through the scenario's
324
+ event store. Cover the replay path separately when the reactor has `[OnceOnly]`
325
+ or `[Replay]` handlers.
326
+
327
+ ## Verify
328
+
329
+ - The class implements `IReactor` and is not generic.
330
+ - Every handler's first parameter is an `[EventType]` record, and there is one
331
+ handler per event type.
332
+ - Every additional parameter resolves to the event context, `ReactorDelivery`, a
333
+ read model, or a service.
334
+ - No handler returns or references `ReactorSideEffect`.
335
+ - No `IEventLog` is injected; side-effect events are returned instead.
336
+ - Handlers returning events carry `[OnceOnly]`.
337
+ - Side effects are idempotent even with `[OnceOnly]`, because recovery
338
+ re-delivers.
339
+ - `[EventStore]` is not combined with an explicit event sequence.
340
+ - No filter attribute is placed where it is inert.
341
+ - The reactor holds no mutable state and injects no storage primitive.
342
+ - The build is clean with no `CHR0004`, `CHR0005`, `CHR0008`, `CHR0013`,
343
+ `CHR0022`, `CHR0031`, or `CHR0032` outstanding, and the reactor
344
+ 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,193 @@
1
+ ---
2
+ name: cratis-chronicle-read-model
3
+ description: Create a Chronicle read model and its query surface - past-tense event types, the read-model record and its key, choosing a projection or a reducer to build it, snapshot versus observable queries, and reading an instance directly through IReadModels. Use when creating a read model from scratch; for changing how an existing model is populated use the projection or reducer skill instead.
4
+ license: MIT
5
+ ---
6
+
7
+ # Chronicle read models
8
+
9
+ A read model is derived state built from events. The path is:
10
+
11
+ ```text
12
+ event types -> read-model record -> a projection or a reducer -> a query surface
13
+ ```
14
+
15
+ Chronicle owns the events, the projection or reducer, and the stored instance.
16
+ The **query surface is Arc's**, and the two halves are separable: a read model
17
+ that only exists to be read from inside the backend needs no query surface at
18
+ all.
19
+
20
+ ## Verified product sources
21
+
22
+ This skill is verified against these exact sources:
23
+
24
+ | Package | Version | Purpose |
25
+ | --- | --- | --- |
26
+ | `Cratis.Chronicle` | `16.45.2` | `Cratis.Chronicle.Events`, `Cratis.Chronicle.Keys`, `Cratis.Chronicle.ReadModels` |
27
+ | `Cratis.Arc.Core` | current | `Cratis.Arc.Queries.ModelBound.ReadModelAttribute` and `PathAttribute` |
28
+ | `Cratis.Arc.MongoDB` | current | the `Observe` family of `IMongoCollection<T>` extensions |
29
+
30
+ Reverify product sources before claiming support for another version. Confirm
31
+ the exact Arc package version in the consuming project before citing one.
32
+
33
+ ## Step 1 — Define the events
34
+
35
+ Events are the source of truth. Each is a `record` carrying
36
+ `Cratis.Chronicle.Events.EventTypeAttribute`, named as a past-tense fact.
37
+
38
+ ```csharp
39
+ using Cratis.Chronicle.Events;
40
+
41
+ /// <summary>Emitted when <description>.</summary>
42
+ [EventType]
43
+ public record <EntityName><PastTenseVerb>(<ConceptType> <PropertyName>);
44
+ ```
45
+
46
+ - One clear purpose per event; do not mix concerns into one fact.
47
+ - **Avoid nullable properties.** Analyzer `CHR0012` warns on them. Model an
48
+ optional fact as a separate event.
49
+ - Prefer strongly typed concept properties over raw primitives.
50
+ - An event never carries its own event-source id as a payload property — the id
51
+ is in the event context.
52
+ - `[EventType]` takes `(string id = "", uint generation = 1)`. Pass neither for a
53
+ new event: the id defaults to the CLR type name and the generation to `1`.
54
+
55
+ ## Step 2 — Define the read-model record
56
+
57
+ ```csharp
58
+ using Cratis.Chronicle.Keys;
59
+
60
+ public record <ReadModelName>(
61
+ [property: Key] <IdType> Id,
62
+ <PropertyType> <PropertyName>);
63
+ ```
64
+
65
+ - `Cratis.Chronicle.Keys.KeyAttribute` takes no arguments and targets a property
66
+ or a record constructor parameter, never the class.
67
+ - When the model's identity **is** the event-source id, that is already the key
68
+ and `[Key]` adds nothing. Placing `[Key]` on an `EventSourceId<T>`-derived
69
+ value is flagged by analyzer `CHR0026`.
70
+ - One read model per use case. Specialization beats a shared model reused across
71
+ conflicting scenarios.
72
+ - Do not give read-model properties default values that could mask a projection
73
+ that was never wired.
74
+ - Never name a property `_subject`, `__subject`, or `__subjects` — Chronicle
75
+ reserves those for compliance-subject tracking, and analyzer `CHR0035`
76
+ rejects them.
77
+ - `Cratis.Chronicle.ReadModels.IndexAttribute` (`[Index]`) marks a property for
78
+ indexing in the sink.
79
+
80
+ ## Step 3 — Choose a projection or a reducer
81
+
82
+ | | Projection | Reducer |
83
+ | --- | --- | --- |
84
+ | Shape | the model is mostly a mapping of event fields | the next state is a function of the current state |
85
+ | Expressed as | model-bound attributes, or the fluent builder | C# control flow in a handler method |
86
+ | Reach for it | first, and second | only after both projection forms fail |
87
+
88
+ A reducer is a legitimate style when the transition genuinely depends on prior
89
+ state — it is not a failure mode. It *is* a failure mode when it re-implements
90
+ something an attribute already expresses. Work down the ladder: model-bound
91
+ attributes, then the fluent builder, then a reducer.
92
+
93
+ Both are discovered by convention. Nothing is registered.
94
+
95
+ Use the projection skill for the attribute and builder surface, and the reducer
96
+ skill for handler signatures and the nullable-current rule.
97
+
98
+ ## Step 4 — Add a query surface, if the model is read from outside
99
+
100
+ Queries are **static** methods on the read-model record, discovered by Arc.
101
+ Marking the record with `[ReadModel]` from `Cratis.Arc.Queries.ModelBound` is
102
+ what gives it that surface.
103
+
104
+ ```csharp
105
+ using System.Reactive.Subjects;
106
+ using Cratis.Arc.Queries.ModelBound;
107
+ using Cratis.Chronicle.Keys;
108
+ using MongoDB.Driver;
109
+
110
+ [ReadModel]
111
+ public record <ReadModelName>([property: Key] <IdType> Id, <PropertyType> <PropertyName>)
112
+ {
113
+ public static async Task<IEnumerable<<ReadModelName>>> All<PluralName>(
114
+ IMongoCollection<<ReadModelName>> collection) =>
115
+ await collection.Find(Builders<<ReadModelName>>.Filter.Empty).ToListAsync();
116
+
117
+ public static ISubject<IEnumerable<<ReadModelName>>> Observe<PluralName>(
118
+ IMongoCollection<<ReadModelName>> collection) =>
119
+ collection.Observe();
120
+ }
121
+ ```
122
+
123
+ The rules Arc's discovery actually enforces:
124
+
125
+ - The method must be **static**. Instance methods are never discovered.
126
+ - The declared return type, after unwrapping one `Task<>`, must be the read
127
+ model, an array of it, something assignable to `IEnumerable<T>` of it,
128
+ `IAsyncEnumerable<T>` of it, `ISubject<T>` of it, or `ISubject<>` of a
129
+ collection of it.
130
+ - **The declared type must be `ISubject<...>`, never `Subject<...>` or
131
+ `BehaviorSubject<...>`.** The check matches the open generic definition
132
+ exactly, so a concrete subject type fails discovery silently. Construct a
133
+ concrete subject inside if you like; declare `ISubject<...>`.
134
+ - Open generic methods are rejected.
135
+ - Use `[Path("<route>")]` from `Cratis.Arc.Queries.ModelBound` for a custom
136
+ route. It targets the class or the method. It is Arc's attribute — do not
137
+ reach for ASP.NET's `[Route]`.
138
+
139
+ Read [references/queries.md](references/queries.md) for the `Observe` family and
140
+ the difference between snapshot and observable queries.
141
+
142
+ ## Step 5 — Or read the instance directly
143
+
144
+ Inside the backend, read an instance through
145
+ `Cratis.Chronicle.ReadModels.IReadModels`, reached from `IEventStore.ReadModels`:
146
+
147
+ ```csharp
148
+ Task<TReadModel> GetInstanceById<TReadModel>(ReadModelKey key, ReadModelSessionId? sessionId = null);
149
+ Task<object> GetInstanceById(Type readModelType, ReadModelKey key, ReadModelSessionId? sessionId = null);
150
+ ```
151
+
152
+ `ReadModelKey` converts implicitly from `string`, `Guid`, and `EventSourceId`,
153
+ so an id can be passed directly. **The declared return type is not nullable even
154
+ though no instance may exist** — check the result before dereferencing it.
155
+
156
+ Neighbouring members include `GetInstances<T>`, `GetSnapshotsById<T>`,
157
+ `Watch<T>`, `Release<T>`, and `Register`. **There is no predicate or `IQueryable`
158
+ surface on `IReadModels`** — filtered reads go through the sink, which is what
159
+ the Arc query methods above do.
160
+
161
+ ### `[Passive]` when the read is a command-side decision
162
+
163
+ `Cratis.Chronicle.ReadModels.PassiveAttribute` on the read-model type makes it
164
+ passive: no sink, nothing materialized, and the instance is computed on demand
165
+ from the events at the moment it is read. Reach for it when a decision needs the
166
+ model **strongly consistent** rather than eventually consistent.
167
+
168
+ `[Passive]` on its own does **not** make a read model a projection. It is
169
+ deliberately excluded from the annotations that do; it changes how an existing
170
+ projection is registered.
171
+
172
+ ## Step 6 — Build and verify
173
+
174
+ Build, fix every error, then prove the behavior: drive the contributing events
175
+ through the read-model specification for this model and assert the projected
176
+ state. Seed each contributing stream with its own event source when the model
177
+ spans streams.
178
+
179
+ ## Verify
180
+
181
+ - Every event is a past-tense fact with one purpose and no nullable properties.
182
+ - No event carries its own event-source id.
183
+ - The read model has exactly one key, and no `[Key]` sits on an
184
+ `EventSourceId<T>` value.
185
+ - No reserved compliance-subject property name is used.
186
+ - Read-model properties carry no defaults that could hide unwired projection.
187
+ - Query methods are `static` and declare `ISubject<...>` rather than a concrete
188
+ subject type.
189
+ - A direct `GetInstanceById` result is checked for absence before use.
190
+ - `[Passive]` is present only where a strongly consistent command-side read is
191
+ genuinely required.
192
+ - The project builds clean and the read-model specifications pass against the
193
+ verified package versions.
@@ -0,0 +1,142 @@
1
+ # Read-model queries
2
+
3
+ Verified against `Cratis.Arc.Core` and `Cratis.Arc.MongoDB` as checked out
4
+ alongside `Cratis.Chronicle` `16.45.2`. Confirm the exact Arc package version in
5
+ the consuming project before citing one.
6
+
7
+ ## Where the pieces live
8
+
9
+ | Type | Namespace | Package |
10
+ | --- | --- | --- |
11
+ | `ReadModelAttribute` (`[ReadModel]`) | `Cratis.Arc.Queries.ModelBound` | `Cratis.Arc.Core` |
12
+ | `PathAttribute` (`[Path]`) | `Cratis.Arc.Queries.ModelBound` | `Cratis.Arc.Core` |
13
+ | the `Observe` extensions | `MongoDB.Driver` | `Cratis.Arc.MongoDB` |
14
+
15
+ `[ReadModel]` is a bare marker with **no constructor parameters**. `[Path]` takes
16
+ one `string path` and targets a class or a method.
17
+
18
+ The `Observe` extensions are deliberately declared in the `MongoDB.Driver`
19
+ namespace so they resolve from an existing `using MongoDB.Driver;`. They are not
20
+ Chronicle extensions — Chronicle declares no `Observe` on `IMongoCollection<T>`.
21
+
22
+ ## Discovery rules
23
+
24
+ Arc scans the read-model type for **static** methods, public and non-public
25
+ alike. Instance methods are never discovered.
26
+
27
+ The declared return type is validated after unwrapping at most one `Task<>`:
28
+
29
+ | Accepted |
30
+ | --- |
31
+ | the read model type |
32
+ | an array of it |
33
+ | anything assignable to `IEnumerable<T>` of it |
34
+ | `IAsyncEnumerable<T>` of it |
35
+ | `ISubject<T>` of it |
36
+ | `ISubject<T>` of a collection of it |
37
+
38
+ Rejected: open generic methods, and **any concrete subject type**. The subject
39
+ check matches `typeof(ISubject<>)` as an open generic definition, so a method
40
+ declared as returning `Subject<T>` or `BehaviorSubject<T>` fails discovery
41
+ without an error at the call site. Declare `ISubject<...>`; construct whatever
42
+ you like inside.
43
+
44
+ ## Snapshot queries
45
+
46
+ A snapshot query answers once with the current state.
47
+
48
+ ```csharp
49
+ public static async Task<IEnumerable<<ReadModelName>>> All<PluralName>(
50
+ IMongoCollection<<ReadModelName>> collection) =>
51
+ await collection.Find(Builders<<ReadModelName>>.Filter.Empty).ToListAsync();
52
+
53
+ public static async Task<<ReadModelName>?> <SingularName>ById(
54
+ <IdType> id,
55
+ IMongoCollection<<ReadModelName>> collection) =>
56
+ await collection.Find(model => model.Id == id).FirstOrDefaultAsync();
57
+ ```
58
+
59
+ Name the method for what it returns — the name is the query's identity on the
60
+ generated client surface.
61
+
62
+ ## Observable queries
63
+
64
+ An observable query pushes updates as projected state lands.
65
+
66
+ ```csharp
67
+ public static ISubject<IEnumerable<<ReadModelName>>> Observe<PluralName>(
68
+ IMongoCollection<<ReadModelName>> collection) =>
69
+ collection.Observe();
70
+
71
+ public static ISubject<<ReadModelName>> Observe<SingularName>ById(
72
+ <IdType> id,
73
+ IMongoCollection<<ReadModelName>> collection) =>
74
+ collection.ObserveById<<ReadModelName>, <IdType>>(id);
75
+ ```
76
+
77
+ Return `ISubject<...>` directly. Do **not** return `Task<ISubject<...>>`.
78
+
79
+ ## The `Observe` family — exact signatures
80
+
81
+ ```csharp
82
+ // collection-valued
83
+ ISubject<IEnumerable<TDocument>> Observe<TDocument>(
84
+ this IMongoCollection<TDocument> collection,
85
+ Expression<Func<TDocument, bool>>? filter,
86
+ FindOptions? options = null);
87
+
88
+ ISubject<IEnumerable<TDocument>> Observe<TDocument>(
89
+ this IMongoCollection<TDocument> collection,
90
+ FilterDefinition<TDocument>? filter = null,
91
+ FindOptions? options = null);
92
+
93
+ // single-valued
94
+ ISubject<TDocument> ObserveSingle<TDocument>(
95
+ this IMongoCollection<TDocument> collection,
96
+ Expression<Func<TDocument, bool>>? filter,
97
+ FindOptions? options = null);
98
+
99
+ ISubject<TDocument> ObserveSingle<TDocument>(
100
+ this IMongoCollection<TDocument> collection,
101
+ FilterDefinition<TDocument>? filter = null,
102
+ FindOptions? options = null);
103
+
104
+ ISubject<TDocument> ObserveById<TDocument, TId>(
105
+ this IMongoCollection<TDocument> collection,
106
+ TId id);
107
+ ```
108
+
109
+ Two details that catch people out:
110
+
111
+ - **`Observe` always returns a collection**, even with an equality filter. For a
112
+ single document use `ObserveSingle` or `ObserveById`; a filtered `Observe`
113
+ declared as `ISubject<TDocument>` does not compile.
114
+ - On the expression overload the `filter` parameter has **no default**. A bare
115
+ `collection.Observe()` therefore binds the `FilterDefinition<TDocument>?`
116
+ overload, which is the intended "everything" call.
117
+
118
+ Also available in the same place: `FindById<T, TId>` and
119
+ `FindByIdAsync<T, TId>`.
120
+
121
+ ## Custom routes
122
+
123
+ ```csharp
124
+ [ReadModel]
125
+ [Path("<resource-path>")]
126
+ public record <ReadModelName>(...)
127
+ {
128
+ [Path("<sub-path>")]
129
+ public static Task<IEnumerable<<ReadModelName>>> <QueryName>(...) => ...;
130
+ }
131
+ ```
132
+
133
+ `[Path]` is Arc's attribute and works at class or method level. ASP.NET's
134
+ `[Route]` belongs to controller-based endpoints and is not the mechanism here.
135
+
136
+ ## Reading without a query surface
137
+
138
+ When only backend code reads the model, skip `[ReadModel]` entirely and use
139
+ `IEventStore.ReadModels.GetInstanceById<T>(key)`. Its declared return type is
140
+ non-nullable even though no instance may exist, so test the result. There is no
141
+ predicate or `IQueryable` surface on `IReadModels` — filtered reads belong on the
142
+ sink, which is what the query methods above use.
@@ -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.