@cratis/pi 7.37.0 → 7.38.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 (77) hide show
  1. package/package/corpus/harnesses/copilot/instructions/ai-distribution.instructions.md +109 -0
  2. package/package/corpus/harnesses/copilot/instructions/application-profile.instructions.md +81 -0
  3. package/package/corpus/harnesses/copilot/instructions/capability-is-not-authority.instructions.md +45 -0
  4. package/package/corpus/harnesses/copilot/instructions/code-quality.csharp.instructions.md +92 -0
  5. package/package/corpus/harnesses/copilot/instructions/code-quality.instructions.md +83 -0
  6. package/package/corpus/harnesses/copilot/instructions/code-quality.typescript.instructions.md +90 -0
  7. package/package/corpus/harnesses/copilot/instructions/components.instructions.md +300 -0
  8. package/package/corpus/harnesses/copilot/instructions/concepts.instructions.md +138 -0
  9. package/package/corpus/harnesses/copilot/instructions/cratis-folder-is-configuration.instructions.md +29 -0
  10. package/package/corpus/harnesses/copilot/instructions/csharp.instructions.md +357 -0
  11. package/package/corpus/harnesses/copilot/instructions/dialogs.instructions.md +278 -0
  12. package/package/corpus/harnesses/copilot/instructions/documentation-structure-and-formatting.instructions.md +162 -0
  13. package/package/corpus/harnesses/copilot/instructions/documentation.instructions.md +90 -0
  14. package/package/corpus/harnesses/copilot/instructions/editing-cratis-docs.instructions.md +74 -0
  15. package/package/corpus/harnesses/copilot/instructions/efcore.instructions.md +84 -0
  16. package/package/corpus/harnesses/copilot/instructions/efcore.specs.instructions.md +45 -0
  17. package/package/corpus/harnesses/copilot/instructions/elixir.instructions.md +50 -0
  18. package/package/corpus/harnesses/copilot/instructions/exit-codes-and-wrappers.instructions.md +34 -0
  19. package/package/corpus/harnesses/copilot/instructions/framework.instructions.md +55 -0
  20. package/package/corpus/harnesses/copilot/instructions/frontend-quality.instructions.md +60 -0
  21. package/package/corpus/harnesses/copilot/instructions/frontend-testing.instructions.md +127 -0
  22. package/package/corpus/harnesses/copilot/instructions/git-commits.instructions.md +139 -0
  23. package/package/corpus/harnesses/copilot/instructions/github-actions.instructions.md +87 -0
  24. package/package/corpus/harnesses/copilot/instructions/glossary.instructions.md +63 -0
  25. package/package/corpus/harnesses/copilot/instructions/go-engineering.instructions.md +61 -0
  26. package/package/corpus/harnesses/copilot/instructions/go.instructions.md +116 -0
  27. package/package/corpus/harnesses/copilot/instructions/guards-and-fuses.instructions.md +61 -0
  28. package/package/corpus/harnesses/copilot/instructions/java.instructions.md +138 -0
  29. package/package/corpus/harnesses/copilot/instructions/kotlin.instructions.md +175 -0
  30. package/package/corpus/harnesses/copilot/instructions/local-work-artifacts.instructions.md +56 -0
  31. package/package/corpus/harnesses/copilot/instructions/new-repository-intake.instructions.md +35 -0
  32. package/package/corpus/harnesses/copilot/instructions/orleans.instructions.md +97 -0
  33. package/package/corpus/harnesses/copilot/instructions/profiles.instructions.md +403 -0
  34. package/package/corpus/harnesses/copilot/instructions/pull-requests.instructions.md +176 -0
  35. package/package/corpus/harnesses/copilot/instructions/react.instructions.md +203 -0
  36. package/package/corpus/harnesses/copilot/instructions/reactors.instructions.md +336 -0
  37. package/package/corpus/harnesses/copilot/instructions/rtk.instructions.md +48 -0
  38. package/package/corpus/harnesses/copilot/instructions/specs.csharp.instructions.md +147 -0
  39. package/package/corpus/harnesses/copilot/instructions/specs.instructions.md +133 -0
  40. package/package/corpus/harnesses/copilot/instructions/specs.scenarios.csharp.instructions.md +281 -0
  41. package/package/corpus/harnesses/copilot/instructions/specs.typescript.instructions.md +141 -0
  42. package/package/corpus/harnesses/copilot/instructions/storybook.instructions.md +85 -0
  43. package/package/corpus/harnesses/copilot/instructions/typescript.instructions.md +151 -0
  44. package/package/corpus/harnesses/copilot/instructions/verification-discipline.instructions.md +27 -0
  45. package/package/corpus/harnesses/copilot/instructions/vertical-slices.instructions.md +414 -0
  46. package/package/corpus/harnesses/copilot/instructions/web-fetching.instructions.md +12 -0
  47. package/package/corpus/harnesses/copilot/instructions/writing-correct-examples.instructions.md +57 -0
  48. package/package/corpus/harnesses/copilot/instructions/writing-cratis-docs.instructions.md +72 -0
  49. package/package/corpus/product-sync.json +2 -2
  50. package/package/corpus/skills/cratis-screenplay-discovery/SKILL.md +11 -6
  51. package/package/corpus/skills/cratis-screenplay-discovery/references/facilitation.md +9 -5
  52. package/package/corpus/skills/cratis-screenplay-discovery/references/human-workshop.md +1 -1
  53. package/package/corpus/skills/cratis-screenplay-discovery/references/plotting-and-handoff.md +28 -4
  54. package/package/corpus/skills/cratis-screenplay-discovery/references/storming-loop.md +2 -1
  55. package/package/corpus/skills/cratis-screenplay-discovery/verification.json +5 -0
  56. package/package/corpus/skills/cratis-screenplay-model-authoring/SKILL.md +23 -29
  57. package/package/corpus/skills/cratis-screenplay-model-authoring/references/mcp-loop.md +15 -1
  58. package/package/corpus/skills/cratis-screenplay-model-authoring/references/model-notes.md +208 -0
  59. package/package/corpus/skills/cratis-screenplay-model-authoring/verification.json +20 -1
  60. package/package/corpus/skills/cratis-screenplay-model-review/SKILL.md +19 -36
  61. package/package/corpus/skills/cratis-screenplay-model-review/references/report-template.md +26 -0
  62. package/package/corpus/skills/cratis-screenplay-model-review/verification.json +7 -0
  63. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/SKILL.md +14 -16
  64. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/references/core-rules-digest.md +8 -3
  65. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/references/handoff-template.md +5 -2
  66. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/references/intake.md +21 -0
  67. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/references/reasoning-notes.md +10 -6
  68. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/references/stop-or-assume.md +32 -1
  69. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/references/worked-example.md +5 -2
  70. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/verification.json +5 -0
  71. package/package/corpus/skills/cratis-screenplay-toolchain/SKILL.md +4 -0
  72. package/package/corpus/skills/cratis-screenplay-toolchain/references/screenplay-contract.json +1716 -183
  73. package/package/corpus/skills/cratis-screenplay-toolchain/references/screenplay-contract.version +1 -1
  74. package/package/corpus/skills/cratis-screenplay-toolchain/references/versions.md +40 -4
  75. package/package/corpus/skills/cratis-screenplay-toolchain/verification.json +6 -0
  76. package/package/corpus/skills/cratis-stage-rendering-and-sandbox/references/admission.md +1 -1
  77. package/package.json +1 -1
@@ -0,0 +1,336 @@
1
+ ---
2
+ applyTo: "**/*.cs"
3
+ paths:
4
+ - "**/*.cs"
5
+ profile: application
6
+ ---
7
+ <!-- cratis-ai: generated Copilot instructions. Do not edit; rules/ is the source. -->
8
+
9
+ # Reactor Instructions
10
+
11
+ Reactors are the "if this then that" of event sourcing — they observe events and produce side effects. Unlike projections (which build state), reactors *do things*: send emails, trigger commands in other slices, call external APIs.
12
+
13
+ **Model first:** when an accepted model under the model root covers this scope (or the repository is opted in: the root holds a committed `.play` file (`git ls-tree -r --name-only HEAD` lists a `.play` file there, narrowed to `-- <root>` when a root is configured) or the project explicitly set `mcpServers.screenplay.root` in `.cratis/ai.json`; master definition in `cratis-screenplay-modeling-lifecycle`), a reactor implements an automation that the model owns (`produces` / `invokes`); read the modeled slice first and keep the reactor to what the model leaves to code (gap-fill). Otherwise (not opted in) stay code-first: this file and the **cratis-chronicle-reactor** skill are the guide, and the entry-point session may propose a model at most once per session. A `.play` file outside the root, an untracked or uncommitted draft or an empty directory is not consent.
14
+
15
+ ## IReactor — Marker Interface
16
+
17
+ `IReactor` is a **marker interface** with no methods to implement. Method dispatch is entirely by convention: the first parameter type of each public method determines which event it handles.
18
+
19
+ ```csharp
20
+ public class InvoiceIssuedNotifier(INotificationService notifications) : IReactor
21
+ {
22
+ /// <summary>
23
+ /// Reacts to <see cref="InvoiceIssued"/> events by sending a notification.
24
+ /// </summary>
25
+ /// <param name="event">The event.</param>
26
+ /// <param name="context">The event context.</param>
27
+ public async Task InvoiceIssued(InvoiceIssued @event, EventContext context) =>
28
+ await notifications.Notify($"Invoice '{@event.Number}' was issued.");
29
+ }
30
+ ```
31
+
32
+ ## Method Signature
33
+
34
+ ```csharp
35
+ public Task MethodName(TEvent @event, EventContext context)
36
+ ```
37
+
38
+ - **First parameter** — the event type. This determines which events the method subscribes to.
39
+ - **Second parameter onward** — dependencies, resolved when the method is invoked: `EventContext`, a read model, or a service. All are optional and there is no limit on how many; see [Taking Dependencies](#taking-dependencies). The analyzer constrains their *kind*, not their count — a primitive, value type or `string` after the first parameter is flagged because it cannot be resolved.
40
+ - **Return type** — `Task` or `void`, or a side-effect type (`TEvent`, `EventForEventSourceId`, or an `IEnumerable<>` of either — or of `object` to mix them) returned directly (sync) or wrapped in `Task<...>` (async). Prefer `Task`/async for real side effects, but synchronous returns are fully supported — there is no "always async" requirement.
41
+ - **Method name** — can be anything descriptive. The name is for readability, not dispatch.
42
+
43
+ ## Handling replay differently — `[Replay]`
44
+
45
+ A reactor sees the same event twice for different reasons: as it happens, and again when its observer is replayed. When those call for different work, mark a second handler for the same event type with **`[Replay]`** and it takes over for the duration of the replay.
46
+
47
+ ```csharp
48
+ public class OrderNotifications(INotificationService notifications) : IReactor
49
+ {
50
+ public Task OrderPlaced(OrderPlaced @event) => notifications.Notify($"Order {@event.Number} placed.");
51
+
52
+ [Replay]
53
+ public Task OrderPlacedDuringReplay(OrderPlaced @event) => Task.CompletedTask;
54
+ }
55
+ ```
56
+
57
+ - With a `[Replay]` handler, **only** it runs during replay — the regular handler does not also run.
58
+ - Without one, the regular handler runs during replay exactly as before, so this changes nothing for existing reactors.
59
+ - An event type handled *only* by a `[Replay]` handler is still subscribed to.
60
+ - Reach for **`[OnceOnly]`** when the side effect must not repeat **during replay**;
61
+ use `[Replay]` when replay needs different work. Neither suppresses ordinary
62
+ catch-up or recovery redelivery, nor limits runs per event source.
63
+
64
+ ## Taking Dependencies
65
+
66
+ Beyond the event and `EventContext`, a handler method can take any number of additional parameters as dependencies. Only the first parameter is fixed (it is the event that drives dispatch); every parameter after it is resolved when the method is invoked.
67
+
68
+ ```csharp
69
+ public class OrderProcessing(IShippingService shipping) : IReactor
70
+ {
71
+ public async Task OrderPlaced(OrderPlaced @event, EventContext context, Order order, IPricingService pricing)
72
+ {
73
+ // 'order' is the read model, resolved by Chronicle for this event's key.
74
+ // 'pricing' is resolved from the service provider.
75
+ await shipping.Schedule(order, pricing.PriceFor(order));
76
+ }
77
+ }
78
+ ```
79
+
80
+ How each parameter is resolved:
81
+
82
+ - **`EventContext`** — receives the event context (position independent).
83
+ - **A read model** — a type that has a reducer or a projection (declarative or model-bound). Chronicle resolves it directly — read models never go through the service provider. A **materialized** read model (the default, for projections and reducers alike) is read from its sink, so it is **eventually consistent** — as current as its observer. Mark it **`[Passive]`** when the reactor needs it **strongly consistent**: a passive read model has no sink, so Chronicle computes it on demand from the events at the point the reactor runs.
84
+ - **Any other type** — resolved from the service provider (constructor injection on the reactor itself is still preferred for collaborators used by every method).
85
+
86
+ ### Resolving the read model key
87
+
88
+ By default the read model is materialized using the `EventSourceId` from the event context. When the key differs from the event source — for example the event carries the id of a related entity — implement `ICanResolveReadModelKey` on the reactor:
89
+
90
+ ```csharp
91
+ public class OrderProcessing : IReactor, ICanResolveReadModelKey
92
+ {
93
+ public ReadModelKey Resolve(object @event, EventContext context) =>
94
+ ((OrderLineAdded)@event).OrderId;
95
+
96
+ public Task OrderLineAdded(OrderLineAdded @event, Order order)
97
+ {
98
+ // 'order' was materialized using OrderId rather than the triggering event's source id.
99
+ return Task.CompletedTask;
100
+ }
101
+ }
102
+ ```
103
+
104
+ The resolved key applies to every read model parameter across all of the reactor's handler methods.
105
+
106
+ ## Returning Side-Effect Events
107
+
108
+ Instead of taking a dependency on `IEventLog`, reactor handler methods can return events that should be appended automatically. The default target is the event log; the EventSourceId defaults to the one from the triggering event.
109
+
110
+ ### Return a single event
111
+
112
+ ```csharp
113
+ public Task<SomeEvent> Handle(AnEvent @event, EventContext context) =>
114
+ Task.FromResult(new SomeEvent(@event.Name));
115
+ ```
116
+
117
+ ### Return a collection of events
118
+
119
+ ```csharp
120
+ public Task<IEnumerable<object>> Handle(AnEvent @event, EventContext context) =>
121
+ Task.FromResult<IEnumerable<object>>([new SomeEvent(), new AnotherEvent()]);
122
+ ```
123
+
124
+ ### Target a specific event source id via `EventForEventSourceId`
125
+
126
+ Return an `EventForEventSourceId` to append to an explicit `EventSourceId` — for example a different entity than the one that triggered the reactor. It also carries the `EventStreamType`, `EventStreamId`, `EventSourceType`, `Subject`, `Occurred` time, `Tags` and `Causation` per event:
127
+
128
+ ```csharp
129
+ public Task<EventForEventSourceId> Handle(AnEvent @event, EventContext context) =>
130
+ Task.FromResult(new EventForEventSourceId(@event.RelatedId, new SomeEvent(@event.Name))
131
+ {
132
+ EventStreamType = new EventStreamType("custom"), // optional
133
+ EventSourceType = new EventSourceType("order"), // optional
134
+ Subject = new Subject(@event.RelatedId), // optional
135
+ });
136
+ ```
137
+
138
+ ### Return events for multiple event source ids
139
+
140
+ Return `IEnumerable<EventForEventSourceId>` to append to several event source ids in one transaction:
141
+
142
+ ```csharp
143
+ public Task<IEnumerable<EventForEventSourceId>> Handle(AnEvent @event, EventContext context) =>
144
+ Task.FromResult<IEnumerable<EventForEventSourceId>>(
145
+ [
146
+ new(@event.RelatedId, new SomeEvent()),
147
+ new(@event.OtherId, new AnotherEvent())
148
+ ]);
149
+ ```
150
+
151
+ > Reactor-level metadata (`ICanProvideEventSourceId`, `ICanProvideSubject`, `[EventStreamType]`, …) applies to bare `TEvent` returns. An `EventForEventSourceId` is self-describing, so its own values are used instead. You can mix bare events and `EventForEventSourceId` in a single `IEnumerable<object>` return — each is appended with its respective metadata, all in one transaction.
152
+
153
+ ### Cross-stream via `EventForEventSourceId`
154
+
155
+ To append a side-effect event to a **different** event source, return `EventForEventSourceId(id, @event)` (single or `IEnumerable<EventForEventSourceId>`) — the same cross-stream wrapper a command `Handle()` uses. The wrapper is self-describing, so it is also where you set the event stream type and id, source type, `Subject`, occurred time, tags and causation; set only the ones you need and the rest take the append defaults.
156
+
157
+ ```csharp
158
+ public Task<IEnumerable<EventForEventSourceId>> Handle(AnEvent @event, EventContext context) =>
159
+ Task.FromResult<IEnumerable<EventForEventSourceId>>(
160
+ [
161
+ new EventForEventSourceId(@event.RelatedId, new SomeEvent())
162
+ ]);
163
+ ```
164
+
165
+ > **Chronicle version note:** reactor side-effect handling of `EventForEventSourceId` wrappers has shipped since Chronicle 15.35.
166
+
167
+ ### Return a command (Arc + Chronicle)
168
+
169
+ When the follow-up belongs to **another slice**, return its `[Command]` instead of an event — the same idiom as returning events, applied to intent. Arc's Chronicle integration executes a returned command through the full pipeline (validation, authorization, `Handle()`) in its own service scope; a collection is executed in order when **every** element is a command (events and commands do not mix in one return). An unauthorized, invalid or throwing result is a **side-effect failure** — it fails the partition like a failed append, so it is never silently dropped.
170
+
171
+ ```csharp
172
+ public class RenewalBilling : IReactor
173
+ {
174
+ [OnceOnly]
175
+ public Task<IssueRenewalInvoice> MembershipRenewed(MembershipRenewed @event, EventContext context) =>
176
+ Task.FromResult(new IssueRenewalInvoice(@event.MemberId, @event.Amount));
177
+ }
178
+ ```
179
+
180
+ - **A reactor runs with no principal.** It is not an HTTP request, so a returned command gated by `[Roles]`/`[Authorize]` is denied. Opt in on the reactor **class** with `[ExecuteCommandsAsSystem("<role>", …)]` (`Cratis.Arc.Chronicle.Reactors`), naming only the roles its returned commands need. The attribute covers **returned** commands only.
181
+ - Mark the handler `[OnceOnly]` — a replay would otherwise execute the command again. Replay exclusion is not exactly-once: recovery re-delivers, so the command itself must be safe to repeat.
182
+ - A reactor retry can execute the command's operations again; inline operations are not a substitute for reactor/outbox durability — see **cratis-arc-command-operation** (verified at Arc v22.48.1).
183
+ - The imperative alternative — inject `ICommandPipeline` and call `Execute(command)` — remains supported (see the **cratis-arc-command-execution** skill). It needs `[OnceOnly]` (`ARCCHR0006` warns otherwise), its `CommandResult` must be inspected (a denied or invalid result is otherwise discarded), and it needs its own execution context: `[ExecuteCommandsAsSystem]` does not apply to it.
184
+
185
+ ## External event stores (outbox / inbox)
186
+
187
+ Cross-service events route through Chronicle's outbox/inbox rather than a shared log:
188
+
189
+ - The producing service appends its **public contract event** to `EventSequenceId.Outbox`.
190
+ - The consuming observer listens to the implicit inbox sequence for the source store (`inbox-<source-store>`).
191
+ - Chronicle creates/reuses the outbox→inbox subscription from the observer's `[EventStore]` metadata, or from `[EventStore]` on the event type/assembly. Put `[assembly: EventStore("<source-store>")]` in the contracts project when every event there originates from one service; use observer-level `[EventStore("<source-store>")]` to override.
192
+ - An observer-level `[EventStore]` **cannot** be combined with `[EventSequence]`, `[EventLog]`, or `Reactor(eventSequence: ...)`.
193
+
194
+ ## Storage lookup failures are not absence
195
+
196
+ At **Chronicle v19.50.1**, a failed next-event lookup leaves the observer's
197
+ position unchanged. Routing defers delivery and the watchdog retries; only
198
+ `NotFound` proves absence and permits fast-forward. The watchdog likewise does
199
+ not advance on storage error. This fixes #4770, not every legacy delivery or
200
+ completion limitation. Quarantined observers do not resume from this retry.
201
+ Sources: `Source/Kernel/Core/Observation/States/Routing.cs` and
202
+ `Observation/Observer.Watchdog.cs` at v19.50.1.
203
+
204
+ ## Confirming downstream completion — `WaitForCompletion`
205
+
206
+ The default is fire-and-forget. When a caller's correctness depends on all observers (projections, reducers, reactors) having processed an appended event before returning — integration tests, synchronous webhook surfaces — call `WaitForCompletion` (from `Cratis.Chronicle.Observation`) on the `AppendResult`/`AppendManyResult`. At Chronicle **v19.50.0**, it returns `AppendResultWaitForCompletionResult` with
207
+ `IsSuccess`, `FailedPartitions`, `TimedOut` and `OutstandingObservers`. Inspect
208
+ all of these: timeout returns `TimedOut = true`, not a successful completion.
209
+ The optional `TimeSpan?` defaults to a 5-second server wait plus 200 ms client
210
+ grace; `Timeout.InfiniteTimeSpan` disables both deadlines. Filtering excludes
211
+ observers that do not consume the appended events when the client supplies the
212
+ first sequence number. Source: `Source/Clients/DotNET/Observation/AppendResultWaitForCompletionExtensions.cs`.
213
+ Reach for it deliberately, not as a default.
214
+
215
+ ## Critical Rules
216
+
217
+ 1. **Idempotent** — Reactors may be called more than once for the same event (e.g. during replay or recovery). Design accordingly. For a side effect that must **not** repeat on replay (emails, payments, external writes), mark the handler method `[OnceOnly]` — Chronicle then skips that handler for every event arriving as part of a **replay** (observer rewind, redaction, revision). That is the whole of it: `[OnceOnly]` is replay-exclusion, **not** exactly-once and **not** a per-event-source counter. Recovering a failed partition re-delivers the event as an ordinary observation, so the handler runs again — which is the point of a retry. When the side effect must survive that retry too, take a `ReactorDelivery` parameter alongside the attribute and keep a receipt under its identity, which is stable across the failure and the recovery.
218
+ 2. **Use event data directly** — Never query the read model back inside a reactor. The event contains all the information you need.
219
+ 3. **Return events and commands instead of injecting the plumbing** — If the reactor needs to produce new events, return them directly as `Task<TEvent>`, `Task<EventForEventSourceId>`, or a collection thereof. For intent in another slice, return the `[Command]` (Arc executes it; a denied or invalid result fails the partition) — or inject `ICommandPipeline` and inspect the result. Either way the handler is `[OnceOnly]`. Avoid injecting `IEventLog` directly into a reactor.
220
+ 4. **Stable identity and single responsibility** — Give side-effecting reactors
221
+ an explicit `[Reactor("stable-id")]`: full-type-name identity changes on a rename
222
+ or move and catches up from the start, repeating side effects even with `[OnceOnly]`.
223
+ Chronicle v19.50.0 discovery warns on likely drift; `ArtifactIdentityDrift = Fail`
224
+ rejects Warning-level reactor/event-type drift (not Information-level read-model
225
+ findings). Each reactor class should have a focused purpose. Multiple handler methods in one reactor are fine if they serve the same automation concern.
226
+ 5. **Failure behavior** — If a reactor throws, *or* a returned side-effect event fails to append (constraint violation, concurrency violation, or error), the failing event-source partition pauses until the issue is resolved. Repeated failures can **quarantine** the observer: once `QuarantineOnFailedPartitionCount`/`QuarantineOnFailedPartitionPercentage` (under `Observers`) is crossed, the observer enters the `Quarantined` state — reminders cancelled, retries stopped, automatic recovery suppressed. **A quarantine ends when an operator clears it (`ClearObserverQuarantine()`), or when the observer is subscribed again** — for an application observer that is when the client connects again, for example after a redeploy; Kernel-owned observers are subscribed again when the Kernel starts. A periodic watchdog (default 60s, `WatchdogInterval`) re-routes stuck/dead observers but does not rescue quarantined ones. Design for resilience.
227
+ 6. **Don't throw to validate malformed inbound events** — reactors are not data-quality validators; invalid payloads must be rejected at the command/append site. When a malformed cross-service fact reaches a consumer, throwing just to reject it pauses the partition and can quarantine the whole observer. Instead append a clear failure/dead-letter event (e.g. `ProvisioningFailed`) or surface it via the operational failure path, and skip partial side effects.
228
+ 7. **No state** — Reactors should be stateless. Inject dependencies via primary constructor, but do not store mutable state on the class.
229
+ 8. **An unattended, irreversible pass reads the log, not the sink — and carries a fuse.** A reactor that deletes, revokes, bills or notifies *because a read model says something is absent* is trusting a materialized sink that is only as current as its observer: a paused partition, a replay in progress or a lagging projection makes "absent" true of the sink and false of the world. Decide from a **`[Passive]`** read model (computed on demand from the events at the moment the reactor runs) or from the events themselves. And the code shape cannot tell an **empty subject set** ("nothing to consider" — fail-safe) from an **empty qualifying set** ("nothing survived the filter" — fail-destructive: every subject is about to be acted on), so an unattended pass states its expected population, refuses when the count is implausible, and caps how many subjects one run may touch — the same discipline as [guards-and-fuses.md](./guards-and-fuses.instructions.md), applied to a reactor.
230
+
231
+ ## Failure transitions **[convention]**
232
+
233
+ A domain failure must produce a timely, observable workflow outcome rather than
234
+ leave work looking active until a watchdog notices. Assign the transition to a
235
+ reliable consumer: an existing state machine can be sufficient; do not add a
236
+ redundant failure reactor when it already owns the event. Watchdogs cover missing
237
+ signals, not ordinary handling of an already recorded failure. Specify the visible
238
+ transition, repeated delivery, and stale failures from earlier attempts. Keep
239
+ public reasons free of secrets. See **cratis-chronicle-reactor** for the distinction
240
+ between domain failure and an observer's operational failure.
241
+
242
+ ## Slice Types That Use Reactors
243
+
244
+ | Slice type | Pattern |
245
+ |------------|---------|
246
+ | **Automation** | Reacts to our own events, makes decisions, triggers side effects: an external call, a follow-up event (`produces`), or a follow-up command in another slice (`invokes`) |
247
+ | **Translation** | Takes data from outside the model's own facts (an external system or another service) and records it as our own facts. A reactor is only its adapter code; a reactor that turns our own event into a follow-up event or command is an Automation |
248
+
249
+ ### Automation Example
250
+
251
+ ```csharp
252
+ public class InvoiceIssuedNotifier(INotificationService notifications) : IReactor
253
+ {
254
+ /// <summary>
255
+ /// Sends a notification when an invoice is issued.
256
+ /// </summary>
257
+ /// <param name="event">The event.</param>
258
+ /// <param name="context">The event context.</param>
259
+ public async Task InvoiceIssued(InvoiceIssued @event, EventContext context) =>
260
+ await notifications.Notify($"Invoice '{@event.Number}' was issued.");
261
+ }
262
+ ```
263
+
264
+ ### Automation Example — Returning a Command
265
+
266
+ ```csharp
267
+ [ExecuteCommandsAsSystem("invoicing")]
268
+ public class RenewalBilling : IReactor
269
+ {
270
+ /// <summary>
271
+ /// Reacts to a membership renewal by invoking the invoice issuing in the invoicing slice.
272
+ /// </summary>
273
+ /// <param name="event">The event.</param>
274
+ /// <param name="context">The event context.</param>
275
+ /// <returns>The command Arc executes as this reactor's side effect.</returns>
276
+ [OnceOnly]
277
+ public Task<IssueRenewalInvoice> MembershipRenewed(MembershipRenewed @event, EventContext context) =>
278
+ Task.FromResult(new IssueRenewalInvoice(@event.MemberId, @event.Amount));
279
+ }
280
+ ```
281
+
282
+ The command is returned, not executed: Arc runs it through validation and authorization under the roles the class opts into, and a rejected command fails the partition instead of vanishing. `[OnceOnly]` keeps a replay from issuing the renewal invoice twice.
283
+
284
+ ## Contract checks for a reactor
285
+
286
+ - **Field lineage:** every field of a returned command or event comes from the trigger event, an injected read model, or a mapping the contract states. Nothing is invented or defaulted.
287
+ - **Contract-authorized filtering:** every condition that skips an event is stated in the contract; do not add a skip the contract does not name.
288
+ - **Repeated delivery:** write a specification for the same event arriving again. Recovery re-delivers even with `[OnceOnly]`.
289
+
290
+ ## Testing Reactors
291
+
292
+ Start with `Specification` plus a direct call for pure reactor decisions, passing the event and supplied values explicitly. Add `ReactorScenario<TReactor>` (from `Cratis.Chronicle.Testing.Reactors`) only where invocation, dependency wiring or side effects contribute proof — construct it with an `IServiceProvider` of NSubstitute mocks, fire events through `Given`, and assert on the mocks:
293
+
294
+ ```csharp
295
+ void Establish()
296
+ {
297
+ _notifications = Substitute.For<INotificationService>();
298
+ _scenario = new(new ServiceCollection().AddSingleton(_notifications).BuildServiceProvider());
299
+ }
300
+
301
+ async Task Because() => await _scenario.Given.ForEventSource(_id).Events(new InvoiceIssued("INV-1001"));
302
+
303
+ [Fact] async Task should_notify_about_the_issued_invoice() => await _notifications.Received(1).Notify(Arg.Is<string>(_ => _.Contains("INV-1001")));
304
+ ```
305
+
306
+ ### Read-model dependencies in scenarios — Chronicle 19.46.0+
307
+
308
+ `ReactorScenario<TReactor>` does **not** build read models from `Given` events,
309
+ including `[Passive]` models. Seed the handler's read-model parameter explicitly
310
+ before firing the trigger:
311
+
312
+ ```csharp
313
+ void Establish() => _scenario.Given.ForEventSourceId(_id).ReadModel(_order);
314
+ async Task Because() => await _scenario.Given.ForEventSource(_id).Events(_orderPlaced);
315
+ ```
316
+
317
+ In a separate absence specification, replace the seeding with
318
+ `_scenario.Given.ForEventSourceId(_id).NoReadModel<Order>()` to deliberately pass
319
+ `null` to an `Order?` parameter. Without either setup, resolution to `null` throws
320
+ `ReadModelNotSeededForReactorScenario` (`Cratis.Chronicle.Testing.Reactors`), even
321
+ for a nullable parameter. Its message names the type and missing key, suggests
322
+ both setup methods, and lists other seeded keys for that type when any exist.
323
+ Use the key from `ICanResolveReadModelKey.Resolve` for seeding or absence when
324
+ implemented, not the triggering source id. `NoReadModel<T>()` does not remove a
325
+ seeded instance. This guard applies to `EventStoreForTesting`, not an explicitly
326
+ supplied store of another type; production read-model resolution is unchanged.
327
+
328
+ Also in Chronicle 19.46.0+, `Events(...)` surfaces a synchronously throwing
329
+ handler's own exception, preserving its stack, rather than the reflection
330
+ `TargetInvocationException`. Capture it with `Catch.Exception` and assert the
331
+ handler's exception type. `EventContext.From(...)` now defaults an omitted
332
+ `occurred` to `DateTimeOffset.UtcNow`, not local `Now`; scenario contexts,
333
+ `EventContext.Empty` and `EmptyWithEventSourceId(...)` use that helper. An
334
+ explicit occurrence time is preserved, not normalized to UTC.
335
+
336
+ For reactors that return side-effect events, assert the resulting appends through the scenario's event store; for non-event side effects, assert on the mocked services (as above). See [specs.scenarios.csharp.md](./specs.scenarios.csharp.instructions.md) for the full `*Scenario` family.
@@ -0,0 +1,48 @@
1
+ ---
2
+ applyTo: "**/*"
3
+ description: "Use when running any shell command, reading files, or searching code. Route commands through rtk for human-readable output (a hook auto-rewrites Bash commands; prefix rtk yourself where it does not); use raw output when the result is parsed, compared exactly, or drives another operation."
4
+ ---
5
+ <!-- cratis-ai: generated Copilot instructions. Do not edit; rules/ is the source. -->
6
+
7
+ # Using rtk (Token-Optimized Commands)
8
+
9
+ [rtk](https://github.com/rtk-ai/rtk) (Rust Token Killer) is a CLI proxy that filters and compresses command output *before it reaches the model* — typically **60–90% fewer tokens** on common dev commands, with no loss of the signal you actually need. The default in this corpus is to **run every command rtk supports through rtk**. The savings are real and compound across a session: build, test, lint, git, search, and file-read output are exactly the high-volume, low-signal outputs rtk trims.
10
+
11
+ ## How it works — let the hook do its job
12
+
13
+ A `PreToolUse` hook **auto-rewrites Bash commands** to their `rtk` equivalent transparently and at zero token overhead (`git status` → `rtk git status`). For any supported command you do **nothing special** — run it normally and the hook wraps it. Where no hook is configured, prefix the command yourself — including each command in an `&&` chain: `rtk git add . && rtk git commit -m "msg"`.
14
+
15
+ - **Audit coverage** with `rtk gain` (savings so far) and `rtk discover` (commands that slipped past rtk — missed opportunities to close).
16
+
17
+ ## When to use raw output instead
18
+
19
+ rtk output is a summary for a reader. It is never evidence of exact file contents, paths, Git state, or command semantics, and a filter can drop the line you needed. Use the raw command — `rtk proxy <cmd>`, or the plain command where nothing rewrites it — whenever:
20
+
21
+ - the output is parsed by a script, compared exactly, or fed to another command;
22
+ - the output is the evidence for a claim about content, paths, or Git state;
23
+ - the command's exit code or exact diagnostics decide what happens next (security checks, structured JSON, machine-readable diagnostics);
24
+ - a wrapped command failed. Read the actual error and retry through `rtk proxy` before concluding the tool, path, or repository is missing.
25
+
26
+ Do not add `rtk` to scripts, hooks, or CI steps that deliberately use native commands; their raw output is the point. Exit codes are never traded for token savings — see [`exit-codes-and-wrappers.md`](./exit-codes-and-wrappers.instructions.md).
27
+
28
+ ## What rtk supports (route these through rtk)
29
+
30
+ - **Files** — `ls`, `tree`, `read`, `find`, `grep`, `diff`
31
+ - **Git & GitHub** — `git status/log/diff/add/commit/push/pull`, `gh pr/issue/run …`
32
+ - **Tests** — `dotnet test`, Jest, Vitest, Playwright, pytest, Go, Cargo, RSpec
33
+ - **Build & lint** — `dotnet build`, `tsc`, ESLint, Biome, Prettier, Cargo Clippy, Ruff, golangci-lint, Rubocop
34
+ - **Package managers** — pnpm/npm/yarn, pip, Bundler, Prisma
35
+ - **Cloud & containers** — AWS CLI, Docker, Kubernetes, OpenShift
36
+
37
+ In practice this means the Cratis **quality-gate commands** — `dotnet build`, `dotnet test`, `yarn lint`, `npx tsc -b`, `git`, `gh` — all flow through rtk automatically. Just run them.
38
+
39
+ ## The one gap — built-in tools bypass the hook
40
+
41
+ The hook only sees **Bash** commands. Claude Code's built-in `Read`, `Grep`, and `Glob` tools (and the Copilot equivalents) do **not** pass through it, so they are **not** auto-rewritten — and those are some of the most token-heavy operations in a session.
42
+
43
+ - For **bulk reads and broad searches** where output volume is large, call **`rtk read` / `rtk grep` / `rtk find`** from the terminal so the savings are captured. This is a deliberate override of the usual "prefer the built-in file tools" default.
44
+ - Keep the **built-in** `Read`/`Grep`/`Glob` for **small, targeted reads** and when you need exact line references for an edit — rtk filters output, so it serves exploration and volume, not the precise content an `Edit` must match verbatim.
45
+
46
+ ## Availability
47
+
48
+ This assumes the `rtk` binary is installed and the hook configured (`rtk init -g`). If `rtk` is **not** on `PATH`, ignore this rule and use the normal tools — never block work on rtk being present.
@@ -0,0 +1,147 @@
1
+ ---
2
+ applyTo: "**/for_*/**/*.cs, **/when_*/**/*.cs"
3
+ paths:
4
+ - "**/for_*/**/*.cs"
5
+ - "**/when_*/**/*.cs"
6
+ ---
7
+ <!-- cratis-ai: generated Copilot instructions. Do not edit; rules/ is the source. -->
8
+
9
+ # How to Write C# Specs
10
+
11
+ Extends the base [specs.md](./specs.instructions.md) (folder structure, naming, philosophy) with C#-specific conventions. This file is the **universal foundation** used in **both** profiles — the `Cratis.Specifications` pattern (`Establish → Because → should_`) with NSubstitute.
12
+
13
+ > **Which spec surface?**
14
+ > - **Framework / library code** (Chronicle kernel, Arc pipeline, source generators, Fundamentals): the `Specification` base + NSubstitute in this file is the dominant mode — unit-test the classes under test in isolation. Reach for a `*Scenario` helper (in [specs.scenarios.csharp.md](./specs.scenarios.csharp.instructions.md)) **only** when testing the very engine your repo provides — Arc → `CommandScenario`, Chronicle → the event/projection/reactor scenarios. The **cratis-application-slice-specifications** skill is application-oriented, not for general framework specs.
15
+ > - **Event-sourced applications on Cratis** (commands, projections, reducers, reactors, constraints): use this base plus a direct call by default for pure decisions; add the in-process scenario family only where pipelines, validation, constraints, projections or wiring contribute proof — see **[specs.scenarios.csharp.md](./specs.scenarios.csharp.instructions.md)** (`profile: application`) and the **cratis-application-slice-specifications** skill.
16
+
17
+ `Cratis.Specifications` keeps the approach of Machine.Specifications (MSpec): `Establish → Because → should_` maps to "Given → When → Then" and keeps each spec focused on *one setup, one action, one set of assertions*.
18
+
19
+ ## Frameworks
20
+
21
+ - [xUnit](https://xunit.net/) **2.x** for execution (not v3).
22
+ - Reference `Cratis.Specifications.XUnit` **4.1.1** for the `Specification` base, convention lifecycle and `ShouldXxx` assertions; the namespace remains `Cratis.Specifications`. Its `Cratis.Specifications` dependency supplies `Catch` and `SpecificationMethods`, not the base or assertions.
23
+ - 4.x targets net8.0/net9.0/net10.0; net48 requires 3.x. `Cratis.Specifications.NUnit` is an alternative with `[Test]` and public test methods; xUnit remains the house default.
24
+ - [NSubstitute](https://nsubstitute.github.io/) for mocking collaborators.
25
+ - Spec projects are named `<Source>.Specs`.
26
+
27
+ ## BDD pattern
28
+
29
+ `Establish` sets up the world, `Because` performs the single action under test, `should_*` facts verify individual outcomes. Any method can be `async Task`.
30
+
31
+ ```csharp
32
+ public class when_combining_parts : Specification
33
+ {
34
+ object[] _parts;
35
+ string _result;
36
+
37
+ void Establish() => _parts = ["First", "Second", "Third"];
38
+ void Because() => _result = KeyHelper.Combine(_parts);
39
+
40
+ [Fact] void should_combine_all_parts() => _result.ShouldEqual("First+Second+Third");
41
+ }
42
+ ```
43
+
44
+ | Method | Purpose | Notes |
45
+ |---|---|---|
46
+ | `void Establish()` | setup, before `Because()` | each class in the chain has its own, run base-first — never call `base.Establish()` |
47
+ | `void Because()` | the single action under test | only in concrete spec files, never in a reusable context |
48
+ | `[Fact] void should_*()` | one assertion per fact | use the `ShouldXxx()` extension methods |
49
+ | `void Destroy()` | teardown after each fact by default | once per eligible class with shared lifecycle opt-in |
50
+
51
+ Shared lifecycle is optional (4.1.0+): add `[assembly: Xunit.TestFramework("Cratis.Specifications.SpecificationTestFramework", "Cratis.Specifications.XUnit")]`. Eligible classes derive from `Specification`, have one public parameterless constructor, do not implement `IDisposable`, and contain only plain facts. Construction, Establish/Because and Destroy run once; all facts share the instance and must not mutate it. Initialization failure fails every fact. Other classes keep per-fact execution. Keep the stock lifecycle by default; details are in the **cratis-specifications-csharp** skill's shared-lifecycle reference.
52
+
53
+ CRSPEC0001–CRSPEC0007 are warnings bundled in `.Specifications`, `.XUnit`, and `.NUnit` 4.0.0+. With warnings as errors, a non-`should_` fact, multiple specs per file (given contexts exempt), a fact on a given context, a `should_` without a test attribute, explicit base lifecycle calls, non-public specs with facts, or `Because` on a given context fail the build. Follow **cratis-specification-by-example**; do not suppress these diagnostics.
54
+
55
+ ## Reusable layered contexts
56
+
57
+ Layered contexts (`all_dependencies → a_<sut> → when_*`) capture shared setup base-first: the base mocks dependencies, the next builds the system under test, each spec adds only what's unique.
58
+
59
+ ```csharp
60
+ // given/all_dependencies.cs — mocks all deps
61
+ public class all_dependencies : Specification
62
+ {
63
+ protected IEventStore _eventStore;
64
+ void Establish() => _eventStore = Substitute.For<IEventStore>();
65
+ }
66
+
67
+ // given/a_reactor_handler.cs — builds the system under test
68
+ public class a_reactor_handler : all_dependencies
69
+ {
70
+ protected ReactorHandler _handler;
71
+ void Establish() => _handler = new(_eventStore);
72
+ }
73
+ ```
74
+
75
+ Put the action under test only in the concrete spec's `Because()`, never in a reusable context.
76
+
77
+ ## NSubstitute patterns
78
+
79
+ ```csharp
80
+ _service.GetValue(Arg.Any<string>()).Returns("result");
81
+ Arg.Is<Request>(r => r.Id == expectedId) // matcher
82
+ _service.Received(1).DoSomething(Arg.Any<string>()); // verify
83
+ _service.DidNotReceive().DoSomethingElse(Arg.Any<int>());
84
+ _handler.Handle(Arg.Any<CommandContext>()).Throws(new Exception("fail"));
85
+ ```
86
+
87
+ Catch exceptions for a unit under test with `Catch.Exception`:
88
+
89
+ ```csharp
90
+ async Task Because() => _error = await Catch.Exception(_sut.DoSomething);
91
+ [Fact] void should_not_fail() => _error.ShouldBeNull();
92
+ ```
93
+
94
+ ## Assertion extension methods
95
+
96
+ From the `Cratis.Specifications.XUnit` package (namespace `Cratis.Specifications`): `.ShouldEqual(expected)`, `.ShouldBeTrue()`, `.ShouldBeFalse()`, `.ShouldBeNull()`, `.ShouldNotBeNull()`, `.ShouldBeEmpty()`, `.ShouldNotBeEmpty()`, `.ShouldContain(item)`, `.ShouldNotContain(item)`, `.ShouldContainOnly(items)`, `.ShouldBeOfExactType<T>()`, `.ShouldBeGreaterThan(n)`, `.ShouldBeLessThan(n)`.
97
+
98
+ Additional helpers include `ShouldNotEqual`, `ShouldBeSame`/`ShouldNotBeSame`, `ShouldMatch(Func<T, bool>)`, `ShouldBeInRange`, inclusive comparisons `ShouldBeGreaterThanOrEqual`/`ShouldBeLessThanOrEqual`, `ShouldContainSingleItem`, `ShouldEachConformTo`, ordered collection `ShouldEqual(IEnumerable<T>)`, collection `ShouldContain(Predicate<T>)`, `ShouldBeAssignableFrom<T>`, and string `ShouldContain(substring, StringComparison)`. `Catch.Exception` accepts `Action` or `Func<Task>`; `Catch.Exception<T>(Action)` catches only T.
99
+
100
+ ## Conventions
101
+
102
+ - Common usings come from `GlobalUsings.Specs.cs` (`Xunit`, `NSubstitute`, `Cratis.Specifications`, …) — don't duplicate them, and don't add a using for the system-under-test namespace.
103
+ - **`using` ordering (SA1210/SA1211):** non-aliased namespaces first, then a blank line, then `using <alias> = …` aliases sorted by alias name. Alias a type whose short name collides with a namespace segment (CS0118) — `using RegisterAuthorCmd = …` style, with a domain-meaningful alias, never a technical `Command`/`Event`/`Component` suffix.
104
+ - Single-statement assertion lambdas use expression-body form (RCS1021) — `[Fact] void should_x() => result.ShouldEqual(...)`, not a `{ … }` block. Don't break long `should_` lines; don't add blank lines between `should_` methods.
105
+ - **Prefer a concept's sentinel over a raw empty primitive.** When a `ConceptAs<T>` / `EventSourceId<T>` value is expected — as an argument, a mock setup, or an assertion — use its `NotSet` (or `Empty` / `New()`) static rather than a `string.Empty` / `Guid.Empty` / `0` that implicitly converts. It states intent and survives a sentinel-value change (see [concepts.md](./concepts.instructions.md)). Reserve raw `string.Empty` for genuinely non-concept `string` values (e.g. asserting a decrypted-to-empty JSON payload).
106
+
107
+ ## Never sleep — await a fact, not a duration
108
+
109
+ **A spec never waits on the clock for the system under test.** `Thread.Sleep`, a bare `Task.Delay`, or a `SpinWait` before an assertion makes the spec pass because the machine happened to be fast enough, and it silently writes today's latency into the suite: the next fix that shifts timing in the observer/projection core then breaks specs that have nothing to do with it. That coupling — not the bugs being fixed — is the single largest source of regressions in Chronicle, and it is why fixes there keep getting rolled back.
110
+
111
+ Wait on a signal instead:
112
+
113
+ | Waiting for | Await |
114
+ |---|---|
115
+ | observers to catch up with an append | `appendResult.WaitForCompletion()` |
116
+ | client artifacts to be registered with the kernel | `eventStore.WaitForRegistration()` |
117
+ | an observer's state or position | `WaitTillActive` / `WaitTillSubscribed` / `WaitTillReachesEventSequenceNumber` / `WaitForState` (reactors, reducers, projections) |
118
+ | a job | the `JobsWaitHelpers` extensions |
119
+ | anything with no helper yet | build the signal: a `SemaphoreSlim` (or `TaskCompletionSource`) released by the code that observes the event, awaited under a timeout — `Source/Clients/XUnit.Integration/EventAppendCollection.cs` is the shipped example, and it never polls |
120
+
121
+ A **deadline** is not a sleep — every helper above takes a `timeout`, and a timeout is what turns a hang into a named failure. Sleeping *between* re-checks is: a poll loop is a completion signal you have not built yet, so add the signal rather than another `while` + `Task.Delay`.
122
+
123
+ Three delays are not waits at all and stay allowed — say which one it is in a comment: a test double that is **slow on purpose** so the test can observe it mid-flight (`Task.Delay(HandleTime)`), an **infrastructure readiness backoff** between retries of a connect/start that can legitimately fail, and a `Task.Delay(1)` / `Task.Yield()` that **widens an interleaving window** in a concurrency spec.
124
+
125
+ The sleeps that predate this rule are frozen per file in `.github/timing-coupling-baseline.txt`; `.github/workflows/timing-coupling-ratchet.yml` fails a pull request that raises any entry or adds a file. The numbers may only go down.
126
+
127
+ ## What NOT to spec
128
+
129
+ Simple properties are compiler-verified. Save spec effort for business logic, coordination between dependencies, and complex transformations.
130
+
131
+ ```csharp
132
+ // ❌ Do NOT spec these
133
+ public string TableName => tableName; // returns constructor parameter
134
+ public IEnumerable<Property> Properties => mapper.Properties; // simple delegation
135
+
136
+ // ✅ Spec complex business logic
137
+ public decimal TotalCost => Items.Sum(i => i.Cost * i.Quantity * (1 + i.TaxRate));
138
+ ```
139
+
140
+ Also don't spec logging or trivial getters.
141
+
142
+ ## See also
143
+
144
+ - [specs.md](./specs.instructions.md) — folder structure, naming, BDD philosophy (the base this extends).
145
+ - [specs.scenarios.csharp.md](./specs.scenarios.csharp.instructions.md) — **application profile**: the in-process scenario family (`CommandScenario` / `EventScenario` / `ReadModelScenario` / `ReactorScenario`) + out-of-process Chronicle integration, for event-sourced apps.
146
+ - [efcore.specs.md](./efcore.specs.instructions.md) — `DbContext` specs with SQLite in-memory.
147
+ - [frontend-testing.md](./frontend-testing.instructions.md) — TypeScript/React application BDD specs.