@eventmodelers/cli 1.0.44 → 1.0.46

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 (108) hide show
  1. package/README.md +4 -1
  2. package/cli.js +53 -1
  3. package/package.json +2 -2
  4. package/shared/build-kit/lib/checks/README.md +59 -0
  5. package/shared/build-kit/lib/ralph.js +110 -28
  6. package/shared/build-kit/lib/util/find-slice.cjs +59 -0
  7. package/shared/build-kit/ralph-claude.js +6 -2
  8. package/stacks/blank/templates/build-kit/lib/backend-prompt.md +106 -102
  9. package/stacks/blank/templates/build-kit/lib/prompt.md +102 -106
  10. package/stacks/kurrent/templates/.claude/skills/build-automation/SKILL.md +422 -0
  11. package/stacks/kurrent/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +19 -0
  12. package/stacks/kurrent/templates/.claude/skills/build-automation/references/idempotent-dispatch-patterns.md +65 -0
  13. package/stacks/kurrent/templates/.claude/skills/build-state-change/SKILL.md +418 -0
  14. package/stacks/kurrent/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +41 -0
  15. package/stacks/kurrent/templates/.claude/skills/build-state-change/references/integration-test-patterns.md +64 -0
  16. package/stacks/kurrent/templates/.claude/skills/build-state-view/SKILL.md +391 -0
  17. package/stacks/kurrent/templates/build-kit/CLAUDE.md +122 -0
  18. package/stacks/kurrent/templates/build-kit/lib/AGENT.md +73 -0
  19. package/stacks/kurrent/templates/build-kit/lib/backend-prompt.md +169 -0
  20. package/stacks/kurrent/templates/build-kit/lib/prompt.md +128 -0
  21. package/stacks/kurrent/templates/root/README.md +46 -0
  22. package/stacks/kurrent/templates/root/docker-compose.yml +45 -0
  23. package/stacks/kurrent/templates/root/mvnw +259 -0
  24. package/stacks/kurrent/templates/root/mvnw.cmd +149 -0
  25. package/stacks/kurrent/templates/root/pom.xml +152 -0
  26. package/stacks/kurrent/templates/root/src/main/java/com/example/quickstart/QuickstartApplication.java +12 -0
  27. package/stacks/kurrent/templates/root/src/main/java/com/example/quickstart/common/EventStore.java +91 -0
  28. package/stacks/kurrent/templates/root/src/main/java/com/example/quickstart/config/KurrentConfiguration.java +42 -0
  29. package/stacks/kurrent/templates/root/src/main/resources/application.properties +14 -0
  30. package/stacks/kurrent/templates/root/src/main/resources/static/index.html +11 -0
  31. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-core-rules/SKILL.md +4 -2
  32. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md +1 -1
  33. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +1 -1
  34. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/SKILL.md +2 -0
  35. package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +1 -1
  36. package/stacks/node/templates/build-kit/CLAUDE.md +22 -0
  37. package/stacks/node/templates/build-kit/lib/check-commit-scope.cjs +123 -0
  38. package/stacks/node/templates/build-kit/lib/checks/00-blocked-paths.cjs +28 -0
  39. package/stacks/node/templates/build-kit/lib/checks/10-slice-scope.cjs +29 -0
  40. package/stacks/node/templates/build-kit/lib/checks/20-append-only-migrations.cjs +20 -0
  41. package/stacks/node/templates/build-kit/lib/checks/30-test-file-present.cjs +44 -0
  42. package/stacks/node/templates/build-kit/lib/checks/40-no-invented-fields.cjs +92 -0
  43. package/stacks/node/templates/build-kit/lib/checks/50-spec-coverage.cjs +50 -0
  44. package/stacks/node/templates/build-kit/lib/checks/90-tsc-build.cjs +22 -0
  45. package/stacks/node/templates/root/.githooks/pre-commit +11 -0
  46. package/stacks/node/templates/root/README.md +67 -0
  47. package/stacks/node/templates/root/package.json +2 -1
  48. package/stacks/node/templates/root/setup-env.sh +7 -1
  49. package/stacks/opencqrs/templates/.claude/skills/build-automation/SKILL.md +434 -0
  50. package/stacks/opencqrs/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +19 -0
  51. package/stacks/opencqrs/templates/.claude/skills/build-automation/references/idempotent-dispatch-patterns.md +62 -0
  52. package/stacks/opencqrs/templates/.claude/skills/build-state-change/SKILL.md +413 -0
  53. package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +46 -0
  54. package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/rest-api-patterns.md +145 -0
  55. package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/test-fixture-patterns.md +85 -0
  56. package/stacks/opencqrs/templates/.claude/skills/build-state-view/SKILL.md +354 -0
  57. package/stacks/opencqrs/templates/build-kit/CLAUDE.md +101 -0
  58. package/stacks/opencqrs/templates/build-kit/lib/AGENT.md +58 -0
  59. package/stacks/opencqrs/templates/build-kit/lib/backend-prompt.md +169 -0
  60. package/stacks/opencqrs/templates/build-kit/lib/prompt.md +128 -0
  61. package/stacks/opencqrs/templates/root/README.md +42 -0
  62. package/stacks/opencqrs/templates/root/docker-compose.yml +37 -0
  63. package/stacks/opencqrs/templates/root/mvnw +259 -0
  64. package/stacks/opencqrs/templates/root/mvnw.cmd +149 -0
  65. package/stacks/opencqrs/templates/root/pom.xml +139 -0
  66. package/stacks/opencqrs/templates/root/src/main/java/com/example/quickstart/QuickstartApplication.java +12 -0
  67. package/stacks/opencqrs/templates/root/src/main/java/com/example/quickstart/config/CqrsConfiguration.java +74 -0
  68. package/stacks/opencqrs/templates/root/src/main/resources/application.properties +23 -0
  69. package/stacks/opencqrs/templates/root/src/main/resources/schema.sql +19 -0
  70. package/stacks/opencqrs/templates/root/src/main/resources/static/index.html +11 -0
  71. package/stacks/supabase/templates/build-kit/CLAUDE.md +25 -0
  72. package/stacks/supabase/templates/build-kit/lib/check-commit-scope.cjs +126 -0
  73. package/stacks/supabase/templates/build-kit/lib/checks/00-blocked-paths.cjs +28 -0
  74. package/stacks/supabase/templates/build-kit/lib/checks/10-slice-scope.cjs +34 -0
  75. package/stacks/supabase/templates/build-kit/lib/checks/20-append-only-migrations.cjs +21 -0
  76. package/stacks/supabase/templates/build-kit/lib/checks/30-test-file-present.cjs +44 -0
  77. package/stacks/supabase/templates/build-kit/lib/checks/40-no-invented-fields.cjs +92 -0
  78. package/stacks/supabase/templates/build-kit/lib/checks/50-spec-coverage.cjs +50 -0
  79. package/stacks/supabase/templates/build-kit/lib/checks/90-tsc-build.cjs +22 -0
  80. package/stacks/supabase/templates/root/.githooks/pre-commit +11 -0
  81. package/stacks/supabase/templates/root/package.json +2 -1
  82. package/stacks/supabase/templates/root/setup-env.sh +7 -1
  83. package/stacks/umadb/templates/.claude/skills/build-automation/SKILL.md +313 -0
  84. package/stacks/umadb/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +42 -0
  85. package/stacks/umadb/templates/.claude/skills/build-state-change/SKILL.md +376 -0
  86. package/stacks/umadb/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +42 -0
  87. package/stacks/umadb/templates/.claude/skills/build-state-change/references/umadb-query-patterns.md +78 -0
  88. package/stacks/umadb/templates/.claude/skills/build-state-view/SKILL.md +338 -0
  89. package/stacks/umadb/templates/build-kit/CLAUDE.md +94 -0
  90. package/stacks/umadb/templates/build-kit/lib/AGENT.md +47 -0
  91. package/stacks/umadb/templates/build-kit/lib/backend-prompt.md +169 -0
  92. package/stacks/umadb/templates/build-kit/lib/prompt.md +128 -0
  93. package/stacks/umadb/templates/root/.mvn/wrapper/maven-wrapper.properties +19 -0
  94. package/stacks/umadb/templates/root/README.md +48 -0
  95. package/stacks/umadb/templates/root/docker-compose.yml +29 -0
  96. package/stacks/umadb/templates/root/mvnw +259 -0
  97. package/stacks/umadb/templates/root/mvnw.cmd +149 -0
  98. package/stacks/umadb/templates/root/pom.xml +151 -0
  99. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/QuickstartApplication.java +12 -0
  100. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/config/UmaDbConfig.java +39 -0
  101. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/DecisionModelLoader.java +76 -0
  102. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/EventCodec.java +40 -0
  103. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/EventDispatcher.java +100 -0
  104. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/OptimisticConcurrencyException.java +14 -0
  105. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/SliceEventListener.java +23 -0
  106. package/stacks/umadb/templates/root/src/main/resources/application.properties +17 -0
  107. package/stacks/umadb/templates/root/src/test/java/io/umadb/quickstart/testsupport/InMemoryUmaDbClient.java +135 -0
  108. package/stacks/umadb/templates/root/src/test/resources/application.properties +8 -0
@@ -0,0 +1,391 @@
1
+ ---
2
+ name: build-state-view
3
+ authors:
4
+ - Martin Dilger
5
+ description: >
6
+ Implement read slices (JPA-backed projections + REST query endpoint + tests) directly against
7
+ KurrentDB 1.2.x (Java client) in this project's one established pattern: a KurrentDB persistent
8
+ subscription (filtered to this context's streams) feeds a projector method that updates a private
9
+ JPA entity; a separate REST controller queries the repository directly. Use when implementing a new
10
+ read slice / projection in this project. Plain Java only. There is exactly one supported style — do
11
+ not offer alternatives.
12
+ ---
13
+
14
+ # KurrentDB — Read Slice (Java)
15
+
16
+ ## Step 0: Discover target project conventions
17
+
18
+ > **Comments & description**: Each element in the slice carries a `comments: string[]` array and a
19
+ > `description` field. Use these as implementation hints. When done, resolve each used comment:
20
+ > `POST <BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes/<nodeId>/comments/<commentId>/resolve` (get
21
+ > IDs first via GET on same path).
22
+
23
+ Before writing any code, read the target project's `.build-kit/CLAUDE.md`.
24
+
25
+ **Determine `{basePackage}`** — every code example below is rooted at
26
+ `{basePackage}.slices.{context}.{slicename}`. Resolve `{basePackage}` as documented in
27
+ `.build-kit/CLAUDE.md`'s Structure section.
28
+
29
+ ## Step 1: Ensure events exist
30
+
31
+ Before implementing the read slice, verify that all events the projector handles exist in
32
+ `src/main/java/.../{context}/api/` (the context's sealed event interface + `{Context}EventTypes`). If
33
+ they don't, create them **first** — see `build-state-change` Step 2. Do not skip registering a new
34
+ event's type name even for a pure read slice — an unregistered event type is silently invisible to this
35
+ projector too.
36
+
37
+ ## Step 2: Implement the read slice
38
+
39
+ If the slice details include `## Scenarios (GWTs)`, use them to derive test cases (Step 5). GWT format
40
+ for read slices: `Given (events) → Then (information)` — no When. Events in Given tell you which
41
+ events the projector handles. The information element in Then describes the expected query result.
42
+
43
+ If the slice description or comments contain `## Implementation Guidelines`, **follow them**.
44
+
45
+ ### Slice package structure
46
+
47
+ ```
48
+ .../slices/{context}/{slicename}/ (i.e. {basePackage}.slices.{context}.{slicename} — see Step 0)
49
+ ├── {SliceName}Entity.java ← @Entity, private to this slice
50
+ ├── {SliceName}Repository.java ← Spring Data repository, private to this slice
51
+ ├── {SliceName}Summary.java ← read model (query result shape)
52
+ ├── {SliceName}Projector.java ← persistent-subscription listener + projection logic
53
+ └── {SliceName}RestApi.java ← @RestController (if REST chosen)
54
+ ```
55
+
56
+ A read slice lives in a single package. **Never reuse another slice's entity/repository** — a read
57
+ model is private to the slice that owns it, even if another slice's projector happens to need
58
+ similar-looking data.
59
+
60
+ ### Entity + repository
61
+
62
+ ```java
63
+ package {basePackage}.slices.{context}.{slicename};
64
+
65
+ import jakarta.persistence.*;
66
+
67
+ @Entity
68
+ @Table(name = "{context}_{slicename}", indexes = {
69
+ @Index(name = "idx_{context}_{slicename}_{col}", columnList = "{filterField}")
70
+ })
71
+ class {SliceName}Entity {
72
+
73
+ @Id
74
+ private String id;
75
+ private String {filterField};
76
+ // ... other fields
77
+
78
+ protected {SliceName}Entity() {}
79
+
80
+ {SliceName}Entity(String id, String {filterField}) {
81
+ this.id = id;
82
+ this.{filterField} = {filterField};
83
+ }
84
+
85
+ {SliceName}Summary toSummary() {
86
+ return new {SliceName}Summary(id, {filterField});
87
+ }
88
+ }
89
+ ```
90
+
91
+ ```java
92
+ package {basePackage}.slices.{context}.{slicename};
93
+
94
+ import org.springframework.data.repository.CrudRepository;
95
+ import java.util.List;
96
+
97
+ interface {SliceName}Repository extends CrudRepository<{SliceName}Entity, String> {
98
+ List<{SliceName}Entity> findAllBy{FilterField}(String {filterField});
99
+ }
100
+ ```
101
+
102
+ ### Result DTO rules
103
+
104
+ - If the read model matches the entity **1:1**, expose the summary record directly (`toSummary()`
105
+ above).
106
+ - If the read model contains fields the caller already knows from the query (e.g. the filter field
107
+ itself), omit those from `{SliceName}Summary` and map only what's new.
108
+
109
+ ### Projector — persistent subscription
110
+
111
+ A persistent subscription is KurrentDB's own durable, at-least-once, ack/nack-based consumer
112
+ mechanism — the server tracks this group's checkpoint, so there's no local progress table to maintain
113
+ (unlike the OpenCQRS/Axon kits' `ProgressTracker`).
114
+
115
+ **Subscription group name**: pick one stable name per read model (e.g. `"{context}-{slicename}"`) and
116
+ never rename it once deployed — KurrentDB persists the checkpoint under this exact group name.
117
+ Renaming it doesn't fail loudly; the "new" group just starts from `fromStart()` again (or from
118
+ wherever `CreatePersistentSubscriptionToAllOptions` says), reprocessing everything and likely
119
+ duplicating rows unless your upserts are idempotent by natural key (they are here, since `save(...)`
120
+ on a JPA entity with a fixed `@Id` upserts).
121
+
122
+ **Filter to this context's streams**: this read model spans every stream in the context (one stream per
123
+ aggregate instance, e.g. `library-4711`, `library-4712`, ...), so subscribe to `$all` with a
124
+ stream-name-prefix filter rather than to one specific stream.
125
+
126
+ ```java
127
+ package {basePackage}.slices.{context}.{slicename};
128
+
129
+ import {basePackage}.slices.{context}.api.{Context}EventTypes;
130
+ import io.kurrent.dbclient.*;
131
+ import jakarta.annotation.PostConstruct;
132
+ import org.springframework.beans.factory.annotation.Autowired;
133
+ import org.springframework.stereotype.Component;
134
+ import org.springframework.transaction.annotation.Transactional;
135
+
136
+ import java.util.concurrent.ExecutionException;
137
+
138
+ @Component
139
+ public class {SliceName}Projector {
140
+
141
+ private static final String GROUP = "{context}-{slicename}";
142
+
143
+ private final KurrentDBPersistentSubscriptionsClient subscriptionsClient;
144
+ private final {SliceName}Repository repository;
145
+
146
+ public {SliceName}Projector(KurrentDBPersistentSubscriptionsClient subscriptionsClient,
147
+ {SliceName}Repository repository) {
148
+ this.subscriptionsClient = subscriptionsClient;
149
+ this.repository = repository;
150
+ }
151
+
152
+ @PostConstruct
153
+ void start() throws ExecutionException, InterruptedException {
154
+ SubscriptionFilter filter = SubscriptionFilter.newBuilder()
155
+ .addStreamNamePrefix("{context-lower}-")
156
+ .build();
157
+ try {
158
+ subscriptionsClient.createToAll(GROUP,
159
+ CreatePersistentSubscriptionToAllOptions.get().fromStart().filter(filter)).get();
160
+ } catch (ExecutionException e) {
161
+ if (!(e.getCause() instanceof io.grpc.StatusRuntimeException grpcException)
162
+ || grpcException.getStatus().getCode() != io.grpc.Status.Code.ALREADY_EXISTS) {
163
+ throw e;
164
+ }
165
+ // group already created on a previous startup — expected, not an error
166
+ }
167
+
168
+ subscriptionsClient.subscribeToAll(GROUP, new PersistentSubscriptionListener() {
169
+ @Override
170
+ @Transactional
171
+ public void onEvent(PersistentSubscription subscription, int retryCount, ResolvedEvent event) {
172
+ try {
173
+ var domainEvent = {Context}EventTypes.deserialize(event.getOriginalEvent());
174
+ domainEvent.ifPresent({SliceName}Projector.this::project);
175
+ subscription.ack(event);
176
+ } catch (Exception ex) {
177
+ subscription.nack(NackAction.Park, ex.getMessage(), event);
178
+ }
179
+ }
180
+
181
+ @Override
182
+ public void onCancelled(PersistentSubscription subscription, Throwable exception) {
183
+ // logged by the framework's own subscription lifecycle; nothing project-specific needed
184
+ }
185
+ });
186
+ }
187
+
188
+ private void project(/* {Context}Event */ Object event) {
189
+ switch (event) {
190
+ case {EventName} e -> repository.save(new {SliceName}Entity(e.idField(), e.{filterField}()));
191
+ default -> { /* not this projection's concern */ }
192
+ }
193
+ }
194
+ }
195
+ ```
196
+
197
+ `domainEvent.ifPresent(...)` already filters out events this context doesn't recognize; `project`'s own
198
+ `switch` further narrows to just the event type(s) this specific read model cares about — every other
199
+ recognized-but-irrelevant event in the context falls through the `default` arm and is still acked
200
+ (acking is "I've seen this event", not "this event changed my data").
201
+
202
+ ### Entity + repository template (for filtered queries)
203
+
204
+ Use `findAllBy{FilterField}(...)` in the query endpoint below — DB-level filtering, not client-side.
205
+
206
+ ## Step 3: REST query endpoint
207
+
208
+ Separate class — plain constructor-injected Spring bean, unrelated to the persistent-subscription
209
+ listener above:
210
+
211
+ ```java
212
+ package {basePackage}.slices.{context}.{slicename};
213
+
214
+ import org.springframework.web.bind.annotation.*;
215
+ import java.util.List;
216
+
217
+ @RestController
218
+ public class {SliceName}RestApi {
219
+
220
+ private final {SliceName}Repository repository;
221
+
222
+ public {SliceName}RestApi({SliceName}Repository repository) {
223
+ this.repository = repository;
224
+ }
225
+
226
+ @GetMapping("/api/{context}/{filterField}")
227
+ public List<{SliceName}Summary> query(@PathVariable String {filterField}) {
228
+ return repository.findAllBy{FilterField}({filterField}).stream()
229
+ .map({SliceName}Entity::toSummary)
230
+ .toList();
231
+ }
232
+ }
233
+ ```
234
+
235
+ This project uses plain Spring **WebMVC** — a blocking return value, not `Mono<...>`.
236
+
237
+ ## Step 4: Design test cases
238
+
239
+ Implement the test cases provided in the slice definition. Do not design your own test cases unless
240
+ specifically instructed to do so.
241
+
242
+ ### Mapping GWT scenarios to tests
243
+
244
+ | GWT Element | Test code |
245
+ |---|---|
246
+ | `NOTHING` in Given | call the repository query directly with no prior `.save(...)` |
247
+ | Event in Given | call the projector's `project(event)` method directly (package-private — see below) |
248
+ | Information in Then | assert the repository query result / mapped summaries |
249
+
250
+ ## Step 5: Implement the test — `@DataJpaTest` + `@Import` + Testcontainers Postgres
251
+
252
+ There is no need to exercise the real persistent-subscription machinery to test a projection's
253
+ business logic — split `project(Object event)` out as its own package-private method (as shown above)
254
+ specifically so a test can call it directly, bypassing `@PostConstruct`/subscribe/ack entirely.
255
+
256
+ **This project is on Spring Boot 4** — `@DataJpaTest` moved to the `spring-boot-data-jpa-test` artifact
257
+ under package `org.springframework.boot.data.jpa.test.autoconfigure` (not the Boot 3
258
+ `org.springframework.boot.test.autoconfigure.orm.jpa` package).
259
+
260
+ **This project's read models target PostgreSQL, not H2** — `@DataJpaTest`'s default behavior tries to
261
+ replace the datasource with an embedded database, which fails outright with no H2/Derby/HSQL on the
262
+ classpath. Use Testcontainers' real Postgres instead:
263
+
264
+ ```java
265
+ package {basePackage}.slices.{context}.{slicename};
266
+
267
+ import {basePackage}.slices.{context}.api.{EventName};
268
+ import org.junit.jupiter.api.Test;
269
+ import org.springframework.beans.factory.annotation.Autowired;
270
+ import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
271
+ import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
272
+ import org.springframework.context.annotation.Import;
273
+ import org.testcontainers.containers.PostgreSQLContainer;
274
+ import org.testcontainers.junit.jupiter.Container;
275
+ import org.testcontainers.junit.jupiter.Testcontainers;
276
+
277
+ import static org.assertj.core.api.Assertions.assertThat;
278
+
279
+ @DataJpaTest
280
+ @Testcontainers
281
+ @Import({SliceName}Projector.class)
282
+ class {SliceName}ProjectorTest {
283
+
284
+ @Container
285
+ @ServiceConnection
286
+ static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17");
287
+
288
+ @Autowired
289
+ private {SliceName}Repository repository;
290
+
291
+ // Constructing {SliceName}Projector directly (not @Autowired) avoids @PostConstruct trying to
292
+ // reach a real KurrentDB from this test — pass null for the subscriptions client since project()
293
+ // never uses it.
294
+ private {SliceName}Projector projector;
295
+
296
+ @org.junit.jupiter.api.BeforeEach
297
+ void setUp() {
298
+ projector = new {SliceName}Projector(null, repository);
299
+ }
300
+
301
+ @Test
302
+ void givenNoEvents_thenEmptyResult() {
303
+ assertThat(repository.findAllBy{FilterField}("filter-value")).isEmpty();
304
+ }
305
+
306
+ @Test
307
+ void givenCreationEvent_thenItemAppears() {
308
+ projector.project(new {EventName}("id-1", "filter-value" /*, other fields */));
309
+
310
+ var result = repository.findAllBy{FilterField}("filter-value").stream()
311
+ .map({SliceName}Entity::toSummary)
312
+ .toList();
313
+
314
+ assertThat(result).containsExactly(new {SliceName}Summary("id-1", "filter-value"));
315
+ }
316
+
317
+ @Test
318
+ void itemsAreIsolatedByFilterField() {
319
+ projector.project(new {EventName}("id-1", "group-A"));
320
+ projector.project(new {EventName}("id-2", "group-B"));
321
+
322
+ assertThat(repository.findAllBy{FilterField}("group-A")).hasSize(1);
323
+ assertThat(repository.findAllBy{FilterField}("group-B")).hasSize(1);
324
+ }
325
+ }
326
+ ```
327
+
328
+ If `project`'s `switch` is package-private (as written above), this test class must live in the same
329
+ package — which it already does, following this stack's convention (`{SliceName}ProjectorTest` next to
330
+ `{SliceName}Projector`).
331
+
332
+ ### Key rules
333
+
334
+ - Call `project(event)` directly — never go through `@PostConstruct`/the persistent-subscription
335
+ listener in a test. `onEvent`'s job (deserialize, ack/nack) is KurrentDB-plumbing, already covered
336
+ once, generically, by `{Context}EventTypes` — it's not this slice's business logic to re-test.
337
+ - **Assert with full objects**: `containsExactly(new Summary(...))` rather than field-by-field
338
+ assertions — catches mapping mistakes a partial assertion would miss.
339
+ - **Requires Docker running locally/in CI** — the `PostgreSQLContainer` starts a real container per
340
+ test class. There is no embedded-database fallback in this project.
341
+
342
+ ## Step 5b: Storyline-Derived Tests (Optional)
343
+
344
+ `slice.json` may also carry a `storylines[]` array — narrated walkthroughs where the *same* read
345
+ model appears as multiple ordered "beats" across one flow (see `elements[]` on each storyline).
346
+ This is a secondary, supplementary source: `specifications[]` (Step 4) remains the primary and
347
+ default source of test cases. Most slices have no `storylines[]` — skip this step silently when
348
+ there's nothing relevant.
349
+
350
+ For each storyline, find beats whose `type` is `READMODEL`. Two such beats **adjacent with only
351
+ `EVENT` beat(s) between them** describe one clean, isolable projection test:
352
+
353
+ - events = the cumulative ordered `EVENT` beats from the start of the storyline through the
354
+ intervening event(s) — call `projector.project(...)` once per event, in order
355
+ - expected result = the later `READMODEL` beat's `fields`/`examples`/`expectEmptyList`
356
+
357
+ Keep these in a clearly separate `@Nested` class named after the storyline's title:
358
+
359
+ ```java
360
+ @Nested
361
+ class StorylineTests {
362
+ @Test
363
+ void afterEvent_readModelShowsExpectedState() {
364
+ projector.project(new {EventName}(/* fields from the intervening beat(s) */));
365
+
366
+ var result = repository.findAllBy{FilterField}("filter-value").stream()
367
+ .map({SliceName}Entity::toSummary)
368
+ .toList();
369
+
370
+ assertThat(result).containsExactly(/* expected shape from the later beat */);
371
+ }
372
+ }
373
+ ```
374
+
375
+ If a beat between two read-model states is a `COMMAND` rather than an `EVENT`, that half belongs to
376
+ `build-state-change` (its own command-handler test), not here — only project the `EVENT`→`READMODEL`
377
+ half. If a storyline segment involves a `SCREEN`/other untraceable beat, don't force a test — leave it
378
+ undocumented rather than fabricating an assertion.
379
+
380
+ ## Final Verification: Does the Implementation Match slice.json?
381
+
382
+ Before marking this slice as `Done`, verify the implementation against slice.json:
383
+
384
+ - [ ] Every field in the read model / query result definition in slice.json has a field in `{SliceName}Summary` — no invented fields
385
+ - [ ] Every event type in `events[]` this projection reacts to is handled in `project`'s `switch` — no events missed or assumed
386
+ - [ ] Every new event's type name is registered in `{Context}EventTypes`
387
+ - [ ] The subscription's group name is stable and won't collide with another slice's
388
+ - [ ] Every GWT scenario in `specifications[]` maps to a test case calling `project(...)` directly
389
+ - [ ] If `storylines[]` is present: every adjacent READMODEL↔READMODEL beat pair for this slice's read model (with only EVENT beats between) has a `@Nested` storyline test — or was deliberately skipped as untraceable
390
+ - [ ] No extra query parameters or filter logic were added beyond what slice.json defines
391
+ - [ ] No field names were assumed or guessed — if a field is not in slice.json, it is not in the code
@@ -0,0 +1,122 @@
1
+ # Project Configuration
2
+
3
+ This project is built with plain Java, Spring Boot, and Maven, backed directly by
4
+ [KurrentDB](https://docs.kurrent.io) (formerly EventStoreDB) as the event store. **There is no CQRS
5
+ framework layer** — unlike this CLI's OpenCQRS or Axon kits, KurrentDB's Java client
6
+ (`kurrentdb-client`) is a plain SDK with no annotations, no command router, no auto-configuration of
7
+ its own. This project's own conventions (documented below, and in each `build-*` skill) are the entire
8
+ "framework". Plain Java — no Kotlin.
9
+
10
+ `{basePackage}` in this project's Java code (`src/main/java/<basePackage>/slices/...`) is this
11
+ project's own Java package prefix, not a fixed value — resolve it, in order: (1) the package of the
12
+ project's `@SpringBootApplication` class, (2) the package of any existing slice already under
13
+ `.../slices/{context}/{slicename}/`, (3) only if no code exists yet, Maven's `<groupId>` in `pom.xml`.
14
+ Never hardcode `com.example.quickstart` (the shipped quickstart scaffold's package) or any other
15
+ specific package.
16
+
17
+ ## Structure
18
+
19
+ - Slices live under `src/main/java/{basePackage}/slices/{context}/{slicename}/` — flat, no
20
+ `write`/`read`/`automation` folder layer in between, except the shared `slices/{context}/api/` folder
21
+ (events + the context's sealed event-union interface, shared by every slice in that context) sitting
22
+ alongside slice folders. Automations live under `slices/{context}/automation/{slicename}/`.
23
+ - **Streams**: one KurrentDB stream per aggregate instance, named `{context-lower}-{id}` (e.g.
24
+ `library-4711`). A parent/child relationship between two slices is its own separate stream with its
25
+ own name — there is no built-in hierarchical-subject mechanism like OpenCQRS's (KurrentDB stream
26
+ names are just opaque strings); if a decision genuinely needs data from another stream, read it
27
+ explicitly via `EventStore`/`KurrentDBClient`, don't try to encode a parent/child path convention.
28
+ - **Events**: plain Java records, implementing that context's sealed marker interface (e.g.
29
+ `LibraryEvent`), living in `slices/{context}/api/`. Every event type has a short, stable string name
30
+ (e.g. `"BookPurchased"`) — **never** the Java classname — registered in that context's
31
+ `{Context}EventTypes` class (serialize/deserialize + the type-name mapping). This is the one registry
32
+ new events must be added to; forgetting to costs nothing at compile time but means the event is
33
+ silently invisible to every reader (`evolve`, a projection, an automation) that scans a stream — see
34
+ `build-state-change` Step 2.
35
+ - **`EventStore`** (`{basePackage}.common.EventStore`) is the one shared low-level helper every write
36
+ slice uses to read/append streams — see its Javadoc. It is intentionally thin; it does not know about
37
+ `decide`/`evolve` or any slice's domain types.
38
+ - **decide/evolve**: every write slice defines its own `evolve(state, event) -> state` (pure, no I/O)
39
+ and `decide(command, state) -> List<Event>` (pure, no I/O, throws on a business-rule violation) as
40
+ static methods — see `build-state-change`. These are genuinely pure Java functions; their tests never
41
+ touch KurrentDB, Spring, or a container.
42
+ - **Read models / automations**: use a [KurrentDB persistent subscription](https://docs.kurrent.io/clients/java/persistent-subscriptions.html)
43
+ (its own named subscription group) — see `build-state-view`/`build-automation`. KurrentDB tracks each
44
+ group's checkpoint durably server-side; there is no local progress-tracking table to maintain (unlike
45
+ the OpenCQRS/Axon kits).
46
+
47
+ ## Code Standards
48
+
49
+ - **Language**: plain Java only — no Kotlin, no Lombok.
50
+ - **Records** for commands, events, and immutable write-model state.
51
+ - Ensure all code is properly typed; avoid raw types.
52
+ - Serialize event payloads with `com.fasterxml.jackson.core:jackson-databind`'s `ObjectMapper` — this is
53
+ declared explicitly in `pom.xml` (Spring Boot 4's own internal JSON handling moved to Jackson 3
54
+ `tools.jackson.core`, which is not what `kurrentdb-client`'s own examples/API use).
55
+
56
+ ## Development Guidelines
57
+
58
+ 1. Each slice should be self-contained and focused on a specific domain.
59
+ 2. Maintain clear separation of concerns within each slice.
60
+ 3. Only check `src/main/java/{basePackage}/slices/{slicename}/*.java`, do not check subfolders unless
61
+ explicitly tasked to.
62
+
63
+ Ignore case for files and slices in prompts. "CartItems" slice is the same as "cartitems".
64
+
65
+ Do not change test files unless explicitly instructed: `src/test/java/**/*Test.java`.
66
+
67
+ At the start of every session, read `.build-kit/AGENTS.md` if it exists to load accumulated project
68
+ learnings.
69
+
70
+ When starting to work on a slice, invoke the `update-slice-status` skill with `InProgress` status before
71
+ doing anything else.
72
+
73
+ ## Building a Slice
74
+
75
+ **CRITICAL: You MUST always use the provided skills to build slices. NEVER implement a slice manually.**
76
+ **ALL fields, event names, command names, and business rules MUST come exclusively from slice.json. Do NOT
77
+ invent, assume, or guess any field or logic not present in the slice definition.**
78
+
79
+ **If, at any point below, the slice's requirements are genuinely ambiguous, contradictory, or missing a
80
+ decision you need in order to proceed — do not guess, and do not build anyway.** Invoke the
81
+ `request-feedback` skill with the specific question; it posts the question as a comment on the slice and
82
+ marks it `Blocked`, and you then stop work on this slice for this run. This is an escalation path, not a
83
+ routine step — read `slice.json` and the matching build skill's own instructions fully first; most slices
84
+ are fully specified and need none of this.
85
+
86
+ When asked to build a slice, always follow this flow:
87
+
88
+ 1. Read the slice definition from `.build-kit/.slices/<context>/<slicename>/slice.json`.
89
+ 2. Determine the slice type:
90
+ - **Translation** — `sliceType === "TRANSLATION"` → read `description` and `notes` from slice.json for
91
+ hints; default to `/build-automation` if nothing else is specified
92
+ - **Automation** — `processors` array is non-empty → invoke `/build-automation`
93
+ - **State-view** — `projections` or `queries` array is non-empty → invoke `/build-state-view`
94
+ - **State-change** — default (has `commands` / `events`) → invoke `/build-state-change`
95
+ 3. Invoke the matching skill and follow its instructions completely. Do not deviate.
96
+ 4. **Verify against slice.json**: After the skill completes, check that every command field, event field,
97
+ and specification in slice.json appears in the implementation. No invented fields — if it is not in
98
+ slice.json, it must not be in the code.
99
+ 5. Run quality checks (`./mvnw compile -q`, then the slice tests only).
100
+ 6. If checks pass, commit with `feat: [Slice Name]` and set slice status to `Done`.
101
+
102
+ After you are done, automatically run the tests for the slice that was edited.
103
+
104
+ ## Example Slice Structure
105
+
106
+ ```
107
+ src/main/java/{basePackage}/slices/
108
+ ├── {context}/
109
+ │ ├── api/ ← {Context}Event sealed interface + records + {Context}EventTypes
110
+ │ ├── {slicename}/ ← state-change slice
111
+ │ │ ├── {SliceName}.java ← write-model record
112
+ │ │ ├── {SliceName}Command.java
113
+ │ │ └── {SliceName}Handling.java ← evolve/decide/handle + Spring wiring
114
+ │ ├── {slicename}/ ← state-view slice
115
+ │ │ ├── {SliceName}Entity.java
116
+ │ │ ├── {SliceName}Repository.java
117
+ │ │ ├── {SliceName}Summary.java
118
+ │ │ ├── {SliceName}Projector.java
119
+ │ │ └── {SliceName}RestApi.java
120
+ │ └── automation/{slicename}/ ← automation slice
121
+ │ └── {AutomationName}Handling.java
122
+ ```
@@ -0,0 +1,73 @@
1
+ # Agent Learnings
2
+
3
+ Patterns and gotchas discovered during task processing. Update this file whenever you encounter something reusable.
4
+
5
+ ## tasks.json
6
+
7
+ - Tasks are objects with `id`, `createdAt`, and `payload` (a `SliceChangedPayload`).
8
+ - After completing a task, remove it from the array entirely — do not add a status field.
9
+ - Write `[]` to `tasks.json` if the last task is completed.
10
+
11
+ ## SliceChangedPayload fields
12
+
13
+ ```
14
+ event always "slice:changed"
15
+ organizationId org UUID or null
16
+ boardId board UUID
17
+ sliceId SLICE_BORDER node UUID — use this with /load-slice
18
+ sliceTitle human-readable slice name (may be null)
19
+ sliceStatus e.g. "Created", "InProgress", "Done", "Blocked" (may be null)
20
+ timestamp unix ms when the change was emitted
21
+ ```
22
+
23
+ ## Slice files
24
+
25
+ The realtime agent writes one file per slice on startup and after each `slice:changed` event:
26
+
27
+ ```
28
+ .slices/<context>/<sliceName>/slice.json
29
+ ```
30
+
31
+ - `<context>` is the slice's context value, or `default` if none.
32
+ - `<sliceName>` is the slice title lowercased with spaces removed (e.g. `"Enable User"` → `enableuser`).
33
+
34
+ These files are always up to date — read them directly before invoking any skill.
35
+
36
+ ## Skill Usage
37
+
38
+ - Always run `/connect` first to load credentials from `.eventmodelers/config.json` before calling any other skill.
39
+ - `/load-slice sliceId=<uuid>` re-fetches all slices from the API, refreshes the slice files, and returns the requested slice. Use it when you need a guaranteed-fresh view of a specific slice.
40
+ - Read `.slices/<context>/<sliceName>/slice.json` directly when you already know the context and name and the file is recent enough.
41
+
42
+ ## Board API
43
+
44
+ - The `boardId` and `organizationId` from each payload provide full context — pass them to skills.
45
+ - Node events use `node:created`, `node:changed`, `node:deleted` — always POST to `/api/org/:orgId/boards/:boardId/nodes/events`.
46
+ - Slice metadata (title, status) lives on the SLICE_BORDER node under `meta.sliceStatus` and `meta.title`.
47
+ - `/update-slice-status` rejects moving a slice into a status it's already in — this is a concurrency guard, not a bug. It means another agent already claimed the slice. Treat it as `ALREADY_IN_STATUS`, skip that slice, and move on to the next `Planned` one instead of erroring out.
48
+
49
+ ## KurrentDB-specific
50
+
51
+ - Forgetting to register a new event type in a context's `{Context}EventTypes` class doesn't fail at
52
+ compile time — it fails silently: `evolve`/a projection/an automation just never sees that event type
53
+ when scanning a stream (it's treated as an unrecognized foreign event and skipped). Always add the
54
+ registration line when a slice introduces a new event.
55
+ - An automation dispatching a command by appending to another stream must catch
56
+ `WrongExpectedVersionException` locally when a retried (at-least-once persistent-subscription)
57
+ redelivery is expected to re-hit an already-applied conflict — do not let it propagate out of the
58
+ persistent-subscription listener's `onEvent`, or the message will be nacked/retried forever for no
59
+ reason. Ack after catching it.
60
+ - Creating a persistent subscription group that already exists throws a raw `io.grpc.StatusRuntimeException`
61
+ with status code `ALREADY_EXISTS` (not one of the SDK's own exception classes) — catch and ignore this
62
+ specific case at startup, since subscription-group creation must be idempotent across app restarts.
63
+ `io.grpc:grpc-api` is only a *runtime*-scope transitive dependency of `kurrentdb-client` — declared
64
+ explicitly (compile scope) in `pom.xml` so `io.grpc.Status`/`StatusRuntimeException` are usable in
65
+ application code at all; verified by compiling against the real dependency.
66
+ - Spring Boot 4 moved classic Jackson 2 (`com.fasterxml.jackson.core`) off the default compile
67
+ classpath (its own internals now use Jackson 3, `tools.jackson.core`) — this project declares
68
+ `com.fasterxml.jackson.core:jackson-databind` explicitly in `pom.xml` for that reason. Similarly,
69
+ `@DataJpaTest`/`@WebMvcTest` moved to `org.springframework.boot.data.jpa.test.autoconfigure`/
70
+ `org.springframework.boot.webmvc.test.autoconfigure` respectively.
71
+ - The `docker.kurrent.io/kurrent-latest/kurrentdb:latest` image can be slow or occasionally flaky to
72
+ start under ARM/amd64 emulation (e.g. Apple Silicon Docker Desktop) — a container startup timeout or
73
+ one-off failure there is usually transient; retry before assuming a real problem.