@eventmodelers/cli 1.0.73 → 1.0.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -3
- package/cli.js +0 -7
- package/package.json +2 -2
- package/stacks/blank/templates/.claude/skills/build-automation/SKILL.md +1 -1
- package/stacks/blank/templates/.claude/skills/build-state-change/SKILL.md +1 -1
- package/stacks/blank/templates/.claude/skills/build-state-view/SKILL.md +1 -1
- package/stacks/blank/templates/root/README.md +1 -1
- package/stacks/modeling-kit/templates/kit/README.md +1 -1
- package/stacks/react/templates/.claude/skills/build-automation/SKILL.md +1 -1
- package/stacks/react/templates/.claude/skills/build-state-change/SKILL.md +1 -1
- package/stacks/react/templates/.claude/skills/build-state-view/SKILL.md +1 -1
- package/stacks/cratis-csharp/templates/.claude/skills/_shared/cratis-conventions.md +0 -251
- package/stacks/cratis-csharp/templates/.claude/skills/build-automation/SKILL.md +0 -122
- package/stacks/cratis-csharp/templates/.claude/skills/build-automation/references/patterns.md +0 -115
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/SKILL.md +0 -191
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/references/patterns.md +0 -234
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/SKILL.md +0 -149
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/references/patterns.md +0 -166
- package/stacks/cratis-csharp/templates/build-kit/CLAUDE.md +0 -78
- package/stacks/cratis-csharp/templates/build-kit/lib/AGENT.md +0 -59
- package/stacks/cratis-csharp/templates/build-kit/lib/backend-prompt.md +0 -140
- package/stacks/cratis-csharp/templates/build-kit/lib/prompt.md +0 -126
- package/stacks/cratis-csharp/templates/root/.frontend/index.css +0 -29
- package/stacks/cratis-csharp/templates/root/.frontend/index.html +0 -17
- package/stacks/cratis-csharp/templates/root/.frontend/main.tsx +0 -18
- package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.json +0 -42
- package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.node.json +0 -11
- package/stacks/cratis-csharp/templates/root/.frontend/vite.config.ts +0 -56
- package/stacks/cratis-csharp/templates/root/App.tsx +0 -23
- package/stacks/cratis-csharp/templates/root/CratisApp.csproj +0 -25
- package/stacks/cratis-csharp/templates/root/CratisApp.sln +0 -18
- package/stacks/cratis-csharp/templates/root/GlobalUsings.cs +0 -3
- package/stacks/cratis-csharp/templates/root/Home.tsx +0 -102
- package/stacks/cratis-csharp/templates/root/Program.cs +0 -26
- package/stacks/cratis-csharp/templates/root/README.md +0 -192
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/AllListings.ts +0 -47
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.cs +0 -11
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.ts +0 -12
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/ListingDataTable.tsx +0 -17
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/index.ts +0 -1
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Register.ts +0 -51
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/RegisterDialog.tsx +0 -18
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Registration.cs +0 -27
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/index.ts +0 -1
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeFeature.tsx +0 -22
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeName.cs +0 -3
- package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/index.ts +0 -1
- package/stacks/cratis-csharp/templates/root/appsettings.Development.json +0 -9
- package/stacks/cratis-csharp/templates/root/appsettings.json +0 -26
- package/stacks/cratis-csharp/templates/root/docker-compose.yml +0 -23
- package/stacks/cratis-csharp/templates/root/package.json +0 -33
- package/stacks/cratis-csharp/templates/root/tsconfig.json +0 -3
package/README.md
CHANGED
|
@@ -18,7 +18,6 @@ Running without `--stack` shows an arrow-key picker. Or go straight to a stack:
|
|
|
18
18
|
npx @eventmodelers/cli init --stack node # Node.js / TypeScript
|
|
19
19
|
npx @eventmodelers/cli init --stack supabase # Supabase
|
|
20
20
|
npx @eventmodelers/cli init --stack axon # Axon Framework (Java/Kotlin)
|
|
21
|
-
npx @eventmodelers/cli init --stack cratis-csharp # Cratis (.NET/C#)
|
|
22
21
|
npx @eventmodelers/cli init --stack opencqrs # OpenCQRS (Java, EventSourcingDB)
|
|
23
22
|
npx @eventmodelers/cli init --stack umadb # UmaDB (Java)
|
|
24
23
|
npx @eventmodelers/cli init --stack kurrent # Kurrent (Java, KurrentDB)
|
|
@@ -89,7 +88,7 @@ your-project/
|
|
|
89
88
|
└── CLAUDE.md ← agent instructions
|
|
90
89
|
```
|
|
91
90
|
|
|
92
|
-
The
|
|
91
|
+
The six backend stacks (`node`, `supabase`, `axon`, `opencqrs`, `umadb`, `kurrent`) also scaffold a real project skeleton into your project root (`templates/root/`) — source layout, build files, migrations, etc.
|
|
93
92
|
|
|
94
93
|
`react` and `supabase-react` are two more registered stacks (installable the same way). `supabase-react` is real, filled-in content — a Vite + React 19 + TypeScript scaffold that authenticates and issues command POSTs via a Supabase session (`src/lib/api.ts`/`src/lib/supabase.ts`), plus `init-style-guide`/`learn-styleguide` skills so generated UI stays on-brand. It's UI-only: `.build-kit/CLAUDE.md` only routes `STATE_CHANGE`/`STATE_VIEW` slices to `build-state-change`/`build-state-view` — an `AUTOMATION` slice has no UI counterpart and gets flagged via `request-feedback` instead, since it belongs to whichever backend stack is installed alongside this one. It needs no overrides at all and uses `shared/build-kit`'s realtime agent as-is.
|
|
95
94
|
|
|
@@ -162,11 +161,19 @@ npx @eventmodelers/cli init --build-kit # blank build-kit scaffold
|
|
|
162
161
|
**Building a new kit for an unsupported stack:**
|
|
163
162
|
|
|
164
163
|
```bash
|
|
165
|
-
npx @eventmodelers/cli init --build-kit
|
|
164
|
+
npx @eventmodelers/cli init --build-kit --demo
|
|
166
165
|
```
|
|
167
166
|
|
|
168
167
|
This scaffolds `.build-kit/CLAUDE.md`, `lib/prompt.md`, `lib/backend-prompt.md`, and the `build-*` skills with TODO placeholders instead of real content. Fill in the TODOs against the actual stack you're integrating (build/test commands, file layout, framework idioms) while building something real with it, then follow "Adding a stack" below to promote it to a first-class stack once it works.
|
|
169
168
|
|
|
169
|
+
`--demo` is what gives you something to build *against* while you do that: it seeds the kit's `.slices/` with the ready-made 16-slice **Understanding Eventsourcing** model (see "Trying it out before you have a board of your own" above), so you don't need a board, credentials, or a connected project to exercise the kit you're writing. With it in place the whole loop is:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
npx @eventmodelers/cli run --local
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
and the agent starts building — `--local` skips platform config and credential lookup entirely, so it works straight out of `init` with nothing connected. Every TODO you fill in gets exercised on the next iteration against real slice data covering all four slice types (state change, state view, automation, translation), which is exactly the coverage a new stack's `build-*` skills need before it's worth promoting.
|
|
176
|
+
|
|
170
177
|
Installing both a build stack and `init-modeling` into the same project reuses this one `.eventmodelers/config.json` — run whichever `init` command second and it finds the existing config already satisfies the required fields and skips straight past the credential prompt.
|
|
171
178
|
|
|
172
179
|
### The modeling agent — `run --modeling` and `--standalone`
|
package/cli.js
CHANGED
|
@@ -63,13 +63,6 @@ const STACKS = {
|
|
|
63
63
|
useShared: true,
|
|
64
64
|
needsBoardId: true,
|
|
65
65
|
},
|
|
66
|
-
'cratis-csharp': {
|
|
67
|
-
label: 'Cratis (.NET/C#)',
|
|
68
|
-
kitSubdir: 'build-kit',
|
|
69
|
-
kitDirName: '.build-kit',
|
|
70
|
-
useShared: true,
|
|
71
|
-
needsBoardId: true,
|
|
72
|
-
},
|
|
73
66
|
opencqrs: {
|
|
74
67
|
label: 'OpenCQRS (Java, EventSourcingDB)',
|
|
75
68
|
kitSubdir: 'build-kit',
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eventmodelers/cli",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon,
|
|
3
|
+
"version": "1.0.74",
|
|
4
|
+
"description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, OpenCQRS, UmaDB, Kurrent, or modeling-only)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"eventmodelers": "cli.js"
|
|
@@ -6,7 +6,7 @@ description: TODO — one-line description of how a reactive slice (a processor/
|
|
|
6
6
|
# Build Automation Slice
|
|
7
7
|
|
|
8
8
|
> **TODO — placeholder installed by `init --build-kit`.** Study an existing stack's
|
|
9
|
-
> build-automation/SKILL.md (stacks/node, stacks/supabase, stacks/axon, or stacks/
|
|
9
|
+
> build-automation/SKILL.md (stacks/node, stacks/supabase, stacks/axon, or stacks/umadb,
|
|
10
10
|
> under `templates/.claude/skills/`) for the level of detail expected, then rewrite
|
|
11
11
|
> every section below for your stack's real conventions — file layout, naming, and
|
|
12
12
|
> the actual framework/language idioms. Delete this callout once done.
|
|
@@ -6,7 +6,7 @@ description: TODO — one-line description of how a write-side slice (command va
|
|
|
6
6
|
# Build State Change Slice
|
|
7
7
|
|
|
8
8
|
> **TODO — placeholder installed by `init --build-kit`.** Study an existing stack's
|
|
9
|
-
> build-state-change/SKILL.md (stacks/node, stacks/supabase, stacks/axon, or stacks/
|
|
9
|
+
> build-state-change/SKILL.md (stacks/node, stacks/supabase, stacks/axon, or stacks/umadb,
|
|
10
10
|
> under `templates/.claude/skills/`) for the level of detail expected, then rewrite
|
|
11
11
|
> every section below for your stack's real conventions — file layout, naming, and
|
|
12
12
|
> the actual framework/language idioms. Delete this callout once done.
|
|
@@ -6,7 +6,7 @@ description: TODO — one-line description of how a read-side slice (a projectio
|
|
|
6
6
|
# Build State View Slice
|
|
7
7
|
|
|
8
8
|
> **TODO — placeholder installed by `init --build-kit`.** Study an existing stack's
|
|
9
|
-
> build-state-view/SKILL.md (stacks/node, stacks/supabase, stacks/axon, or stacks/
|
|
9
|
+
> build-state-view/SKILL.md (stacks/node, stacks/supabase, stacks/axon, or stacks/umadb,
|
|
10
10
|
> under `templates/.claude/skills/`) for the level of detail expected, then rewrite
|
|
11
11
|
> every section below for your stack's real conventions — file layout, naming, and
|
|
12
12
|
> the actual framework/language idioms. Delete this callout once done.
|
|
@@ -7,7 +7,7 @@ layout, a slices directory the skills in `.claude/skills/` generate into, and wh
|
|
|
7
7
|
local infra (docker-compose, migrations, etc.) it needs to run.
|
|
8
8
|
|
|
9
9
|
See an existing stack's `templates/root/` in the eventmodelers-cli source —
|
|
10
|
-
`stacks/node`, `stacks/supabase`, `stacks/axon`, `stacks/
|
|
10
|
+
`stacks/node`, `stacks/supabase`, `stacks/axon`, `stacks/umadb` — for the shape
|
|
11
11
|
a real one takes.
|
|
12
12
|
|
|
13
13
|
Once this is filled in and working, consider contributing it back as a first-class
|
|
@@ -7,7 +7,7 @@ Config directory for modeling-only projects — skills + agent loop, no backend
|
|
|
7
7
|
There's exactly one runtime mode for a modeling-kit install: a single warm Claude
|
|
8
8
|
process, kept alive across turns, that a prompt is written straight into as soon as
|
|
9
9
|
it's fetched off the board's queue. There is no cold-spawn loop and no `tasks.json`
|
|
10
|
-
file — that's what build-kit stacks (`node`, `supabase`, `axon`, `
|
|
10
|
+
file — that's what build-kit stacks (`node`, `supabase`, `axon`, `umadb`) use
|
|
11
11
|
instead, for their independent, self-contained slice-implementation tasks.
|
|
12
12
|
|
|
13
13
|
The loop itself lives in `@eventmodelers/cli`, not in this directory — start it from
|
|
@@ -6,7 +6,7 @@ description: TODO — one-line description of how a reactive slice (a processor/
|
|
|
6
6
|
# Build Automation Slice
|
|
7
7
|
|
|
8
8
|
> **TODO — placeholder installed by `init --build-kit`.** Study an existing stack's
|
|
9
|
-
> build-automation/SKILL.md (stacks/node, stacks/supabase, stacks/axon, or stacks/
|
|
9
|
+
> build-automation/SKILL.md (stacks/node, stacks/supabase, stacks/axon, or stacks/umadb,
|
|
10
10
|
> under `templates/.claude/skills/`) for the level of detail expected, then rewrite
|
|
11
11
|
> every section below for your stack's real conventions — file layout, naming, and
|
|
12
12
|
> the actual framework/language idioms. Delete this callout once done.
|
|
@@ -6,7 +6,7 @@ description: TODO — one-line description of how a write-side slice (command va
|
|
|
6
6
|
# Build State Change Slice
|
|
7
7
|
|
|
8
8
|
> **TODO — placeholder installed by `init --build-kit`.** Study an existing stack's
|
|
9
|
-
> build-state-change/SKILL.md (stacks/node, stacks/supabase, stacks/axon, or stacks/
|
|
9
|
+
> build-state-change/SKILL.md (stacks/node, stacks/supabase, stacks/axon, or stacks/umadb,
|
|
10
10
|
> under `templates/.claude/skills/`) for the level of detail expected, then rewrite
|
|
11
11
|
> every section below for your stack's real conventions — file layout, naming, and
|
|
12
12
|
> the actual framework/language idioms. Delete this callout once done.
|
|
@@ -6,7 +6,7 @@ description: TODO — one-line description of how a read-side slice (a projectio
|
|
|
6
6
|
# Build State View Slice
|
|
7
7
|
|
|
8
8
|
> **TODO — placeholder installed by `init --build-kit`.** Study an existing stack's
|
|
9
|
-
> build-state-view/SKILL.md (stacks/node, stacks/supabase, stacks/axon, or stacks/
|
|
9
|
+
> build-state-view/SKILL.md (stacks/node, stacks/supabase, stacks/axon, or stacks/umadb,
|
|
10
10
|
> under `templates/.claude/skills/`) for the level of detail expected, then rewrite
|
|
11
11
|
> every section below for your stack's real conventions — file layout, naming, and
|
|
12
12
|
> the actual framework/language idioms. Delete this callout once done.
|
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
# Cratis Conventions — Distilled
|
|
2
|
-
|
|
3
|
-
> Self-contained distillation of the Cratis way of building software, taken from the shared
|
|
4
|
-
> Cratis AI configuration (cratis.io). The build skills (`build-state-change`, `build-state-view`,
|
|
5
|
-
> `build-automation`) all rely on these conventions. When in doubt, follow the established pattern in
|
|
6
|
-
> the target project over anything written here — **consistency is king**.
|
|
7
|
-
|
|
8
|
-
## Stack
|
|
9
|
-
|
|
10
|
-
- .NET / C# 13, ASP.NET Core. **Cratis Arc** for CQRS, **Cratis Chronicle** for event sourcing,
|
|
11
|
-
**MongoDB** for read models.
|
|
12
|
-
- React + TypeScript (Vite), PrimeReact UI — generated from C# via **proxy generation** on `dotnet build`.
|
|
13
|
-
- Specs: xUnit + **Cratis.Specifications** + NSubstitute, BDD `for_*/when_*/and_*` layout.
|
|
14
|
-
|
|
15
|
-
## The eight values (apply these when a rule doesn't cover a case)
|
|
16
|
-
|
|
17
|
-
1. **Lovable APIs** — sane defaults, flexible, overridable.
|
|
18
|
-
2. **Easy to do right, hard to do wrong** — convention over configuration; discovery by naming.
|
|
19
|
-
3. **Events are facts** — immutable, never nullable, never ambiguous, never multipurpose. A nullable
|
|
20
|
-
property on an event means you need a *second* event.
|
|
21
|
-
4. **High cohesion through vertical slices** — everything for a behavior lives together.
|
|
22
|
-
5. **Full-stack type safety** — C# → proxy generation → TypeScript, no manual sync.
|
|
23
|
-
6. **Specialization over reuse** — dedicated read models per use case, never one shared model.
|
|
24
|
-
7. **Consistency is king** — match the established pattern over local cleverness.
|
|
25
|
-
8. American English spelling everywhere (behavior, color, initialize, modeling, dialog, …).
|
|
26
|
-
|
|
27
|
-
## Non-negotiable structural rules
|
|
28
|
-
|
|
29
|
-
- **ONE `.cs` file per slice** holds ALL backend artifacts: `[Command]` records with `Handle()`,
|
|
30
|
-
validators, constraints, `[EventType]` records, `[ReadModel]` records with static query methods,
|
|
31
|
-
projections/reducers, reactors, and slice-specific concepts.
|
|
32
|
-
- **Never split by artifact type** — no `Commands/`, `Handlers/`, `Events/` folders.
|
|
33
|
-
- **Commands define `Handle()` directly on the record** — never a separate handler class.
|
|
34
|
-
- **`[EventType]` takes NO arguments** — the type name is the identifier.
|
|
35
|
-
- **Drop the `.Features.` segment from namespaces** — `MyApp.Authors.Registration`, not
|
|
36
|
-
`MyApp.Features.Authors.Registration`.
|
|
37
|
-
- Complete one slice end-to-end before starting the next.
|
|
38
|
-
- **File header:** match the project. The shipped starter's example slices use **no** header — follow
|
|
39
|
-
that. (Cratis's own framework repos use a copyright header; only add one if the project's existing
|
|
40
|
-
`.cs` files already carry it.)
|
|
41
|
-
|
|
42
|
-
## Folder convention
|
|
43
|
-
|
|
44
|
-
Slices live in a **module → feature → slice** hierarchy. **Discover the actual top-level folder from an
|
|
45
|
-
existing slice in the project** (the shipped starter uses `SomeModule/SomeFeature/...`; some projects
|
|
46
|
-
use a `Features/` root) — match what's already there. Namespace mirrors the folders and drops any
|
|
47
|
-
`.Features.` segment: `<Root>.<Module>.<Feature>.<Slice>` (the starter's `<Root>` is `CratisApp`).
|
|
48
|
-
|
|
49
|
-
```
|
|
50
|
-
<Module>/<Feature>/
|
|
51
|
-
├── <Feature>.tsx ← composition page (layout, buttons, dialogs, tables)
|
|
52
|
-
├── index.ts ← barrel: export * from './<Feature>'
|
|
53
|
-
├── <Concept>.cs ← shared concepts for this feature (ConceptAs<T>)
|
|
54
|
-
└── <Slice>/
|
|
55
|
-
├── <Slice>.cs ← ALL backend artifacts in ONE file
|
|
56
|
-
├── <Proxy>.ts ← GENERATED by dotnet build — never hand-edit (marked // @generated)
|
|
57
|
-
├── <Component>.tsx ← React component(s) using the co-located generated proxy
|
|
58
|
-
├── index.ts ← barrel export
|
|
59
|
-
└── when_<behavior>/ ← integration specs (state-change slices)
|
|
60
|
-
├── and_<scenario>.cs
|
|
61
|
-
└── ...
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
**Proxies are generated next to the source.** The starter `.csproj` sets
|
|
65
|
-
`CratisProxiesUseSourceFileAsOutputFile=true`, so `dotnet build` writes each TypeScript proxy beside its
|
|
66
|
-
`.cs` file (`Register.ts` next to `Registration.cs`). Frontend imports the proxy from the **same folder**
|
|
67
|
-
(`import { Register } from './Register'`). Never edit a `// @generated` file. (Some projects instead
|
|
68
|
-
generate into a separate `api/` folder — follow whatever the existing slices do.)
|
|
69
|
-
|
|
70
|
-
## Concepts — `ConceptAs<T>` (never raw primitives in domain models)
|
|
71
|
-
|
|
72
|
-
```csharp
|
|
73
|
-
public record AuthorId(Guid Value) : ConceptAs<Guid>(Value)
|
|
74
|
-
{
|
|
75
|
-
public static readonly AuthorId NotSet = new(Guid.Empty);
|
|
76
|
-
public static implicit operator Guid(AuthorId id) => id.Value;
|
|
77
|
-
public static implicit operator AuthorId(Guid value) => new(value);
|
|
78
|
-
public static implicit operator EventSourceId(AuthorId id) => new(id.Value.ToString());
|
|
79
|
-
public static AuthorId New() => new(Guid.NewGuid());
|
|
80
|
-
}
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
- Positional `record` inheriting `ConceptAs<T>` (value equality + immutability for free).
|
|
84
|
-
- Add implicit conversion **to** `T`. Add a `static readonly NotSet` sentinel instead of `null`.
|
|
85
|
-
- `Guid` identity concepts: add `static New()` and an implicit conversion to `EventSourceId`.
|
|
86
|
-
- One concept per file, in the feature folder it belongs to (shared between features → `Features/` root).
|
|
87
|
-
|
|
88
|
-
## Events — `[EventType]`
|
|
89
|
-
|
|
90
|
-
```csharp
|
|
91
|
-
[EventType]
|
|
92
|
-
public record AuthorRegistered(AuthorName Name);
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
- **No attribute arguments** — never `[EventType("some-guid")]`.
|
|
96
|
-
- Past-tense names: `AuthorRegistered`, `BookReserved`, `AddressChanged`.
|
|
97
|
-
- Never nullable properties; one purpose per event.
|
|
98
|
-
|
|
99
|
-
## Commands — `[Command]` with `Handle()` on the record
|
|
100
|
-
|
|
101
|
-
```csharp
|
|
102
|
-
[Command]
|
|
103
|
-
public record RegisterAuthor(AuthorName Name)
|
|
104
|
-
{
|
|
105
|
-
public AuthorRegistered Handle() => new(Name);
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
- `[Command]` from `Cratis.Arc.Commands.ModelBound`; required for discovery.
|
|
110
|
-
- `Handle()` returns: single event, `(eventSourceId, event)` tuple (Arc returns the id as
|
|
111
|
-
`CommandResult<T>.response`), `IEnumerable<object>` for multiple events, `Result<TSuccess,TError>`,
|
|
112
|
-
or `void`.
|
|
113
|
-
- Imperative name — `RegisterAuthor`, not `RegisterAuthorCommand`.
|
|
114
|
-
- Event source resolved from a `[Key]` parameter, an `EventSourceId`-convertible concept, or
|
|
115
|
-
`ICanProvideEventSourceId`.
|
|
116
|
-
- Inject services / constraints / read models as `Handle()` parameters — DI provides them. A read-model
|
|
117
|
-
parameter gives you current Chronicle-derived state for **DCB** business rules.
|
|
118
|
-
|
|
119
|
-
### Validation
|
|
120
|
-
```csharp
|
|
121
|
-
public class RegisterAuthorValidator : CommandValidator<RegisterAuthor>
|
|
122
|
-
{
|
|
123
|
-
public RegisterAuthorValidator() =>
|
|
124
|
-
RuleFor(c => c.Name).NotEmpty().WithMessage("Name is required").MaximumLength(100);
|
|
125
|
-
}
|
|
126
|
-
```
|
|
127
|
-
- Extend `CommandValidator<T>` (not `AbstractValidator<T>`) — auto-discovered, runs client-side and
|
|
128
|
-
via the auto-generated `/validate` endpoint.
|
|
129
|
-
|
|
130
|
-
## Read Models — `[ReadModel]` + static query methods
|
|
131
|
-
|
|
132
|
-
```csharp
|
|
133
|
-
[ReadModel]
|
|
134
|
-
public record AuthorListItem(AuthorId Id, AuthorName Name, int BookCount)
|
|
135
|
-
{
|
|
136
|
-
public static async Task<IEnumerable<AuthorListItem>> AllAuthors(
|
|
137
|
-
IMongoCollection<AuthorListItem> collection)
|
|
138
|
-
=> await collection.Find(_ => true).ToListAsync();
|
|
139
|
-
|
|
140
|
-
public static ISubject<IEnumerable<AuthorListItem>> ObserveAllAuthors(
|
|
141
|
-
IMongoCollection<AuthorListItem> collection)
|
|
142
|
-
=> collection.Observe();
|
|
143
|
-
}
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
- `[ReadModel]` required. Query methods are **`public static`** on the record — the method name becomes
|
|
147
|
-
the TypeScript query proxy class name. No controller, no `IReadModels` injection.
|
|
148
|
-
- Observable (real-time) queries return `ISubject<T>` directly — never `Task<ISubject<T>>`.
|
|
149
|
-
- Prefer model-bound projection attributes; AutoMap is **on by default** (`.From<EventType>()` directly).
|
|
150
|
-
- **Projections join events, never read models.** One read model per use case.
|
|
151
|
-
|
|
152
|
-
### Projection (declarative mapping)
|
|
153
|
-
```csharp
|
|
154
|
-
public class AuthorListItemProjection : IProjectionFor<AuthorListItem>
|
|
155
|
-
{
|
|
156
|
-
public void Define(IProjectionBuilderFor<AuthorListItem> builder) => builder
|
|
157
|
-
.From<AuthorRegistered>(from => from.Set(m => m.Name).To(e => e.Name))
|
|
158
|
-
.From<BookAdded>(from => from.Add(m => m.BookCount).With(_ => 1));
|
|
159
|
-
}
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
### Model-bound shorthand
|
|
163
|
-
```csharp
|
|
164
|
-
[ReadModel]
|
|
165
|
-
public record AuthorInfo(
|
|
166
|
-
[Key] Guid Id,
|
|
167
|
-
[FromEvent<AuthorRegistered>] string Name);
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### Reducer (running aggregates — balances, counts, sums)
|
|
171
|
-
```csharp
|
|
172
|
-
public class BalanceReducer : IReducerFor<Balance>
|
|
173
|
-
{
|
|
174
|
-
public Balance Deposited(FundsDeposited @event, Balance? current, EventContext context)
|
|
175
|
-
=> (current ?? new(0m)) with { Amount = (current?.Amount ?? 0m) + @event.Amount };
|
|
176
|
-
}
|
|
177
|
-
```
|
|
178
|
-
Return the complete new state; never mutate `current` (`null` on first event). Projections/reducers
|
|
179
|
-
are discovered automatically — no registration.
|
|
180
|
-
|
|
181
|
-
## Reactors — `IReactor` (automation + translation slices)
|
|
182
|
-
|
|
183
|
-
```csharp
|
|
184
|
-
public class StockKeeping(IStockKeeper stock, ICommandPipeline commands) : IReactor
|
|
185
|
-
{
|
|
186
|
-
public async Task BookReserved(BookReserved @event, EventContext context) =>
|
|
187
|
-
await commands.Execute(new DecreaseStock(@event.Isbn, await stock.GetStock(@event.Isbn)));
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
- `IReactor` is a **marker interface** — dispatch is by the **first parameter type** of each public
|
|
192
|
-
method. Method name is for readability only. Signature: `Task Method(TEvent @event, EventContext context)`.
|
|
193
|
-
- Reactors *do things* (side effects); they never build read-model state.
|
|
194
|
-
- To write new events, inject `ICommandPipeline` and `Execute` a command — **never** use `IEventLog`
|
|
195
|
-
directly from a reactor.
|
|
196
|
-
- **Design for idempotency** — a reactor may run more than once (replay/recovery). Use event data
|
|
197
|
-
directly; never read the read model back inside a reactor. Keep reactors stateless.
|
|
198
|
-
- **Translation** = a reactor that adapts an event by triggering a command in its own slice.
|
|
199
|
-
|
|
200
|
-
## Specs — BDD with Cratis.Specifications
|
|
201
|
-
|
|
202
|
-
```csharp
|
|
203
|
-
public class when_registering : Specification
|
|
204
|
-
{
|
|
205
|
-
RegisterAuthor _command;
|
|
206
|
-
AuthorRegistered _event;
|
|
207
|
-
|
|
208
|
-
void Establish() => _command = new(new AuthorName("John"));
|
|
209
|
-
void Because() => _event = _command.Handle();
|
|
210
|
-
|
|
211
|
-
[Fact] void should_carry_the_name() => _event.Name.Value.ShouldEqual("John");
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
- Inherit `Specification`. `Establish()` = setup, `Because()` = the **one** action, `[Fact] should_*()`
|
|
216
|
-
= one assertion each (no blank lines between facts). Fields `private`/`protected`, `_camelCase`.
|
|
217
|
-
- Shared setup → `given/` contexts (`all_dependencies` → `a_<sut>`). Mock with NSubstitute.
|
|
218
|
-
- Folder/file names read as English: `for_<Type>/when_<behavior>/and_<condition>.cs`.
|
|
219
|
-
- Chronicle integration specs live in the slice's `when_<behavior>/` folder and run against a real
|
|
220
|
-
event store using `Given<context>` + `ChronicleOutOfProcessFixture`; assert with
|
|
221
|
-
`Context.ShouldHaveAppendedEvent<T>(...)`.
|
|
222
|
-
|
|
223
|
-
## React (frontend) — only relevant once `dotnet build` has generated proxies
|
|
224
|
-
|
|
225
|
-
- Import the generated command/query proxy from the slice's `../api/...` path.
|
|
226
|
-
- Commands: `const [command] = MyCommand.use();` → set props → `await command.execute()` →
|
|
227
|
-
check `result.isSuccess` / `result.response`.
|
|
228
|
-
- Queries: `MyQuery.use()` (snapshot) or `MyQuery.useWithPaging(pageSize)`.
|
|
229
|
-
- Dialogs: `CommandDialog` from `@cratis/components/CommandDialog`, `Dialog` from
|
|
230
|
-
`@cratis/components/Dialogs`. **Never** import `Dialog` from `primereact/dialog`.
|
|
231
|
-
- PrimeReact CSS variables for colors (no hard-coded hex). No `any` — use `unknown` + type guards.
|
|
232
|
-
Full descriptive names (`event`, not `e`; `index`, not `idx`).
|
|
233
|
-
|
|
234
|
-
## Build / test / sequencing
|
|
235
|
-
|
|
236
|
-
- **`dotnet build`** compiles the backend AND regenerates the TypeScript proxies. The frontend cannot
|
|
237
|
-
reference a slice's proxy until the backend compiles — so the order is strict:
|
|
238
|
-
**Backend → `dotnet build` → Frontend.** Backend and frontend of the same slice never run in parallel.
|
|
239
|
-
- Zero warnings, zero errors — warnings are treated as errors.
|
|
240
|
-
- `dotnet test` (filter to the slice while iterating:
|
|
241
|
-
`dotnet test --filter "FullyQualifiedName~<SliceName>"`).
|
|
242
|
-
- Slice-by-slice order: Backend → Specs → `dotnet build` → Frontend → Composition page → Routes.
|
|
243
|
-
|
|
244
|
-
## Final verification against `slice.json` (the JSON is the source of truth)
|
|
245
|
-
|
|
246
|
-
- Every `commands[]` field → a Command record property. No invented fields, none missing.
|
|
247
|
-
- Every event in `events[]` → an `[EventType]` record; names match exactly; no invented fields.
|
|
248
|
-
- Every read model field → a `[ReadModel]` property fed by a projection/reducer mapping.
|
|
249
|
-
- Every specification / GWT scenario in the slice → an executable spec.
|
|
250
|
-
- No business rule appears in `Handle()` that is not in the slice `description`/`comments`.
|
|
251
|
-
- If a field is not in `slice.json`, it is not in the code.
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: build-automation
|
|
3
|
-
description: >
|
|
4
|
-
Implement automation and translation slices the Cratis way — an IReactor that observes events and
|
|
5
|
-
produces side effects, triggering further writes via ICommandPipeline. Use when: (1) implementing a
|
|
6
|
-
new automation / reactor in a Cratis project, (2) a slice.json has a non-empty processors[] section
|
|
7
|
-
or sliceType === "TRANSLATION", (3) the user provides an Event Modeling artifact or description of an
|
|
8
|
-
event-driven reaction and asks to implement it, (4) the user says "implement", "create", "add" an
|
|
9
|
-
automation, reactor, translation, or event-to-command flow in a Cratis Arc / Chronicle project.
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# Cratis — Automation / Translation Slice
|
|
13
|
-
|
|
14
|
-
An automation *reacts* to events and *does things* (side effects). A translation adapts an event by
|
|
15
|
-
triggering a command in its own slice. Both are implemented with an **`IReactor`**.
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
event → IReactor method (dispatch by first param type) → side effect / ICommandPipeline.Execute(command)
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
> **Read first:** [../_shared/cratis-conventions.md](../_shared/cratis-conventions.md). Everything
|
|
22
|
-
> below assumes those rules.
|
|
23
|
-
|
|
24
|
-
## Step 0 — Discover conventions
|
|
25
|
-
|
|
26
|
-
Read `.build-kit/CLAUDE.md` and one existing reactor. Confirm the namespace root, how reactors are placed within a
|
|
27
|
-
slice, which services are available for side effects, and how existing translations call
|
|
28
|
-
`ICommandPipeline`. Resolve slice `comments` when done (see the state-change skill's Step 0).
|
|
29
|
-
|
|
30
|
-
## Step 1 — Understand the input (`slice.json` is the source of truth)
|
|
31
|
-
|
|
32
|
-
| Element | What to extract |
|
|
33
|
-
|---|---|
|
|
34
|
-
| **Trigger event(s)** | Which event(s) the reactor observes (the method's first parameter type) |
|
|
35
|
-
| **Reaction** | The side effect — notify, call a service, or trigger a command |
|
|
36
|
-
| **Target command** | For translation: which command to `Execute`, and how its fields map from the event |
|
|
37
|
-
| **Idempotency** | The reaction must be safe to run more than once (replay/recovery) |
|
|
38
|
-
| **Specifications** | Each scenario → an executable spec |
|
|
39
|
-
| **Storylines** (optional) | `storylines[]` — ordered walkthrough "beats"; an event-triggering-this-reactor transition is a supplementary spec source, see Step 4a |
|
|
40
|
-
|
|
41
|
-
If a command field has no source in the trigger event or an injected service, do not invent it.
|
|
42
|
-
|
|
43
|
-
## Step 2 — Write the reactor in the slice `.cs` file
|
|
44
|
-
|
|
45
|
-
`<Module>/<Feature>/<Slice>/<Slice>.cs` — namespace `<Root>.<Module>.<Feature>.<Slice>` (mirror the folders; drop any `.Features.`).
|
|
46
|
-
|
|
47
|
-
### Automation — side effect
|
|
48
|
-
```csharp
|
|
49
|
-
public class ProjectRegisteredNotifier(INotificationService notifications) : IReactor
|
|
50
|
-
{
|
|
51
|
-
public async Task ProjectRegistered(ProjectRegistered @event, EventContext context) =>
|
|
52
|
-
await notifications.Notify($"Project '{@event.Name}' was registered.");
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### Translation — event → command via ICommandPipeline
|
|
57
|
-
```csharp
|
|
58
|
-
public class StockKeeping(IStockKeeper stock, ICommandPipeline commands) : IReactor
|
|
59
|
-
{
|
|
60
|
-
public async Task BookReserved(BookReserved @event, EventContext context) =>
|
|
61
|
-
await commands.Execute(new DecreaseStock(@event.Isbn, await stock.GetStock(@event.Isbn)));
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
**Critical rules:**
|
|
66
|
-
- `IReactor` is a **marker interface** — dispatch is by the **first parameter type**. Method name is
|
|
67
|
-
for readability only. Signature: `Task Method(TEvent @event, EventContext context)` (`EventContext`
|
|
68
|
-
optional).
|
|
69
|
-
- To write new events, inject `ICommandPipeline` and `Execute` a command — **never** use `IEventLog`
|
|
70
|
-
directly from a reactor.
|
|
71
|
-
- **Design for idempotency** — a reactor may run more than once. Use event data directly; never query
|
|
72
|
-
the read model back inside a reactor. Keep the reactor stateless (constructor-injected deps only).
|
|
73
|
-
- One focused concern per reactor class; multiple handler methods are fine if they serve that concern.
|
|
74
|
-
|
|
75
|
-
If the translation's target command doesn't exist yet, implement it first with the
|
|
76
|
-
**build-state-change** skill, then reference it here.
|
|
77
|
-
|
|
78
|
-
## Step 3 — Build
|
|
79
|
-
|
|
80
|
-
`dotnet build` from the project root — zero warnings / errors.
|
|
81
|
-
|
|
82
|
-
## Step 4 — Specs
|
|
83
|
-
|
|
84
|
-
Write specs proving the reactor reacts correctly — for translations, assert the expected command was
|
|
85
|
-
executed (mock `ICommandPipeline` with NSubstitute and verify `Execute` was called with the mapped
|
|
86
|
-
command); for automations, assert the side-effect service was invoked. Cover idempotency where the
|
|
87
|
-
slice calls for it. One spec per scenario. Run `dotnet test --filter "FullyQualifiedName~<SliceName>"`.
|
|
88
|
-
See [references/patterns.md](references/patterns.md).
|
|
89
|
-
|
|
90
|
-
## Step 4a — Storyline-derived specs (optional)
|
|
91
|
-
|
|
92
|
-
If `storylines[]` is non-empty — a storyline embedded in this slice's slice.json already belongs
|
|
93
|
-
entirely to this slice, no need to match beats against `events[]`/`commands[]` by id/title — look
|
|
94
|
-
for an `EVENT → (COMMAND | EVENT)` transition: a trigger event beat, followed by the command it
|
|
95
|
-
triggers (translation) or a further event/side-effect signal (automation).
|
|
96
|
-
|
|
97
|
-
- **Establish** — append the trigger event (and any preceding events in the storyline needed to
|
|
98
|
-
reach it).
|
|
99
|
-
- **Because** — invoke/replay the reactor.
|
|
100
|
-
- **should_\*** — for a translation, assert `ICommandPipeline.Execute` was called with the mapped
|
|
101
|
-
command from the beat's `fields`; for a plain automation, assert the mocked side-effect service
|
|
102
|
-
was invoked.
|
|
103
|
-
|
|
104
|
-
If the storyline continues past the triggered command into that command's own event, or into a
|
|
105
|
-
read model, those halves belong to **build-state-change** / **build-state-view** respectively — a
|
|
106
|
-
reactor spec only covers the event-in → reaction-out step.
|
|
107
|
-
|
|
108
|
-
Skip silently when the beat sequence can't be isolated (e.g. no traceable trigger event).
|
|
109
|
-
|
|
110
|
-
## Final verification — does the implementation match `slice.json`?
|
|
111
|
-
|
|
112
|
-
- [ ] The reactor observes exactly the trigger event(s) named in the slice.
|
|
113
|
-
- [ ] For translation: the executed command and its field mapping match the slice; the command exists.
|
|
114
|
-
- [ ] No new events written via `IEventLog` — only via `ICommandPipeline`.
|
|
115
|
-
- [ ] The reaction is idempotent; the reactor is stateless.
|
|
116
|
-
- [ ] Every scenario → an executable spec; `dotnet build` clean; specs pass.
|
|
117
|
-
- [ ] A `storylines[]` transition triggering this reactor has a spec, or was deliberately skipped
|
|
118
|
-
(not silently ignored).
|
|
119
|
-
|
|
120
|
-
## References
|
|
121
|
-
- [references/patterns.md](references/patterns.md) — full reactor/translation code and reactor specs.
|
|
122
|
-
- [../_shared/cratis-conventions.md](../_shared/cratis-conventions.md) — the Cratis conventions.
|
package/stacks/cratis-csharp/templates/.claude/skills/build-automation/references/patterns.md
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
# Automation / Translation Patterns — Cratis Arc + Chronicle
|
|
2
|
-
|
|
3
|
-
Complete code for automation and translation slices. The reactor lives in the slice's single
|
|
4
|
-
`<Module>/<Feature>/<Slice>/<Slice>.cs` (the shipped starter uses `SomeModule/SomeFeature/...` — match
|
|
5
|
-
whatever the existing slices use). The example slices use no file header (match the project); namespace
|
|
6
|
-
mirrors the folders and drops any `.Features.`. Examples below use short illustrative names; mirror your
|
|
7
|
-
project's actual module/feature layout instead.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## 1. Automation — react with a side effect
|
|
12
|
-
|
|
13
|
-
```csharp
|
|
14
|
-
// Features/Projects/RegistrationNotifications/RegistrationNotifications.cs
|
|
15
|
-
namespace MyApp.Projects.RegistrationNotifications;
|
|
16
|
-
|
|
17
|
-
using Cratis.Chronicle.Events;
|
|
18
|
-
using Cratis.Chronicle.Reactors;
|
|
19
|
-
|
|
20
|
-
public class ProjectRegisteredNotifier(INotificationService notifications) : IReactor
|
|
21
|
-
{
|
|
22
|
-
/// <summary>Sends a notification when a project is registered.</summary>
|
|
23
|
-
public async Task ProjectRegistered(ProjectRegistered @event, EventContext context) =>
|
|
24
|
-
await notifications.Notify($"Project '{@event.Name}' was registered.");
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
---
|
|
29
|
-
|
|
30
|
-
## 2. Translation — event → command via ICommandPipeline
|
|
31
|
-
|
|
32
|
-
```csharp
|
|
33
|
-
// Features/Inventory/StockKeeping/StockKeeping.cs
|
|
34
|
-
namespace MyApp.Inventory.StockKeeping;
|
|
35
|
-
|
|
36
|
-
using Cratis.Arc.Commands;
|
|
37
|
-
using Cratis.Chronicle.Events;
|
|
38
|
-
using Cratis.Chronicle.Reactors;
|
|
39
|
-
|
|
40
|
-
public class StockKeeping(IStockKeeper stock, ICommandPipeline commands) : IReactor
|
|
41
|
-
{
|
|
42
|
-
/// <summary>Decreases stock when a book is reserved.</summary>
|
|
43
|
-
public async Task BookReserved(BookReserved @event, EventContext context) =>
|
|
44
|
-
await commands.Execute(new DecreaseStock(@event.Isbn, await stock.GetStock(@event.Isbn)));
|
|
45
|
-
}
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
The `DecreaseStock` command lives in its own slice (build it with the build-state-change skill first if
|
|
49
|
-
it doesn't exist). The reactor only triggers it.
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
## 3. Multiple handlers in one reactor (same concern)
|
|
54
|
-
|
|
55
|
-
```csharp
|
|
56
|
-
public class OrderFulfillment(ICommandPipeline commands) : IReactor
|
|
57
|
-
{
|
|
58
|
-
public Task OrderPlaced(OrderPlaced @event, EventContext context) =>
|
|
59
|
-
commands.Execute(new ReserveInventory(@event.OrderId, @event.Items));
|
|
60
|
-
|
|
61
|
-
public Task PaymentCaptured(PaymentCaptured @event, EventContext context) =>
|
|
62
|
-
commands.Execute(new ShipOrder(@event.OrderId));
|
|
63
|
-
}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
---
|
|
67
|
-
|
|
68
|
-
## 4. Idempotency
|
|
69
|
-
|
|
70
|
-
A reactor may be invoked more than once for the same event (replay, recovery, redelivery). Use only the
|
|
71
|
-
event's own data, keep the reactor stateless, and make the triggered command/side effect safe to repeat
|
|
72
|
-
(e.g. the target command is itself idempotent, or carries a key that de-duplicates). Never read the read
|
|
73
|
-
model back inside the reactor to "check if already done".
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
## 5. Reactor spec (translation — assert the command was executed)
|
|
78
|
-
|
|
79
|
-
```csharp
|
|
80
|
-
namespace MyApp.Inventory.StockKeeping.for_StockKeeping.when_a_book_is_reserved;
|
|
81
|
-
|
|
82
|
-
public class and_stock_is_available : Specification
|
|
83
|
-
{
|
|
84
|
-
IStockKeeper _stock;
|
|
85
|
-
ICommandPipeline _commands;
|
|
86
|
-
StockKeeping _reactor;
|
|
87
|
-
|
|
88
|
-
void Establish()
|
|
89
|
-
{
|
|
90
|
-
_stock = Substitute.For<IStockKeeper>();
|
|
91
|
-
_stock.GetStock("123").Returns(Task.FromResult(5));
|
|
92
|
-
_commands = Substitute.For<ICommandPipeline>();
|
|
93
|
-
_reactor = new StockKeeping(_stock, _commands);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async Task Because() => await _reactor.BookReserved(new BookReserved("123"), EventContext.Empty);
|
|
97
|
-
|
|
98
|
-
[Fact] void should_decrease_stock() =>
|
|
99
|
-
_commands.Received(1).Execute(Arg.Is<DecreaseStock>(c => c.Isbn == "123" && c.Quantity == 5));
|
|
100
|
-
}
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
For automations, mock the side-effect service (e.g. `INotificationService`) and assert it was called.
|
|
104
|
-
Match the project's existing reactor-spec harness (`IReactorInvoker` / `ReactorHandler`) for full
|
|
105
|
-
event-store-driven specs — discover it from an existing reactor's specs.
|
|
106
|
-
|
|
107
|
-
---
|
|
108
|
-
|
|
109
|
-
## Checklist
|
|
110
|
-
|
|
111
|
-
- One `.cs` file, namespace without `.Features.` (no file header unless the project uses one).
|
|
112
|
-
- `IReactor` marker; dispatch by **first parameter type**; `Task Method(TEvent, EventContext)`.
|
|
113
|
-
- New writes only via `ICommandPipeline.Execute(...)` — never `IEventLog` from a reactor.
|
|
114
|
-
- Stateless and idempotent; constructor-injected dependencies only.
|
|
115
|
-
- One spec per scenario; `dotnet build` clean; specs pass.
|