@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,85 @@
1
+ # CommandHandlingTestFixture Cheat Sheet
2
+
3
+ `@CommandHandlingTest` (package `com.opencqrs.framework.command`, from the `framework-test` module) is a
4
+ narrow Spring test slice — it discovers this project's `@CommandHandlerConfiguration` classes, but never
5
+ touches EventSourcingDB, HTTP, or a real database. Inject one `CommandHandlingTestFixture<YourCommand>`
6
+ per test method via `@Autowired`.
7
+
8
+ ## Given
9
+
10
+ | Call | Meaning |
11
+ |---|---|
12
+ | `.given().nothing()` | no prior events/state — the creation case |
13
+ | `.given().events(e1, e2, ...)` | replays these through your `@StateRebuilding` methods to build prior state. **Prefer this** — it exercises the same reconstruction path production uses |
14
+ | `.given().state(new WriteModel(...))` | injects prior state directly, bypassing `@StateRebuilding` entirely. Only reach for this when replaying realistic events is impractical |
15
+
16
+ ## When
17
+
18
+ `.when(new YourCommand(...))` — executes the command handler under test.
19
+
20
+ ## Then — outcome
21
+
22
+ - `.succeeds()` — command executed without throwing
23
+ - `.fails().throwing(ExceptionClass.class)` — asserts the exact exception type thrown
24
+
25
+ ## Then — result value
26
+
27
+ `.succeeds().havingResult(expectedValue)` — asserts the command handler method's return value.
28
+ Chain this **before** `.allEvents()`/`.nextEvents()` if you also want to assert published events on the
29
+ same fixture call.
30
+
31
+ ## Then — published events
32
+
33
+ Two complementary entry points:
34
+
35
+ - `.allEvents()` — operates on the complete captured event list, no cursor (fine for a fixture used once
36
+ per test)
37
+ - `.nextEvents()` — a consuming cursor; use this if you called `.havingResult(...)` first and want to
38
+ keep asserting on the same fixture
39
+
40
+ Matcher methods (call one of these on the result of `.allEvents()`/`.nextEvents()`):
41
+
42
+ | Method | Behavior |
43
+ |---|---|
44
+ | `.exactly(payload1, payload2, ...)` | events match the given payloads in order, by `Object.equals` — records make this a plain field-by-field match |
45
+ | `.single(consumer)` | exactly one event was captured, **and** it matches the consumer's assertion |
46
+ | `.once(consumer)` | exactly one event matches the consumer's predicate; other unrelated events may also exist |
47
+ | `.any(consumer)` | at least one event matches |
48
+ | `.every(consumer)` | every captured event matches |
49
+ | `.none(consumer)` | no event matches |
50
+
51
+ `consumer` typically uses one of:
52
+
53
+ - `e -> e.ofType(SomeEvent.class)` — type-only check
54
+ - `e -> e.asserting(a -> a.commandSubject().payloadType(SomeEvent.class))` — asserts the event's subject
55
+ equals the command's subject, and its payload type
56
+ - `e -> e.asserting(a -> a.commandSubject().noMetaData().payloadType(SomeEvent.class))` — same, plus
57
+ asserts no metadata was published with the event
58
+
59
+ ## Mocking collaborators
60
+
61
+ Exactly like `@SpringBootTest` — `@MockitoBean` at the test class level:
62
+
63
+ ```java
64
+ @CommandHandlingTest
65
+ class {SliceName}HandlingTest {
66
+
67
+ @MockitoBean
68
+ private SomeCollaborator someCollaborator;
69
+
70
+ @Test
71
+ void test(@Autowired CommandHandlingTestFixture<{SliceName}Command> fixture) {
72
+ doReturn(someValue).when(someCollaborator).someMethod(any());
73
+ // ...
74
+ }
75
+ }
76
+ ```
77
+
78
+ ## What this fixture deliberately does NOT do
79
+
80
+ - No event upcasting, no event-type-string resolution — events are matched/compared as plain Java
81
+ objects.
82
+ - No real event store — nothing is ever actually persisted.
83
+ - Command metadata propagation onto published events (the `opencqrs.metadata.propagation.keys`
84
+ mechanism) is **not** exercised — this fixture only cares about the command handler's own logic.
85
+ - State caching is disabled — every `.when(...)` call rebuilds state from the given events/state fresh.
@@ -0,0 +1,354 @@
1
+ ---
2
+ name: build-state-view
3
+ authors:
4
+ - Martin Dilger
5
+ description: >
6
+ Implement read slices (JPA-backed projections + REST query endpoint + tests) using OpenCQRS 2.0.0
7
+ (Spring Boot, EventSourcingDB) in this project's one established pattern: Event(s) → @EventHandling
8
+ projector method(s) updating a private JPA entity → a separate REST controller querying the
9
+ repository directly. Use when implementing a new read slice / projection in this project. Plain Java
10
+ only. There is exactly one supported style — do not offer alternatives.
11
+ ---
12
+
13
+ # OpenCQRS — Read Slice (Java)
14
+
15
+ ## Step 0: Discover target project conventions
16
+
17
+ > **Comments & description**: Each element in the slice carries a `comments: string[]` array and a
18
+ > `description` field. Use these as implementation hints. When done, resolve each used comment:
19
+ > `POST <BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes/<nodeId>/comments/<commentId>/resolve` (get
20
+ > IDs first via GET on same path).
21
+
22
+ Before writing any code, read the target project's `.build-kit/CLAUDE.md`.
23
+
24
+ **Determine `{basePackage}`** — every code example below is rooted at
25
+ `{basePackage}.slices.{context}.{slicename}`. Resolve `{basePackage}` as documented in
26
+ `.build-kit/CLAUDE.md`'s Structure section.
27
+
28
+ ## Step 1: Ensure events exist
29
+
30
+ Before implementing the read slice, verify that all events the projector handles exist in
31
+ `src/main/java/.../{context}/api/`. If they don't, create them **first** (plain records — see
32
+ `build-state-change` Step 2), and register each new one's type string in `CqrsConfiguration`'s
33
+ `eventTypeResolver()` bean. Do not skip this even for a pure read slice — an unregistered event type
34
+ fails at runtime the first time it's read back.
35
+
36
+ ## Step 2: Implement the read slice
37
+
38
+ If the slice details include `## Scenarios (GWTs)`, use them to derive test cases (Step 5). GWT format
39
+ for read slices: `Given (events) → Then (information)` — no When. Events in Given tell you which events
40
+ the projector handles. The information element in Then describes the expected query result.
41
+
42
+ If the slice description or comments contain `## Implementation Guidelines`, **follow them**.
43
+
44
+ ### Slice package structure
45
+
46
+ ```
47
+ .../slices/{context}/{slicename}/ (i.e. {basePackage}.slices.{context}.{slicename} — see Step 0)
48
+ ├── {SliceName}Entity.java ← @Entity, private to this slice
49
+ ├── {SliceName}Repository.java ← Spring Data repository, private to this slice
50
+ ├── {SliceName}Summary.java ← read model (query result shape)
51
+ ├── {SliceName}Projector.java ← @Service with @EventHandling method(s)
52
+ └── {SliceName}RestApi.java ← @RestController (if REST chosen)
53
+ ```
54
+
55
+ A read slice lives in a single package. **Do NOT add Domain/Application/Presentation section
56
+ comments** — those are only for write slices. **Never reuse another slice's entity/repository** — a
57
+ read 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
+ Add a derived-query method (`findAllBy{FilterField}`) for filtered queries instead of `findAll()` +
103
+ client-side filtering — DB-level filtering, not in-memory.
104
+
105
+ ### Result DTO rules
106
+
107
+ - If the read model matches the entity **1:1**, expose the summary record directly (`toSummary()`
108
+ above).
109
+ - If the read model contains fields the caller already knows from the query (e.g. the filter field
110
+ itself), omit those from `{SliceName}Summary` and map only what's new.
111
+
112
+ ### Projector
113
+
114
+ `@EventHandling`-annotated methods receive their collaborators as **method parameters** annotated
115
+ `@Autowired` (resolved from the `ApplicationContext` at dispatch time) — not via the class's
116
+ constructor. This is the framework's actual injection mechanism for these methods; a constructor field
117
+ would be wrong here.
118
+
119
+ **Processing-group name**: pick one stable name per read model (e.g. `"{context}-{slicename}"`) and
120
+ never rename it once deployed — it's the literal key `JdbcProgressTracker` persists this projection's
121
+ checkpoint under (`EVENTHANDLER_PROGRESS.GROUP_KEY`). Renaming it doesn't fail loudly; it just orphans
122
+ the old checkpoint and makes this projection silently replay from the beginning on next start. If the
123
+ slice has two or more `@EventHandling` methods, define a small private meta-annotation instead of
124
+ repeating the group-name string on every method:
125
+
126
+ ```java
127
+ package {basePackage}.slices.{context}.{slicename};
128
+
129
+ import com.opencqrs.framework.eventhandler.EventHandling;
130
+ import java.lang.annotation.*;
131
+
132
+ @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
133
+ @Retention(RetentionPolicy.RUNTIME)
134
+ @EventHandling("{context}-{slicename}")
135
+ @interface {SliceName}Handling {}
136
+ ```
137
+
138
+ (If the slice has exactly one handler method, use `@EventHandling("{context}-{slicename}")` directly
139
+ instead — no meta-annotation needed.)
140
+
141
+ ```java
142
+ package {basePackage}.slices.{context}.{slicename};
143
+
144
+ import {basePackage}.slices.{context}.api.*;
145
+ import org.springframework.beans.factory.annotation.Autowired;
146
+ import org.springframework.stereotype.Service;
147
+ import org.springframework.transaction.annotation.Transactional;
148
+
149
+ @Service
150
+ @Transactional
151
+ public class {SliceName}Projector {
152
+
153
+ @{SliceName}Handling
154
+ public void on({EventName} event, @Autowired {SliceName}Repository repository) {
155
+ repository.save(new {SliceName}Entity(event.idField(), event.{filterField}()));
156
+ }
157
+ }
158
+ ```
159
+
160
+ `@Transactional` on the class (or per-method) wraps each single event-handling call in its own
161
+ transaction — this is a Spring convention layered on top, not something `@EventHandling` provides for
162
+ free. Omitting it does **not** mean no transaction runs at all: since this project's
163
+ `JdbcProgressTracker` has `setProceedTransactionally(true)` (see `CqrsConfiguration`), the handler
164
+ already participates in the same transaction as the checkpoint update, making the projection write and
165
+ the checkpoint advance atomic together — but only add an explicit `@Transactional` if this projector's
166
+ own logic needs a transaction boundary beyond that (e.g. multiple repository calls that must commit
167
+ together).
168
+
169
+ ### REST query endpoint
170
+
171
+ Separate class — this is plain Spring wiring (constructor-injected field), not an `@EventHandling`
172
+ method, so no per-method `@Autowired` is needed here:
173
+
174
+ ```java
175
+ package {basePackage}.slices.{context}.{slicename};
176
+
177
+ import org.springframework.web.bind.annotation.*;
178
+ import java.util.List;
179
+
180
+ @RestController
181
+ public class {SliceName}RestApi {
182
+
183
+ private final {SliceName}Repository repository;
184
+
185
+ public {SliceName}RestApi({SliceName}Repository repository) {
186
+ this.repository = repository;
187
+ }
188
+
189
+ @GetMapping("/api/{context}/{filterField}")
190
+ public List<{SliceName}Summary> query(@PathVariable String {filterField}) {
191
+ return repository.findAllBy{FilterField}({filterField}).stream()
192
+ .map({SliceName}Entity::toSummary)
193
+ .toList();
194
+ }
195
+ }
196
+ ```
197
+
198
+ This project uses plain Spring **WebMVC** — a blocking return value, not `Mono<...>`.
199
+
200
+ ## Step 3: Design test cases
201
+
202
+ Implement the test cases provided in the slice definition. Do not design your own test cases unless
203
+ specifically instructed to do so.
204
+
205
+ ### Mapping GWT scenarios to tests
206
+
207
+ | GWT Element | Test code |
208
+ |---|---|
209
+ | `NOTHING` in Given | call the repository query directly with no prior `.save(...)` |
210
+ | Event in Given | call `projector.on(event, repository)` directly |
211
+ | Information in Then | assert the repository query result / mapped summaries |
212
+
213
+ ## Step 4: Implement the test — `@DataJpaTest` + `@Import`
214
+
215
+ There is no built-in test fixture for `@EventHandling` methods (unlike `build-state-change`'s
216
+ `CommandHandlingTestFixture`) — write a plain JPA test slice instead. `@DataJpaTest` boots only JPA
217
+ infrastructure (no EventSourcingDB, no HTTP, no full application context); `@Import` pulls the plain
218
+ `@Service` projector class into that same narrow context so its `@Autowired` repository parameter
219
+ resolves correctly when you call it directly.
220
+
221
+ **This project is on Spring Boot 4** — `@DataJpaTest` moved to the `spring-boot-data-jpa-test` artifact
222
+ under package `org.springframework.boot.data.jpa.test.autoconfigure` (not the Boot 3
223
+ `org.springframework.boot.test.autoconfigure.orm.jpa` package). Verified by compiling and running
224
+ against the real dependency — don't "correct" this import back to the Boot 3 package.
225
+
226
+ **This project's read models target PostgreSQL, not H2** — `@DataJpaTest`'s default behavior tries to
227
+ replace the datasource with an embedded database, which fails outright with no H2/Derby/HSQL on the
228
+ classpath (`Failed to replace DataSource with an embedded database for tests`, verified). Use
229
+ Testcontainers' real Postgres instead, exactly like the project's other Postgres-backed tests
230
+ (`spring-boot-testcontainers` + `testcontainers-postgresql` are already on the test classpath):
231
+
232
+ ```java
233
+ package {basePackage}.slices.{context}.{slicename};
234
+
235
+ import org.junit.jupiter.api.Test;
236
+ import org.springframework.beans.factory.annotation.Autowired;
237
+ import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
238
+ import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
239
+ import org.springframework.context.annotation.Import;
240
+ import org.testcontainers.containers.PostgreSQLContainer;
241
+ import org.testcontainers.junit.jupiter.Container;
242
+ import org.testcontainers.junit.jupiter.Testcontainers;
243
+
244
+ import static org.assertj.core.api.Assertions.assertThat;
245
+
246
+ @DataJpaTest
247
+ @Testcontainers
248
+ @Import({SliceName}Projector.class)
249
+ class {SliceName}ProjectorTest {
250
+
251
+ @Container
252
+ @ServiceConnection
253
+ static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17");
254
+
255
+ @Autowired
256
+ private {SliceName}Projector projector;
257
+
258
+ @Autowired
259
+ private {SliceName}Repository repository;
260
+
261
+ @Test
262
+ void givenNoEvents_thenEmptyResult() {
263
+ assertThat(repository.findAllBy{FilterField}("filter-value")).isEmpty();
264
+ }
265
+
266
+ @Test
267
+ void givenCreationEvent_thenItemAppears() {
268
+ projector.on(new {CreationEvent}("id-1", "filter-value" /*, other fields */), repository);
269
+
270
+ var result = repository.findAllBy{FilterField}("filter-value").stream()
271
+ .map({SliceName}Entity::toSummary)
272
+ .toList();
273
+
274
+ assertThat(result).containsExactly(new {SliceName}Summary("id-1", "filter-value"));
275
+ }
276
+
277
+ @Test
278
+ void itemsAreIsolatedByFilterField() {
279
+ projector.on(new {CreationEvent}("id-1", "group-A"), repository);
280
+ projector.on(new {CreationEvent}("id-2", "group-B"), repository);
281
+
282
+ assertThat(repository.findAllBy{FilterField}("group-A")).hasSize(1);
283
+ assertThat(repository.findAllBy{FilterField}("group-B")).hasSize(1);
284
+ }
285
+ }
286
+ ```
287
+
288
+ ### Key rules
289
+
290
+ - Call the `@EventHandling` method directly (`projector.on(event, repository)`) — you're calling a
291
+ plain public Java method, not going through the framework's dispatch machinery. The same method also
292
+ works standalone in production; `@DataJpaTest` just gives you a real repository to pass it.
293
+ - **Assert with full objects**: `containsExactly(new Summary(...))` rather than field-by-field
294
+ assertions — catches mapping mistakes a partial assertion would miss.
295
+ - If the projector method also declares a `Map<String, ?> metadata` parameter, pass `Map.of()` (or the
296
+ relevant test values) directly — no need to route it through any dispatch machinery.
297
+ - **Requires Docker running locally/in CI** — the `PostgreSQLContainer` starts a real container per test
298
+ class. This is slower than a mocked unit test, but exercises the exact same schema/dialect production
299
+ uses; there is no embedded-database fallback in this project (see above).
300
+
301
+ ## Step 4b: Storyline-Derived Tests (Optional)
302
+
303
+ `slice.json` may also carry a `storylines[]` array — narrated walkthroughs where the *same* read
304
+ model appears as multiple ordered "beats" across one flow (see `elements[]` on each storyline).
305
+ This is a secondary, supplementary source: `specifications[]` (Step 3) remains the primary and
306
+ default source of test cases. Most slices have no `storylines[]` — skip this step silently when
307
+ there's nothing relevant.
308
+
309
+ A storyline embedded in this slice's slice.json already belongs entirely to this slice — no need
310
+ to match beats against `readmodels[]` by id/title. For each storyline, find beats whose `type` is
311
+ `READMODEL`. Two such beats **adjacent with only `EVENT` beat(s) between them** describe one clean,
312
+ isolable projection test:
313
+
314
+ - events = the cumulative ordered `EVENT` beats from the start of the storyline through the
315
+ intervening event(s)
316
+ - expected result = the later `READMODEL` beat's `fields`/`examples`/`expectEmptyList`
317
+
318
+ Write these as ordinary `@Test` methods (the same `projector.on(event, repository)` pattern applies
319
+ unchanged), but keep them in a clearly separate `@Nested` class named after the storyline's title, so
320
+ they never get confused with the exhaustive `specifications[]` suite:
321
+
322
+ ```java
323
+ @Nested
324
+ class StorylineTests {
325
+ @Test
326
+ void afterEvent_readModelShowsExpectedState() {
327
+ projector.on(new {EventName}(/* fields from the intervening beat(s) */), repository);
328
+
329
+ var result = repository.findAllBy{FilterField}("filter-value").stream()
330
+ .map({SliceName}Entity::toSummary)
331
+ .toList();
332
+
333
+ assertThat(result).containsExactly(/* expected shape from the later beat */);
334
+ }
335
+ }
336
+ ```
337
+
338
+ If a beat between two read-model states is a `COMMAND` rather than an `EVENT`, that half belongs
339
+ to `build-state-change` (its own command-handler test), not here — only project the `EVENT`→
340
+ `READMODEL` half. If a storyline segment involves a `SCREEN`/other untraceable beat, don't force a
341
+ test — leave it undocumented in code rather than fabricating an assertion.
342
+
343
+ ## Final Verification: Does the Implementation Match slice.json?
344
+
345
+ Before marking this slice as `Done`, verify the implementation against slice.json:
346
+
347
+ - [ ] Every field in the read model / query result definition in slice.json has a field in `{SliceName}Summary` — no invented fields
348
+ - [ ] Every event type in `events[]` has an `@EventHandling` method in the projector — no events missed or assumed
349
+ - [ ] Every new event's type string is registered in `CqrsConfiguration`'s `eventTypeResolver()`
350
+ - [ ] The projector's processing-group name is stable and won't collide with another slice's
351
+ - [ ] Every GWT scenario in `specifications[]` maps to a test case in `{SliceName}ProjectorTest`
352
+ - [ ] 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
353
+ - [ ] No extra query parameters or filter logic were added beyond what slice.json defines
354
+ - [ ] No field names were assumed or guessed — if a field is not in slice.json, it is not in the code
@@ -0,0 +1,101 @@
1
+ # Project Configuration
2
+
3
+ This project is built with [OpenCQRS](https://docs.opencqrs.com), Spring Boot, and Maven, backed by
4
+ [EventSourcingDB](https://www.eventsourcingdb.io) for events. **Plain Java — no Kotlin.**
5
+
6
+ `{basePackage}` in this project's Java code (`src/main/java/<basePackage>/slices/...`) is this project's
7
+ own Java package prefix, not a fixed value — resolve it, in order: (1) the package of the project's
8
+ `@SpringBootApplication` class, (2) the package of any existing slice already under
9
+ `.../slices/{context}/{slicename}/`, (3) only if no code exists yet, Maven's `<groupId>` in `pom.xml`.
10
+ Never hardcode `com.example.quickstart` (the shipped quickstart scaffold's package) or any other specific
11
+ package.
12
+
13
+ ## Structure
14
+
15
+ - Slices live under `src/main/java/{basePackage}/slices/{context}/{slicename}/` — flat, no
16
+ `write`/`read`/`automation` folder layer in between, except the shared `slices/{context}/api/` folder
17
+ (commands + events for that context) sitting alongside slice folders.
18
+ - Every event type used anywhere in the project must be registered in
19
+ `src/main/java/{basePackage}/config/CqrsConfiguration.java`'s `eventTypeResolver()` bean — this is a
20
+ single project-wide map, not per-slice. Forgetting to add a new event here doesn't fail at compile time;
21
+ it fails at runtime the first time that event is read back (`EventTypeResolutionException`, or silently
22
+ falls back to a classname-based type that breaks across renames). Whenever `build-state-change` or
23
+ `build-automation` introduces a new event, add its line to this map as part of that slice — never skip
24
+ it.
25
+ - Subjects are hierarchical, path-like strings (`Command.getSubject()`), e.g. `/book/{isbn}`,
26
+ `/book/{isbn}/page/{page}`. A child slice's subject nested under a parent's is how OpenCQRS models
27
+ parent/child aggregate relationships — see `build-state-change`'s Step 1 for how this replaces
28
+ compound-identifier/tagging schemes other frameworks need.
29
+ - **Command handlers always derive their state from [`@StateRebuilding`
30
+ methods](https://docs.opencqrs.com/reference/extension_points/state_rebuilding_handler/) reconstructing
31
+ the write model from sourced events — never from an external database, JPA repository, or any other
32
+ read model.** If a rule needs data from another subject, add a `@StateRebuilding` method for that
33
+ event type and use `sourcingMode = SourcingMode.RECURSIVE`; do not query a database instead. See
34
+ `build-state-change`'s Step 4.
35
+
36
+ ## Code Standards
37
+
38
+ - **Language**: plain Java only — no Kotlin, no Lombok.
39
+ - **Records** for commands, events, and immutable write-model state.
40
+ - Ensure all code is properly typed; avoid raw types.
41
+
42
+ ## Development Guidelines
43
+
44
+ 1. Each slice should be self-contained and focused on a specific domain.
45
+ 2. Maintain clear separation of concerns within each slice.
46
+ 3. Only check `src/main/java/{basePackage}/slices/{slicename}/*.java`, do not check subfolders unless
47
+ explicitly tasked to.
48
+
49
+ Ignore case for files and slices in prompts. "CartItems" slice is the same as "cartitems".
50
+
51
+ Do not change test files unless explicitly instructed: `src/test/java/**/*Test.java`.
52
+
53
+ At the start of every session, read `.build-kit/AGENTS.md` if it exists to load accumulated project
54
+ learnings.
55
+
56
+ When starting to work on a slice, invoke the `update-slice-status` skill with `InProgress` status before
57
+ doing anything else.
58
+
59
+ ## Building a Slice
60
+
61
+ **CRITICAL: You MUST always use the provided skills to build slices. NEVER implement a slice manually.**
62
+ **ALL fields, event names, command names, and business rules MUST come exclusively from slice.json. Do NOT
63
+ invent, assume, or guess any field or logic not present in the slice definition.**
64
+
65
+ **If, at any point below, the slice's requirements are genuinely ambiguous, contradictory, or missing a
66
+ decision you need in order to proceed — do not guess, and do not build anyway.** Invoke the
67
+ `request-feedback` skill with the specific question; it posts the question as a comment on the slice and
68
+ marks it `Blocked`, and you then stop work on this slice for this run. This is an escalation path, not a
69
+ routine step — read `slice.json` and the matching build skill's own instructions fully first; most slices
70
+ are fully specified and need none of this.
71
+
72
+ When asked to build a slice, always follow this flow:
73
+
74
+ 1. Read the slice definition from `.build-kit/.slices/<context>/<slicename>/slice.json`.
75
+ 2. Determine the slice type:
76
+ - **Translation** — `sliceType === "TRANSLATION"` → read `description` and `notes` from slice.json for
77
+ hints; default to `/build-automation` if nothing else is specified
78
+ - **Automation** — `processors` array is non-empty → invoke `/build-automation`
79
+ - **State-view** — `projections` or `queries` array is non-empty → invoke `/build-state-view`
80
+ - **State-change** — default (has `commands` / `events`) → invoke `/build-state-change`
81
+ 3. Invoke the matching skill and follow its instructions completely. Do not deviate.
82
+ 4. **Verify against slice.json**: After the skill completes, check that every command field, event field,
83
+ and specification in slice.json appears in the implementation. No invented fields — if it is not in
84
+ slice.json, it must not be in the code.
85
+ 5. Run quality checks (`./mvnw compile -q`, then the slice tests only).
86
+ 6. If checks pass, commit with `feat: [Slice Name]` and set slice status to `Done`.
87
+
88
+ After you are done, automatically run the tests for the slice that was edited.
89
+
90
+ ## Example Slice Structure
91
+
92
+ ```
93
+ src/main/java/{basePackage}/slices/
94
+ ├── {context}/
95
+ │ ├── api/ ← shared Command/Event records for this context
96
+ │ ├── {slicename}/ ← state-change slice
97
+ │ │ ├── {SliceName}Command.java
98
+ │ │ ├── {SliceName}.java ← write-model record
99
+ │ │ └── {SliceName}Handling.java
100
+ │ └── automation/{slicename}/ ← automation slice
101
+ ```
@@ -0,0 +1,58 @@
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
+ ## OpenCQRS-specific
50
+
51
+ - Forgetting to register a new event type in `CqrsConfiguration`'s `eventTypeResolver()` bean doesn't fail
52
+ at compile time — it fails at runtime the first time that event is read back. Always add the line when
53
+ a slice introduces a new event.
54
+ - An automation's event-handling method that dispatches a command via `CommandRouter` must catch
55
+ `CommandSubjectAlreadyExistsException`/`CommandSubjectDoesNotExistException` locally when a retried
56
+ (at-least-once) delivery is expected to re-hit an already-applied conflict — letting a
57
+ `NonTransientException` escape an `@EventHandling` method halts that entire processing group's loop, not
58
+ just that one event.