@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.
Files changed (52) hide show
  1. package/README.md +10 -3
  2. package/cli.js +0 -7
  3. package/package.json +2 -2
  4. package/stacks/blank/templates/.claude/skills/build-automation/SKILL.md +1 -1
  5. package/stacks/blank/templates/.claude/skills/build-state-change/SKILL.md +1 -1
  6. package/stacks/blank/templates/.claude/skills/build-state-view/SKILL.md +1 -1
  7. package/stacks/blank/templates/root/README.md +1 -1
  8. package/stacks/modeling-kit/templates/kit/README.md +1 -1
  9. package/stacks/react/templates/.claude/skills/build-automation/SKILL.md +1 -1
  10. package/stacks/react/templates/.claude/skills/build-state-change/SKILL.md +1 -1
  11. package/stacks/react/templates/.claude/skills/build-state-view/SKILL.md +1 -1
  12. package/stacks/cratis-csharp/templates/.claude/skills/_shared/cratis-conventions.md +0 -251
  13. package/stacks/cratis-csharp/templates/.claude/skills/build-automation/SKILL.md +0 -122
  14. package/stacks/cratis-csharp/templates/.claude/skills/build-automation/references/patterns.md +0 -115
  15. package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/SKILL.md +0 -191
  16. package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/references/patterns.md +0 -234
  17. package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/SKILL.md +0 -149
  18. package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/references/patterns.md +0 -166
  19. package/stacks/cratis-csharp/templates/build-kit/CLAUDE.md +0 -78
  20. package/stacks/cratis-csharp/templates/build-kit/lib/AGENT.md +0 -59
  21. package/stacks/cratis-csharp/templates/build-kit/lib/backend-prompt.md +0 -140
  22. package/stacks/cratis-csharp/templates/build-kit/lib/prompt.md +0 -126
  23. package/stacks/cratis-csharp/templates/root/.frontend/index.css +0 -29
  24. package/stacks/cratis-csharp/templates/root/.frontend/index.html +0 -17
  25. package/stacks/cratis-csharp/templates/root/.frontend/main.tsx +0 -18
  26. package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.json +0 -42
  27. package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.node.json +0 -11
  28. package/stacks/cratis-csharp/templates/root/.frontend/vite.config.ts +0 -56
  29. package/stacks/cratis-csharp/templates/root/App.tsx +0 -23
  30. package/stacks/cratis-csharp/templates/root/CratisApp.csproj +0 -25
  31. package/stacks/cratis-csharp/templates/root/CratisApp.sln +0 -18
  32. package/stacks/cratis-csharp/templates/root/GlobalUsings.cs +0 -3
  33. package/stacks/cratis-csharp/templates/root/Home.tsx +0 -102
  34. package/stacks/cratis-csharp/templates/root/Program.cs +0 -26
  35. package/stacks/cratis-csharp/templates/root/README.md +0 -192
  36. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/AllListings.ts +0 -47
  37. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.cs +0 -11
  38. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.ts +0 -12
  39. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/ListingDataTable.tsx +0 -17
  40. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/index.ts +0 -1
  41. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Register.ts +0 -51
  42. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/RegisterDialog.tsx +0 -18
  43. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Registration.cs +0 -27
  44. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/index.ts +0 -1
  45. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeFeature.tsx +0 -22
  46. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeName.cs +0 -3
  47. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/index.ts +0 -1
  48. package/stacks/cratis-csharp/templates/root/appsettings.Development.json +0 -9
  49. package/stacks/cratis-csharp/templates/root/appsettings.json +0 -26
  50. package/stacks/cratis-csharp/templates/root/docker-compose.yml +0 -23
  51. package/stacks/cratis-csharp/templates/root/package.json +0 -33
  52. package/stacks/cratis-csharp/templates/root/tsconfig.json +0 -3
@@ -1,191 +0,0 @@
1
- ---
2
- name: build-state-change
3
- description: >
4
- Implement Event Sourcing write slices the Cratis way — using Cratis Arc (CQRS) + Cratis Chronicle
5
- (event sourcing) in a .NET / C# project. A write slice is: Command → Handle() → Event(s), with
6
- optional validators, constraints, and DCB business rules. Use when: (1) implementing a new write
7
- slice / command in a Cratis project, (2) a slice.json has a non-empty commands[] / events[] section,
8
- (3) the user provides an Event Modeling artifact, specification, or natural-language description of a
9
- command and asks to implement it, (4) the user says "implement", "create", "add" a write slice,
10
- command, or state change in a Cratis Arc / Chronicle project.
11
- ---
12
-
13
- # Cratis — Write Slice (State Change)
14
-
15
- A write slice mutates state by recording events. In Cratis Arc the whole path lives in **one `.cs`
16
- file**:
17
-
18
- ```
19
- [Command] record + Handle() → validator/constraint → [EventType] record(s) → dotnet build → specs
20
- ```
21
-
22
- > **Read first:** [../_shared/cratis-conventions.md](../_shared/cratis-conventions.md) — the
23
- > non-negotiable Cratis rules. Everything below assumes them.
24
-
25
- ## Step 0 — Discover the target project's conventions
26
-
27
- Before writing code, read the project's `.build-kit/CLAUDE.md` and **at least one existing slice** (the starter
28
- ships one under `SomeModule/SomeFeature/`). Confirm:
29
-
30
- - The namespace root (read the `.csproj` `<RootNamespace>` and existing `.cs` files — never
31
- hard-code it; the namespace mirrors the folders and drops any `.Features.` segment).
32
- - How existing commands return results (single event / tuple / `Result<,>` / `void`).
33
- - How `ConceptAs<T>` identity types are declared and where they live.
34
- - Whether existing `.cs` files use a file header (the starter uses none).
35
-
36
- > **Comments & description:** each slice element carries a `comments: string[]` array and a
37
- > `description`. Use them as implementation hints. When done, resolve each used comment:
38
- > `POST <BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes/<nodeId>/comments/<commentId>/resolve`
39
- > (get IDs first via GET on the same path).
40
-
41
- ## Step 1 — Understand the input (`slice.json` is the source of truth)
42
-
43
- Extract, regardless of input format:
44
-
45
- | Element | What to extract |
46
- |---|---|
47
- | **Command** | Name (imperative), fields, which field is the event source / `[Key]` |
48
- | **Events** | Names (past tense), fields, which events this command appends |
49
- | **Business rules** | Preconditions, invariants, idempotency — from `description` / `comments` only |
50
- | **State needed for rules** | Which read model must be inspected (DCB) to evaluate a rule |
51
- | **Specifications** | Each GWT / scenario maps 1:1 to an executable spec |
52
- | **Storylines** (optional) | `storylines[]` — ordered walkthrough "beats"; a `COMMAND → EVENT` transition in one is a supplementary spec source, see Step 5a |
53
-
54
- **If a field is not in `slice.json`, it does not go in the code.** If requirements are unclear, ask
55
- the user before proceeding.
56
-
57
- ### Determine the trigger
58
- If unclear how the command is dispatched, ask:
59
- > - **UI / REST** — exposed automatically by Arc; add a React component + integration spec.
60
- > - **Automation only** — dispatched internally by a reactor (no UI). The command still exists; no `.tsx`.
61
-
62
- ## Step 2 — Create concept types (if needed)
63
-
64
- For any new identity / value, add one `ConceptAs<T>` per file in the feature folder. Use the canonical
65
- Guid-identity pattern (with `NotSet`, `New()`, and the `EventSourceId` conversion) — see the shared
66
- conventions doc and [references/patterns.md](references/patterns.md).
67
-
68
- ## Step 3 — Write the slice `.cs` file
69
-
70
- `<Module>/<Feature>/<Slice>/<Slice>.cs` — ALL backend artifacts in this one file; namespace
71
- `<Root>.<Module>.<Feature>.<Slice>` (no file header unless the project uses one).
72
-
73
- ### Events first
74
- ```csharp
75
- [EventType] // NEVER any arguments
76
- public record AuthorRegistered(AuthorName Name);
77
- ```
78
- Past tense, no nullable properties, one purpose each. If the context's events already exist elsewhere,
79
- reuse them — don't redefine.
80
-
81
- ### Command with `Handle()` on the record
82
- ```csharp
83
- [Command]
84
- public record RegisterAuthor(AuthorName Name)
85
- {
86
- public AuthorRegistered Handle() => new(Name); // Arc appends the returned event
87
- }
88
- ```
89
-
90
- Pick the return shape that matches the slice:
91
- - **single event** → `EventName Handle()`
92
- - **generate + return a new id** → `(NewId, EventName) Handle()` (Arc returns the id as
93
- `CommandResult<T>.response`)
94
- - **multiple events** → `IEnumerable<object> Handle() => [ new A(...), new B(...) ];`
95
- - **success/error** → `Result<TSuccess, TError> Handle()`
96
- - **side-effect only** → `void Handle()`
97
-
98
- Event source: a `[Key]` parameter, an `EventSourceId`-convertible concept, or `ICanProvideEventSourceId`.
99
-
100
- ### Business rules (DCB) — inject the read model
101
- When a rule depends on event-sourced state, take the read model as a `Handle()` parameter; Arc injects
102
- current state. Throw / return an error for violations:
103
- ```csharp
104
- [Command]
105
- public record RegisterAuthor(AuthorName Name)
106
- {
107
- public Result<AuthorRegistered, RegistrationError> Handle(AuthorByName existing) =>
108
- existing is not null
109
- ? RegistrationError.NameAlreadyTaken
110
- : new AuthorRegistered(Name);
111
- }
112
- ```
113
- Only encode rules that appear in the slice `description` / `comments`. See
114
- [references/patterns.md](references/patterns.md) for the full DCB and constraint patterns.
115
-
116
- ### Validation (optional but recommended)
117
- ```csharp
118
- public class RegisterAuthorValidator : CommandValidator<RegisterAuthor>
119
- {
120
- public RegisterAuthorValidator() =>
121
- RuleFor(c => c.Name).NotEmpty().WithMessage("Name is required").MaximumLength(100);
122
- }
123
- ```
124
- Extend `CommandValidator<T>` — auto-discovered, no registration. One `RuleFor` per validation in the slice.
125
-
126
- ## Step 4 — Build
127
-
128
- From the project root: `dotnet build`. Fix ALL warnings and errors before continuing (warnings = errors).
129
- This also regenerates the TypeScript proxies the frontend depends on.
130
-
131
- ## Step 5 — Write specs (one per scenario in `slice.json`)
132
-
133
- Put integration specs in `<Module>/<Feature>/<Slice>/when_<behavior>/and_<scenario>.cs`. Cover, from the
134
- slice's specifications:
135
- - **Happy path** — command succeeds, correct event appended.
136
- - **Each validation failure** — one spec per rule.
137
- - **Each DCB business-rule violation** — one spec per read-model condition in `Handle()`.
138
- - **Each constraint violation.**
139
-
140
- Use the Cratis BDD pattern (`Specification`, `Establish` / `Because` / `[Fact] should_*`). For
141
- Chronicle integration specs use `Given<context>` + `ChronicleOutOfProcessFixture` and assert with
142
- `Context.ShouldHaveAppendedEvent<T>(...)`. See [references/patterns.md](references/patterns.md).
143
-
144
- Run `dotnet test --filter "FullyQualifiedName~<SliceName>"`. Fix all failures.
145
-
146
- ## Step 5a — Storyline-derived specs (optional)
147
-
148
- If `storylines[]` is non-empty — a storyline embedded in this slice's slice.json already belongs
149
- entirely to this slice, no need to match beats against `commands[]` by id/title — look for a
150
- `COMMAND → EVENT` transition: a `type: "COMMAND"` beat, followed by its resulting event beat(s) —
151
- possibly followed by a READMODEL beat after that.
152
-
153
- - **Establish** — append every event from the storyline's start up to (not including) the command
154
- beat.
155
- - **Because** — `Handle()` the command, built from the beat's `fields`.
156
- - **should_\*** — assert the expected event(s) (the beat(s) immediately following the command).
157
-
158
- Only the `COMMAND → EVENT` half lives here — if the storyline continues `EVENT → READMODEL`, that
159
- half is a separate spec in **build-state-view** (its Step 4a). Don't chase a single spec across
160
- both a command dispatch and a read-model assertion unless the project already has an established
161
- pattern doing that — Chronicle integration specs assert appended events, not read-model state,
162
- and vice versa for projection specs.
163
-
164
- Place these alongside the `specifications[]`-derived specs but in their own
165
- `and_<storyline-title>.cs` file, so a reader can tell at a glance which specs are exhaustive
166
- coverage and which are one narrated walkthrough.
167
-
168
- Skip silently (no spec) when the transition isn't isolable — e.g. the command beat has no
169
- traceable preceding events.
170
-
171
- ## Step 6 — Frontend (only if the command is UI-triggered)
172
-
173
- After `dotnet build` generated the proxy (co-located next to the `.cs`), add
174
- `<Module>/<Feature>/<Slice>/<Component>.tsx` importing the proxy from `./`, using `CommandDialog` /
175
- inline form. Add a barrel `index.ts`. Register it in the feature's composition
176
- page. See the shared conventions doc's React section and [references/patterns.md](references/patterns.md).
177
-
178
- ## Final verification — does the implementation match `slice.json`?
179
-
180
- - [ ] Every `commands[]` field → a Command record property (no invented, none missing).
181
- - [ ] Every `events[]` entry → an `[EventType]` record; names match exactly; fields match.
182
- - [ ] Every specification / GWT scenario → an executable spec.
183
- - [ ] No business rule in `Handle()` that is absent from the slice `description` / `comments`.
184
- - [ ] A `storylines[]` `COMMAND → EVENT` transition for this command has a spec, or was
185
- deliberately skipped (not silently ignored).
186
- - [ ] `dotnet build` is clean (0 warnings / 0 errors); slice specs pass.
187
-
188
- ## References
189
- - [references/patterns.md](references/patterns.md) — full command/event/validator/constraint/DCB code,
190
- integration specs, and the React command UI patterns.
191
- - [../_shared/cratis-conventions.md](../_shared/cratis-conventions.md) — the Cratis conventions.
@@ -1,234 +0,0 @@
1
- # Write Slice Patterns — Cratis Arc + Chronicle
2
-
3
- Complete code for state-change (write) slices. All artifacts go in a single
4
- `<Module>/<Feature>/<Slice>/<Slice>.cs` file (the shipped starter uses `SomeModule/SomeFeature/...` —
5
- match whatever the existing slices use). The example slices use no file header (match the project).
6
- Namespace mirrors the folders and drops any `.Features.`. The examples below use short illustrative
7
- names (`MyApp.Authors.Registration`); mirror your project's actual module/feature layout instead.
8
-
9
- ---
10
-
11
- ## 1. Concept (one per file, in the feature folder)
12
-
13
- ```csharp
14
- // Features/Authors/AuthorId.cs
15
- namespace MyApp.Authors;
16
-
17
- public record AuthorId(Guid Value) : ConceptAs<Guid>(Value)
18
- {
19
- public static readonly AuthorId NotSet = new(Guid.Empty);
20
- public static implicit operator Guid(AuthorId id) => id.Value;
21
- public static implicit operator AuthorId(Guid value) => new(value);
22
- public static implicit operator EventSourceId(AuthorId id) => new(id.Value.ToString());
23
- public static AuthorId New() => new(Guid.NewGuid());
24
- }
25
-
26
- // Features/Authors/AuthorName.cs
27
- public record AuthorName(string Value) : ConceptAs<string>(Value)
28
- {
29
- public static readonly AuthorName NotSet = new(string.Empty);
30
- public static implicit operator string(AuthorName name) => name.Value;
31
- public static implicit operator AuthorName(string value) => new(value);
32
- }
33
- ```
34
-
35
- ---
36
-
37
- ## 2. Simplest write slice — single event
38
-
39
- ```csharp
40
- // Features/Authors/Registration/Registration.cs
41
- namespace MyApp.Authors.Registration;
42
-
43
- using Cratis.Arc.Commands.ModelBound;
44
- using Cratis.Chronicle.Events;
45
-
46
- [Command]
47
- public record RegisterAuthor([Key] AuthorId Id, AuthorName Name)
48
- {
49
- public AuthorRegistered Handle() => new(Name);
50
- }
51
-
52
- [EventType]
53
- public record AuthorRegistered(AuthorName Name);
54
-
55
- public class RegisterAuthorValidator : CommandValidator<RegisterAuthor>
56
- {
57
- public RegisterAuthorValidator() =>
58
- RuleFor(c => c.Name).NotEmpty().WithMessage("Name is required").MaximumLength(100);
59
- }
60
- ```
61
-
62
- The `[Key]` parameter is the event source id. With a concept that converts to `EventSourceId`, you may
63
- omit `[Key]` and let Arc resolve it.
64
-
65
- ---
66
-
67
- ## 3. Generate-and-return a new id
68
-
69
- ```csharp
70
- [Command]
71
- public record RegisterAuthor(AuthorName Name)
72
- {
73
- public (AuthorId, AuthorRegistered) Handle()
74
- {
75
- var id = AuthorId.New();
76
- return (id, new AuthorRegistered(Name)); // id flows back as CommandResult<AuthorId>.response
77
- }
78
- }
79
- ```
80
-
81
- ---
82
-
83
- ## 4. Multiple events
84
-
85
- ```csharp
86
- [Command]
87
- public record TransferFunds(AccountId FromId, AccountId ToId, decimal Amount)
88
- {
89
- public IEnumerable<object> Handle() =>
90
- [
91
- new FundsWithdrawn(FromId, Amount),
92
- new FundsDeposited(ToId, Amount),
93
- ];
94
- }
95
- ```
96
-
97
- ---
98
-
99
- ## 5. Injected dependency / constraint
100
-
101
- `Handle()` parameters are DI-resolved. Use for external services or constraints (e.g. uniqueness):
102
-
103
- ```csharp
104
- [Command]
105
- public record OpenDebitAccount([Key] AccountId Id, string Name, OwnerId OwnerId)
106
- {
107
- public async Task<DebitAccountOpened> Handle(IUniqueAccountConstraint constraint)
108
- {
109
- await constraint.Validate(Name); // throws on violation
110
- return new DebitAccountOpened(Name, OwnerId);
111
- }
112
- }
113
- ```
114
-
115
- ---
116
-
117
- ## 6. Business rule via DCB (inject a read model)
118
-
119
- When a rule depends on event-sourced state, accept the relevant read model as a `Handle()` parameter.
120
- Arc materializes current state from the event log and injects it. Return a `Result<,>` for a clean
121
- success/error contract:
122
-
123
- ```csharp
124
- [Command]
125
- public record ReserveBook([Key] BookId Id, MemberId Member)
126
- {
127
- public Result<BookReserved, ReservationError> Handle(BookAvailability availability)
128
- {
129
- if (availability is null || !availability.IsAvailable)
130
- return ReservationError.NotAvailable;
131
- return new BookReserved(Member);
132
- }
133
- }
134
-
135
- public enum ReservationError { NotAvailable }
136
- ```
137
-
138
- Only encode rules that appear in the slice `description` / `comments`.
139
-
140
- ---
141
-
142
- ## 7. Integration spec (Chronicle, full stack)
143
-
144
- `Features/Authors/Registration/when_registering/and_there_are_no_authors.cs`
145
-
146
- ```csharp
147
- using context = MyApp.Authors.Registration.when_registering.and_there_are_no_authors.context;
148
-
149
- namespace MyApp.Authors.Registration.when_registering;
150
-
151
- [Collection(ChronicleCollection.Name)]
152
- public class and_there_are_no_authors(context context) : Given<context>(context)
153
- {
154
- public class context(ChronicleOutOfProcessFixture fixture) : given.an_http_client(fixture)
155
- {
156
- public CommandResult<AuthorId>? Result;
157
-
158
- async Task Because() =>
159
- Result = await Client.ExecuteCommand<RegisterAuthor, AuthorId>(
160
- "/api/authors/register",
161
- new RegisterAuthor(AuthorId.New(), new AuthorName("John Doe")));
162
- }
163
-
164
- [Fact] void should_be_successful() => Context.Result!.IsSuccess.ShouldBeTrue();
165
- [Fact] void should_have_appended_one_event() =>
166
- Context.ShouldHaveTailSequenceNumber(EventSequenceNumber.First);
167
- [Fact] void should_append_author_registered_event() =>
168
- Context.ShouldHaveAppendedEvent<AuthorRegistered>(
169
- EventSequenceNumber.First, Context.Result!.Response,
170
- evt => evt.Name.Value.ShouldEqual("John Doe"));
171
- }
172
- ```
173
-
174
- ### Pure unit spec on `Handle()` (no I/O)
175
-
176
- ```csharp
177
- namespace MyApp.Authors.Registration.for_RegisterAuthor;
178
-
179
- public class when_registering : Specification
180
- {
181
- RegisterAuthor _command;
182
- AuthorRegistered _event;
183
-
184
- void Establish() => _command = new(AuthorId.New(), new AuthorName("John"));
185
- void Because() => _event = _command.Handle();
186
-
187
- [Fact] void should_carry_the_name() => _event.Name.Value.ShouldEqual("John");
188
- }
189
- ```
190
-
191
- ---
192
-
193
- ## 8. React command UI (after `dotnet build` generated the proxy)
194
-
195
- The proxy (`RegisterAuthor.ts`) is generated **next to** `Registration.cs` (the starter's `.csproj`
196
- sets `CratisProxiesUseSourceFileAsOutputFile=true`), so import it from the **same folder**. Add a barrel
197
- `index.ts` (`export * from './AddAuthor'`) like the shipped example slice.
198
-
199
- ```tsx
200
- // Authors/Registration/AddAuthor.tsx
201
- import { CommandDialog } from '@cratis/components/CommandDialog';
202
- import { InputTextField } from '@cratis/components/CommandForm';
203
- import { RegisterAuthor } from './RegisterAuthor'; // co-located generated proxy
204
-
205
- export const AddAuthor = () => (
206
- <CommandDialog command={RegisterAuthor} title="Add author" okLabel="Add" cancelLabel="Cancel">
207
- <InputTextField<RegisterAuthor> value={c => c.name} title="Name" />
208
- </CommandDialog>
209
- );
210
- ```
211
-
212
- Inline form alternative:
213
-
214
- ```tsx
215
- const [command] = RegisterAuthor.use();
216
- const submit = async () => {
217
- const result = await command.execute();
218
- if (result.isSuccess) onSuccess(result.response);
219
- };
220
- ```
221
-
222
- Never import `Dialog` from `primereact/dialog` — use the Cratis wrappers. PrimeReact CSS variables for
223
- colors. No `any`. Full descriptive variable names.
224
-
225
- ---
226
-
227
- ## Checklist
228
-
229
- - One `.cs` file, namespace without `.Features.` (no file header unless the project uses one).
230
- - `[Command]` with `Handle()` on the record — no handler class.
231
- - `[EventType]` with **no** arguments; past-tense names; no nullable fields.
232
- - `ConceptAs<T>` for every identity / value — no raw `Guid` / `string` in the domain.
233
- - `CommandValidator<T>` for validations; read-model parameter for DCB rules.
234
- - One spec per scenario in `slice.json`; `dotnet build` clean; specs pass.
@@ -1,149 +0,0 @@
1
- ---
2
- name: build-state-view
3
- description: >
4
- Implement read slices the Cratis way — a [ReadModel] record with static query methods, fed by a
5
- Chronicle projection or reducer, plus the React component that renders it. Use when: (1) implementing
6
- a new read slice / projection in a Cratis project, (2) a slice.json has a non-empty readModel /
7
- projections / queries section, (3) the user provides a read-slice Event Modeling artifact or
8
- specification and asks to implement it, (4) the user says "implement", "create", "add" a read slice,
9
- read model, projection, reducer, or query in a Cratis Arc / Chronicle project.
10
- ---
11
-
12
- # Cratis — Read Slice (State View)
13
-
14
- A read slice projects events into a queryable read model. In Cratis the path lives in **one `.cs`
15
- file**:
16
-
17
- ```
18
- [ReadModel] record + static queries → projection or reducer → dotnet build → React
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 read slice. Confirm the namespace root, how existing read models
27
- declare queries (snapshot vs observable), whether projections use model-bound attributes or
28
- `IProjectionFor<T>`, and the MongoDB collection wiring. Resolve slice `comments` when done (see the
29
- state-change skill's Step 0 for the resolve endpoint).
30
-
31
- ## Step 1 — Understand the input (`slice.json` is the source of truth)
32
-
33
- | Element | What to extract |
34
- |---|---|
35
- | **Read model** | Name, fields, the key field |
36
- | **Source events** | Which events feed each field (projections **join events, never read models**) |
37
- | **Queries** | The queries to expose; whether each is snapshot or real-time (observable) |
38
- | **Specifications** | Each scenario → an executable spec proving the projection result |
39
- | **Storylines** (optional) | `storylines[]` — ordered walkthrough "beats"; a read-model chain in one is a supplementary spec source, see Step 4a |
40
-
41
- If a field has no source event in `slice.json`, do not invent one. One read model per use case —
42
- never reuse a model across slices.
43
-
44
- ## Step 2 — Write the slice `.cs` file
45
-
46
- `<Module>/<Feature>/<Slice>/<Slice>.cs` — namespace `<Root>.<Module>.<Feature>.<Slice>` (mirror the folders; drop any `.Features.`).
47
-
48
- ### Read model + static query methods
49
- ```csharp
50
- [ReadModel]
51
- public record AuthorListItem(AuthorId Id, AuthorName Name, int BookCount)
52
- {
53
- public static async Task<IEnumerable<AuthorListItem>> AllAuthors(
54
- IMongoCollection<AuthorListItem> collection)
55
- => await collection.Find(_ => true).ToListAsync();
56
-
57
- public static async Task<AuthorListItem?> GetAuthor(
58
- AuthorId id, IMongoCollection<AuthorListItem> collection)
59
- => await collection.Find(a => a.Id == id).FirstOrDefaultAsync();
60
-
61
- // Real-time push — return ISubject<T> directly (never Task<ISubject<T>>)
62
- public static ISubject<IEnumerable<AuthorListItem>> ObserveAllAuthors(
63
- IMongoCollection<AuthorListItem> collection)
64
- => collection.Observe();
65
- }
66
- ```
67
- - `[ReadModel]` required. Query methods are **`public static`** on the record; the method name becomes
68
- the TS query proxy class name. No controller, no `IReadModels`.
69
- - Favor reactive (`ISubject<T>`) queries when the slice wants live updates.
70
-
71
- ### Choose projection vs reducer
72
- - **Projection** — shaped read models, field mapping, joins, children (most read slices).
73
- - **Reducer** — running aggregates (balances, counts, sums) where state accumulates across events.
74
-
75
- ### Projection (declarative; AutoMap on by default)
76
- ```csharp
77
- public class AuthorListItemProjection : IProjectionFor<AuthorListItem>
78
- {
79
- public void Define(IProjectionBuilderFor<AuthorListItem> builder) => builder
80
- .From<AuthorRegistered>(from => from.Set(m => m.Name).To(e => e.Name))
81
- .From<BookAdded>(from => from.Add(m => m.BookCount).With(_ => 1));
82
- }
83
- ```
84
- Or model-bound shorthand for simple cases:
85
- ```csharp
86
- [ReadModel]
87
- public record AuthorInfo([Key] Guid Id, [FromEvent<AuthorRegistered>] string Name);
88
- ```
89
-
90
- Projections/reducers are discovered automatically — no registration. See
91
- [references/patterns.md](references/patterns.md) for joins, children, composite keys, and reducers.
92
-
93
- ## Step 3 — Build
94
-
95
- `dotnet build` from the project root — zero warnings / errors. This generates the query proxy the
96
- frontend imports.
97
-
98
- ## Step 4 — Specs
99
-
100
- Write specs proving the projection/reducer produces the expected read model from a sequence of events —
101
- one spec per scenario in `slice.json`. Run `dotnet test --filter "FullyQualifiedName~<SliceName>"`.
102
- See [references/patterns.md](references/patterns.md).
103
-
104
- ## Step 4a — Storyline-derived specs (optional)
105
-
106
- If the slice's `storylines[]` array (in `slice.json`, alongside `specifications[]`) is non-empty —
107
- a storyline embedded in this slice's slice.json already belongs entirely to this slice, no need to
108
- match beats against `readmodels[]` by id/title — look for a **read-model chain**: two beats of
109
- `type: "READMODEL"`, with only `EVENT` beat(s) between them. That's a self-contained projection
110
- spec:
111
-
112
- - **Establish** — append every event from the storyline's start up through the intervening
113
- event(s), in order.
114
- - **Because** — run the projection.
115
- - **should_\*** — assert against the *later* beat's `fields`/`examples`/`expectEmptyList`.
116
-
117
- Put these in their own `for_<ReadModel>/when_<storyline-title>/` folder (not `when_<behavior>/`),
118
- so they're never confused with the exhaustive `specifications[]` suite — a storyline is a
119
- narrated walkthrough, not exhaustive coverage.
120
-
121
- If the storyline's transition into this read model is instead `COMMAND → EVENT → READMODEL`, only
122
- the `EVENT → READMODEL` half belongs here — the `COMMAND → EVENT` half is a separate spec in
123
- **build-state-change** (see its Step 5a). Don't try to assert read-model state from a dispatched
124
- command in one spec unless the project already has an established pattern for that.
125
-
126
- Skip a beat sequence entirely — no spec, no placeholder — if it can't be isolated (e.g. a SCREEN
127
- beat with no traceable event). A storyline is source material for tests, not a mandate to write
128
- one for every beat.
129
-
130
- ## Step 5 — Frontend
131
-
132
- Add `<Module>/<Feature>/<Slice>/<Component>.tsx` importing the co-located generated query proxy from
133
- `./` (`MyQuery.use()` snapshot, `MyQuery.useWithPaging(pageSize)` for paging, observable proxy for live
134
- data; or `DataTableForObservableQuery` for a table). Add a barrel `index.ts`. Register it in the
135
- feature's composition page; add routing in `App.tsx` if it's a new page.
136
-
137
- ## Final verification — does the implementation match `slice.json`?
138
-
139
- - [ ] Every read-model field → a record property fed by a projection/reducer mapping.
140
- - [ ] Every source event used exists; projections map from **events**, never read models.
141
- - [ ] Every query in the slice is exposed as a static method; observable where the slice wants live data.
142
- - [ ] Every scenario → an executable spec.
143
- - [ ] A `storylines[]` read-model chain for this read model has a spec, or was deliberately
144
- skipped (not silently ignored).
145
- - [ ] `dotnet build` clean; specs pass.
146
-
147
- ## References
148
- - [references/patterns.md](references/patterns.md) — full projection/reducer/query/spec code + React.
149
- - [../_shared/cratis-conventions.md](../_shared/cratis-conventions.md) — the Cratis conventions.