@eventmodelers/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/README.md +175 -0
  2. package/cli.js +676 -0
  3. package/package.json +35 -0
  4. package/shared/build-kit/code-export.mjs +560 -0
  5. package/shared/build-kit/lib/ollama-agent.js +147 -0
  6. package/shared/build-kit/package.json +11 -0
  7. package/shared/build-kit/ralph-ollama.js +39 -0
  8. package/shared/build-kit/realtime-agent.js +18 -0
  9. package/stacks/axon/templates/.claude/skills/build-automation/SKILL.md +407 -0
  10. package/stacks/axon/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +114 -0
  11. package/stacks/axon/templates/.claude/skills/build-automation-workflow/SKILL.md +496 -0
  12. package/stacks/axon/templates/.claude/skills/build-automation-workflow/references/axon-workflow-api.md +279 -0
  13. package/stacks/axon/templates/.claude/skills/build-automation-workflow/references/feature-flag-patterns.md +114 -0
  14. package/stacks/axon/templates/.claude/skills/build-state-change/SKILL.md +396 -0
  15. package/stacks/axon/templates/.claude/skills/build-state-change/references/axon-test-fixture-patterns.md +162 -0
  16. package/stacks/axon/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +56 -0
  17. package/stacks/axon/templates/.claude/skills/build-state-change/references/rest-api-patterns.md +115 -0
  18. package/stacks/axon/templates/.claude/skills/build-state-view/SKILL.md +282 -0
  19. package/stacks/axon/templates/.claude/skills/connect/SKILL.md +178 -0
  20. package/stacks/axon/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +611 -0
  21. package/stacks/axon/templates/.claude/skills/load-slice/SKILL.md +141 -0
  22. package/stacks/axon/templates/.claude/skills/update-slice-status/SKILL.md +105 -0
  23. package/stacks/axon/templates/build-kit/lib/AGENT.md +47 -0
  24. package/stacks/axon/templates/build-kit/lib/agent.sh +20 -0
  25. package/stacks/axon/templates/build-kit/lib/backend-prompt.md +158 -0
  26. package/stacks/axon/templates/build-kit/lib/prompt.md +126 -0
  27. package/stacks/axon/templates/build-kit/lib/ralph.js +369 -0
  28. package/stacks/axon/templates/build-kit/ralph-claude.js +44 -0
  29. package/stacks/axon/templates/build-kit/ralph.sh +98 -0
  30. package/stacks/axon/templates/root/.env.example +5 -0
  31. package/stacks/axon/templates/root/CLAUDE.md +60 -0
  32. package/stacks/axon/templates/root/README.md +44 -0
  33. package/stacks/axon/templates/root/docker-compose.yml +64 -0
  34. package/stacks/axon/templates/root/mvnw +259 -0
  35. package/stacks/axon/templates/root/mvnw.cmd +149 -0
  36. package/stacks/axon/templates/root/pom.xml +125 -0
  37. package/stacks/axon/templates/root/src/main/java/io/axoniq/quickstart/QuickstartApplication.java +128 -0
  38. package/stacks/axon/templates/root/src/main/java/io/axoniq/quickstart/config/WebConfig.java +155 -0
  39. package/stacks/axon/templates/root/src/main/resources/application.properties +2 -0
  40. package/stacks/axon/templates/root/src/main/resources/static/bg-body.png +0 -0
  41. package/stacks/axon/templates/root/src/main/resources/static/bg-header.png +0 -0
  42. package/stacks/axon/templates/root/src/main/resources/static/index.html +596 -0
  43. package/stacks/cratis-csharp/templates/.claude/skills/_shared/cratis-conventions.md +251 -0
  44. package/stacks/cratis-csharp/templates/.claude/skills/build-automation/SKILL.md +99 -0
  45. package/stacks/cratis-csharp/templates/.claude/skills/build-automation/references/patterns.md +115 -0
  46. package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/SKILL.md +163 -0
  47. package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/references/patterns.md +234 -0
  48. package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/SKILL.md +120 -0
  49. package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/references/patterns.md +166 -0
  50. package/stacks/cratis-csharp/templates/.claude/skills/connect/SKILL.md +169 -0
  51. package/stacks/cratis-csharp/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +609 -0
  52. package/stacks/cratis-csharp/templates/.claude/skills/load-slice/SKILL.md +141 -0
  53. package/stacks/cratis-csharp/templates/.claude/skills/update-slice-status/SKILL.md +105 -0
  54. package/stacks/cratis-csharp/templates/build-kit/lib/AGENT.md +58 -0
  55. package/stacks/cratis-csharp/templates/build-kit/lib/agent.sh +20 -0
  56. package/stacks/cratis-csharp/templates/build-kit/lib/backend-prompt.md +129 -0
  57. package/stacks/cratis-csharp/templates/build-kit/lib/prompt.md +124 -0
  58. package/stacks/cratis-csharp/templates/build-kit/lib/ralph.js +302 -0
  59. package/stacks/cratis-csharp/templates/build-kit/ralph-claude.js +37 -0
  60. package/stacks/cratis-csharp/templates/build-kit/ralph.sh +98 -0
  61. package/stacks/cratis-csharp/templates/root/.frontend/index.css +29 -0
  62. package/stacks/cratis-csharp/templates/root/.frontend/index.html +17 -0
  63. package/stacks/cratis-csharp/templates/root/.frontend/main.tsx +18 -0
  64. package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.json +42 -0
  65. package/stacks/cratis-csharp/templates/root/.frontend/tsconfig.node.json +11 -0
  66. package/stacks/cratis-csharp/templates/root/.frontend/vite.config.ts +56 -0
  67. package/stacks/cratis-csharp/templates/root/App.tsx +23 -0
  68. package/stacks/cratis-csharp/templates/root/CLAUDE.md +62 -0
  69. package/stacks/cratis-csharp/templates/root/CratisApp.csproj +25 -0
  70. package/stacks/cratis-csharp/templates/root/CratisApp.sln +18 -0
  71. package/stacks/cratis-csharp/templates/root/GlobalUsings.cs +3 -0
  72. package/stacks/cratis-csharp/templates/root/Home.tsx +102 -0
  73. package/stacks/cratis-csharp/templates/root/Program.cs +26 -0
  74. package/stacks/cratis-csharp/templates/root/README.md +192 -0
  75. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/AllListings.ts +47 -0
  76. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.cs +11 -0
  77. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/Listing.ts +12 -0
  78. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/ListingDataTable.tsx +17 -0
  79. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Listing/index.ts +1 -0
  80. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Register.ts +51 -0
  81. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/RegisterDialog.tsx +18 -0
  82. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/Registration.cs +27 -0
  83. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/Registration/index.ts +1 -0
  84. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeFeature.tsx +22 -0
  85. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/SomeName.cs +3 -0
  86. package/stacks/cratis-csharp/templates/root/SomeModule/SomeFeature/index.ts +1 -0
  87. package/stacks/cratis-csharp/templates/root/appsettings.Development.json +9 -0
  88. package/stacks/cratis-csharp/templates/root/appsettings.json +26 -0
  89. package/stacks/cratis-csharp/templates/root/docker-compose.yml +23 -0
  90. package/stacks/cratis-csharp/templates/root/package.json +33 -0
  91. package/stacks/cratis-csharp/templates/root/tsconfig.json +3 -0
  92. package/stacks/modeling-kit/templates/.claude/settings.local.json +13 -0
  93. package/stacks/modeling-kit/templates/.claude/skills/analyze-existing-model/SKILL.md +215 -0
  94. package/stacks/modeling-kit/templates/.claude/skills/attributes/SKILL.md +176 -0
  95. package/stacks/modeling-kit/templates/.claude/skills/connect/SKILL.md +182 -0
  96. package/stacks/modeling-kit/templates/.claude/skills/discover-storyboard/SKILL.md +383 -0
  97. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-applying-conways-law/SKILL.md +465 -0
  98. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/SKILL.md +587 -0
  99. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/references/facilitating-event-modeling-workshops.md +626 -0
  100. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-checking-completeness/SKILL.md +542 -0
  101. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-designing-event-models/SKILL.md +497 -0
  102. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md +783 -0
  103. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-inputs/SKILL.md +604 -0
  104. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md +699 -0
  105. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-integrating-legacy-systems/SKILL.md +674 -0
  106. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/SKILL.md +231 -0
  107. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/domain-patterns.md +150 -0
  108. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/patterns.md +329 -0
  109. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-optimizing-stream-design/references/snapshotting.md +204 -0
  110. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +352 -0
  111. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/references/project-planning-with-event-modeling.md +341 -0
  112. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-plotting-events/SKILL.md +153 -0
  113. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/SKILL.md +136 -0
  114. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/references/examples.md +78 -0
  115. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/references/patterns.md +85 -0
  116. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/SKILL.md +655 -0
  117. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-translating-external-events/SKILL.md +479 -0
  118. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models/SKILL.md +234 -0
  119. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models-checklist/SKILL.md +353 -0
  120. package/stacks/modeling-kit/templates/.claude/skills/examples/SKILL.md +200 -0
  121. package/stacks/modeling-kit/templates/.claude/skills/handle-comment/SKILL.md +94 -0
  122. package/stacks/modeling-kit/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +441 -0
  123. package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +290 -0
  124. package/stacks/modeling-kit/templates/.claude/skills/storyboard/SKILL.md +212 -0
  125. package/stacks/modeling-kit/templates/.claude/skills/storyboard-screen/SKILL.md +123 -0
  126. package/stacks/modeling-kit/templates/.claude/skills/timeline/SKILL.md +353 -0
  127. package/stacks/modeling-kit/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
  128. package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +134 -0
  129. package/stacks/modeling-kit/templates/kit/lib/agent.sh +20 -0
  130. package/stacks/modeling-kit/templates/kit/lib/ollama-agent.js +147 -0
  131. package/stacks/modeling-kit/templates/kit/lib/ralph.js +286 -0
  132. package/stacks/modeling-kit/templates/kit/package.json +12 -0
  133. package/stacks/modeling-kit/templates/kit/ralph-claude.js +50 -0
  134. package/stacks/modeling-kit/templates/kit/ralph-ollama.js +38 -0
  135. package/stacks/modeling-kit/templates/kit/ralph.sh +61 -0
  136. package/stacks/modeling-kit/templates/kit/realtime-agent.js +18 -0
  137. package/stacks/modeling-kit/templates/root/CLAUDE.md +56 -0
  138. package/stacks/node/templates/.claude/skills/build-automation/SKILL.md +266 -0
  139. package/stacks/node/templates/.claude/skills/build-state-change/SKILL.md +344 -0
  140. package/stacks/node/templates/.claude/skills/build-state-view/SKILL.md +412 -0
  141. package/stacks/node/templates/.claude/skills/connect/SKILL.md +178 -0
  142. package/stacks/node/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +628 -0
  143. package/stacks/node/templates/.claude/skills/load-slice/SKILL.md +143 -0
  144. package/stacks/node/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
  145. package/stacks/node/templates/build-kit/README.md +86 -0
  146. package/stacks/node/templates/build-kit/lib/AGENT.md +47 -0
  147. package/stacks/node/templates/build-kit/lib/agent.sh +20 -0
  148. package/stacks/node/templates/build-kit/lib/backend-prompt.md +152 -0
  149. package/stacks/node/templates/build-kit/lib/prompt.md +122 -0
  150. package/stacks/node/templates/build-kit/lib/ralph.js +369 -0
  151. package/stacks/node/templates/build-kit/ralph-claude.js +44 -0
  152. package/stacks/node/templates/build-kit/ralph.sh +98 -0
  153. package/stacks/node/templates/root/.env.example +15 -0
  154. package/stacks/node/templates/root/CLAUDE.md +60 -0
  155. package/stacks/node/templates/root/docker-compose.yml +15 -0
  156. package/stacks/node/templates/root/flyway.conf +17 -0
  157. package/stacks/node/templates/root/migrations/V1__schema.sql.example +12 -0
  158. package/stacks/node/templates/root/package.json +50 -0
  159. package/stacks/node/templates/root/server.ts +130 -0
  160. package/stacks/node/templates/root/setup-env.sh +53 -0
  161. package/stacks/node/templates/root/src/common/assertions.ts +6 -0
  162. package/stacks/node/templates/root/src/common/db.ts +32 -0
  163. package/stacks/node/templates/root/src/common/loadPostgresEventstore.ts +23 -0
  164. package/stacks/node/templates/root/src/common/parseEndpoint.ts +51 -0
  165. package/stacks/node/templates/root/src/common/processorDlq.ts +28 -0
  166. package/stacks/node/templates/root/src/common/realtimeBroadcast.ts +12 -0
  167. package/stacks/node/templates/root/src/common/replay.ts +16 -0
  168. package/stacks/node/templates/root/src/common/routes.ts +19 -0
  169. package/stacks/node/templates/root/src/common/testHelpers.ts +44 -0
  170. package/stacks/node/templates/root/src/swagger.ts +34 -0
  171. package/stacks/node/templates/root/src/util/assertions.ts +6 -0
  172. package/stacks/node/templates/root/src/util/hash.ts +9 -0
  173. package/stacks/node/templates/root/src/util/sanitize.ts +23 -0
  174. package/stacks/node/templates/root/tsconfig.json +32 -0
  175. package/stacks/node/templates/root/vercel.json +8 -0
  176. package/stacks/supabase/templates/.claude/skills/build-automation/SKILL.md +266 -0
  177. package/stacks/supabase/templates/.claude/skills/build-state-change/SKILL.md +344 -0
  178. package/stacks/supabase/templates/.claude/skills/build-state-view/SKILL.md +390 -0
  179. package/stacks/supabase/templates/.claude/skills/build-webhook/SKILL.md +303 -0
  180. package/stacks/supabase/templates/.claude/skills/connect/SKILL.md +178 -0
  181. package/stacks/supabase/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +628 -0
  182. package/stacks/supabase/templates/.claude/skills/load-slice/SKILL.md +143 -0
  183. package/stacks/supabase/templates/.claude/skills/update-slice-status/SKILL.md +110 -0
  184. package/stacks/supabase/templates/build-kit/README.md +86 -0
  185. package/stacks/supabase/templates/build-kit/lib/AGENT.md +47 -0
  186. package/stacks/supabase/templates/build-kit/lib/agent.sh +20 -0
  187. package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +152 -0
  188. package/stacks/supabase/templates/build-kit/lib/prompt.md +123 -0
  189. package/stacks/supabase/templates/build-kit/lib/ralph.js +369 -0
  190. package/stacks/supabase/templates/build-kit/ralph-claude.js +44 -0
  191. package/stacks/supabase/templates/build-kit/ralph.sh +98 -0
  192. package/stacks/supabase/templates/root/.env.example +22 -0
  193. package/stacks/supabase/templates/root/CLAUDE.md +61 -0
  194. package/stacks/supabase/templates/root/flyway.conf +17 -0
  195. package/stacks/supabase/templates/root/package.json +52 -0
  196. package/stacks/supabase/templates/root/server.ts +177 -0
  197. package/stacks/supabase/templates/root/setup-env.sh +53 -0
  198. package/stacks/supabase/templates/root/src/common/assertions.ts +6 -0
  199. package/stacks/supabase/templates/root/src/common/db.ts +32 -0
  200. package/stacks/supabase/templates/root/src/common/loadPostgresEventstore.ts +23 -0
  201. package/stacks/supabase/templates/root/src/common/parseEndpoint.ts +51 -0
  202. package/stacks/supabase/templates/root/src/common/processorDlq.ts +28 -0
  203. package/stacks/supabase/templates/root/src/common/realtimeBroadcast.ts +19 -0
  204. package/stacks/supabase/templates/root/src/common/replay.ts +16 -0
  205. package/stacks/supabase/templates/root/src/common/routes.ts +23 -0
  206. package/stacks/supabase/templates/root/src/common/testHelpers.ts +54 -0
  207. package/stacks/supabase/templates/root/src/supabase/README.md +141 -0
  208. package/stacks/supabase/templates/root/src/supabase/api.ts +56 -0
  209. package/stacks/supabase/templates/root/src/supabase/requireOrgaAdmin.ts +32 -0
  210. package/stacks/supabase/templates/root/src/supabase/requireSysUser.ts +40 -0
  211. package/stacks/supabase/templates/root/src/supabase/requireUser.ts +72 -0
  212. package/stacks/supabase/templates/root/src/swagger.ts +34 -0
  213. package/stacks/supabase/templates/root/src/util/assertions.ts +6 -0
  214. package/stacks/supabase/templates/root/src/util/hash.ts +9 -0
  215. package/stacks/supabase/templates/root/src/util/sanitize.ts +23 -0
  216. package/stacks/supabase/templates/root/supabase/config.toml +295 -0
  217. package/stacks/supabase/templates/root/supabase/migrations/V1__schema.sql.example +12 -0
  218. package/stacks/supabase/templates/root/supabase/seed.sql +1 -0
  219. package/stacks/supabase/templates/root/tsconfig.json +32 -0
  220. package/stacks/supabase/templates/root/vercel.json +8 -0
@@ -0,0 +1,266 @@
1
+ ---
2
+ name: build-automation
3
+ description: Implements an emmett automation slice (reactor processor + command handler) from a slice.json definition
4
+ ---
5
+
6
+ # Build Automation Slice
7
+
8
+ > Before doing anything else, read the slice definition from `.slices/{Context}/{slicename}/slice.json`. This file is the **source of truth** for which trigger event drives the automation and what command it fires.
9
+
10
+ ---
11
+
12
+ ## What an Automation Slice is
13
+
14
+ An automation slice reacts to events from the event store and fires a command in response. It replaces the old CRON + TODO-list pattern with an event-driven **reactor**.
15
+
16
+ Architecture:
17
+
18
+ ```
19
+ Event Store
20
+ │ (TriggerEvent emitted by another slice)
21
+
22
+ processor.ts ← reactor — listens, maps, fires command
23
+
24
+
25
+ {SliceName}Command.ts ← command handler (decide/evolve)
26
+
27
+
28
+ Event Store ← new events appended
29
+ ```
30
+
31
+ An automation slice is a **state-change slice with a reactor**. Always build the command handler first, then wire the processor.
32
+
33
+ ---
34
+
35
+ ## Step 1 — Read the slice.json
36
+
37
+ From the slice definition, extract:
38
+ - **sliceName** — the command being fired by the automation
39
+ - **context** — bounded context
40
+ - **processors[]** — each processor defines:
41
+ - `triggerEvent` — the event that starts the automation
42
+ - `command` — the command to fire
43
+ - `processorId` — unique kebab-case identifier
44
+ - **commands[]** — command data fields
45
+ - **events[]** — events emitted by the command
46
+ > **Comments & description**: Each element (commands, events, readmodels, processors, screens, tables) carries a `comments: string[]` array (board comments on that node) and a `description` field. The slice itself also has `comments: string[]`. Use these as implementation hints — pass them as code comments, documentation, or validation logic where they add value. When done, resolve each used comment: `POST <BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes/<nodeId>/comments/<commentId>/resolve` (get comment IDs first via GET on the same path without the last two segments).
47
+
48
+
49
+ ---
50
+
51
+ ## Step 2 — Build the command handler
52
+
53
+ Follow the **build-state-change** skill to create:
54
+ - `{SliceName}Command.ts` (Command type, evolve, decide, handle{SliceName})
55
+ - `{SliceName}.test.ts` (DeciderSpecification tests)
56
+
57
+ **Do NOT create a `routes.ts`** for automations — the command is fired internally by the processor, not via HTTP.
58
+
59
+ Refer to the build-state-change skill for the full command handler structure.
60
+
61
+ ---
62
+
63
+ ## Step 3 — Ensure the trigger event type exists
64
+
65
+ The trigger event must be defined in `[Context]Events.ts`. If it belongs to a different context, import it from that context's events file.
66
+
67
+ If the trigger event is missing from the union type, add it:
68
+
69
+ ```typescript
70
+ // in {TriggerContext}Events.ts
71
+ export type {TriggerEventName} = Event<'{TriggerEventName}', {
72
+ id: string;
73
+ // fields the processor will use to construct the command
74
+ }, CommonMeta>;
75
+
76
+ export type {TriggerContext}Events = /* existing */ | {TriggerEventName};
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Step 4 — Create `processor.ts`
82
+
83
+ File: `src/slices/{context}/{SliceName}/processor.ts`
84
+
85
+ ```typescript
86
+ import {type {TriggerEventName}} from '../{TriggerContext}Events';
87
+ import {{SliceName}Command, handle{SliceName}} from './{SliceName}Command';
88
+ import {PostgresEventStore, PostgreSQLEventStoreConsumer} from '@event-driven-io/emmett-postgresql';
89
+ import {storeDlqMessage} from '../../../common/processorDlq';
90
+ import {v4} from 'uuid';
91
+
92
+ const PROCESSOR_ID = '{unique-kebab-case-processor-id}';
93
+
94
+ let _consumer: PostgreSQLEventStoreConsumer<{TriggerEventName}> | null = null;
95
+
96
+ export const processor = {
97
+ start: async (eventStore: PostgresEventStore) => {
98
+ _consumer = eventStore.consumer<{TriggerEventName}>();
99
+
100
+ _consumer.reactor<{TriggerEventName}>({
101
+ processorId: PROCESSOR_ID,
102
+ processorInstanceId: v4(), // new UUID each restart — enables competing consumers
103
+ canHandle: ['{TriggerEventName}'],
104
+ lock: {
105
+ timeoutSeconds: 30,
106
+ acquisitionPolicy: {type: 'retry', retries: 60, minTimeout: 1000, maxTimeout: 2000},
107
+ },
108
+ eachMessage: async (message) => {
109
+ try {
110
+ console.log(`Processing ${message.type} for ${message.data.id}`);
111
+
112
+ const command: {SliceName}Command = {
113
+ type: '{SliceName}',
114
+ data: {
115
+ id: message.data.id,
116
+ // map fields from message.data to the command's data shape
117
+ },
118
+ metadata: {
119
+ correlation_id: message.data.id,
120
+ causation_id: message.metadata?.correlation_id,
121
+ },
122
+ };
123
+
124
+ await handle{SliceName}(message.data.id, command);
125
+ } catch (err) {
126
+ console.error(`${PROCESSOR_ID}: failed to process message`, message.data, err);
127
+ await storeDlqMessage(PROCESSOR_ID, message, err);
128
+ }
129
+ },
130
+ });
131
+
132
+ _consumer?.start().catch(err =>
133
+ console.error(`${PROCESSOR_ID} consumer error:`, err),
134
+ );
135
+ },
136
+
137
+ stop: async () => {
138
+ await _consumer?.stop();
139
+ },
140
+ };
141
+ ```
142
+
143
+ ### Key decisions when filling in the template
144
+
145
+ **`PROCESSOR_ID`** — unique kebab-case string identifying this processor across restarts. Use format: `{slicename}-automation` (e.g. `assign-user-to-organization-automation`). Never reuse IDs between processors.
146
+
147
+ **`processorInstanceId`** — `v4()` UUID generated at startup. A new UUID each time the server restarts allows multiple competing consumers to run safely in parallel.
148
+
149
+ **`canHandle`** — must list only the exact event type string(s) this reactor listens to.
150
+
151
+ **`lock`** — do not change the lock configuration unless there is a specific reason. The defaults provide safe at-least-once delivery with retry.
152
+
153
+ **Metadata mapping:**
154
+ - `correlation_id` in the command → pass the trigger message's `id` (aggregate identifier)
155
+ - `causation_id` in the command → pass the trigger message's `metadata?.correlation_id`
156
+
157
+ **DLQ** — always wrap `eachMessage` in try/catch and call `storeDlqMessage` on failure. Failed messages are not retried automatically; the DLQ record allows manual reprocessing.
158
+
159
+ ---
160
+
161
+ ## Step 5 — Register the processor in application startup
162
+
163
+ Find the app startup file (usually `src/index.ts` or `src/server.ts`) where other processors are started. Add:
164
+
165
+ ```typescript
166
+ import {processor as {SliceName}Processor} from './slices/{context}/{SliceName}/processor';
167
+
168
+ // during startup, after eventStore is initialized:
169
+ await {SliceName}Processor.start(eventStore);
170
+
171
+ // during shutdown:
172
+ await {SliceName}Processor.stop();
173
+ ```
174
+
175
+ The `eventStore` instance is the one returned by `findEventstore()` — reuse the shared instance, do not create a second one.
176
+
177
+ ---
178
+
179
+ ## Step 6 — Verify the event store bootstrap
180
+
181
+ The event store **must** call `schema.migrate()` before any processor starts. Check `src/common/loadPostgresEventstore.ts`:
182
+
183
+ ```typescript
184
+ export const findEventstore = async () => {
185
+ // ...
186
+ await eventStoreInstance.schema.migrate(); // ← must be present
187
+ return eventStoreInstance;
188
+ };
189
+ ```
190
+
191
+ If this line is missing, add it. Without it, the emmett schema functions (`emt_try_acquire_processor_lock` etc.) are not created and the reactor will fail to start.
192
+
193
+ ---
194
+
195
+ ## Processor patterns reference
196
+
197
+ ### Single trigger event → single command (standard)
198
+
199
+ ```typescript
200
+ eachMessage: async (message) => {
201
+ const command: MyCommand = {
202
+ type: 'MyCommand',
203
+ data: {id: message.data.id},
204
+ metadata: {
205
+ correlation_id: message.data.id,
206
+ causation_id: message.metadata?.correlation_id,
207
+ },
208
+ };
209
+ await handleMyCommand(message.data.id, command);
210
+ },
211
+ ```
212
+
213
+ ### Conditional processing (skip if condition not met)
214
+
215
+ ```typescript
216
+ eachMessage: async (message) => {
217
+ if (!message.data.someField) {
218
+ console.log(`Skipping — someField not set for ${message.data.id}`);
219
+ return;
220
+ }
221
+ // proceed normally
222
+ },
223
+ ```
224
+
225
+ ### Multiple commands from one trigger
226
+
227
+ ```typescript
228
+ eachMessage: async (message) => {
229
+ await handleFirstCommand(message.data.id, firstCommand);
230
+ await handleSecondCommand(message.data.id, secondCommand);
231
+ },
232
+ ```
233
+
234
+ ---
235
+
236
+ ## Files to create / modify
237
+
238
+ ```
239
+ src/slices/{context}/{SliceName}/
240
+ ├── {SliceName}Command.ts ← command handler (decide/evolve/handle) — see build-state-change
241
+ ├── {SliceName}.test.ts ← DeciderSpecification tests — see build-state-change
242
+ └── processor.ts ← reactor (start/stop)
243
+
244
+ src/
245
+ └── index.ts (or server.ts) ← register processor.start() / processor.stop()
246
+
247
+ src/common/
248
+ └── loadPostgresEventstore.ts ← verify schema.migrate() is called
249
+ ```
250
+
251
+ ---
252
+
253
+ ## Checklist
254
+
255
+ - [ ] `PROCESSOR_ID` is unique across all processors in the codebase (grep to verify)
256
+ - [ ] `processorInstanceId` uses `v4()` — not a hardcoded string
257
+ - [ ] `canHandle` matches the exact event type string from `{Context}Events.ts`
258
+ - [ ] `eachMessage` is wrapped in try/catch with `storeDlqMessage` fallback
259
+ - [ ] Processor registered in application startup (start + stop)
260
+ - [ ] `schema.migrate()` is called in `loadPostgresEventstore.ts`
261
+ - [ ] No `routes.ts` created (automations are not exposed via HTTP)
262
+ - [ ] Command handler tests cover idempotency (what happens if the command fires twice)
263
+ - [ ] Every processor in `processors[]` has a corresponding `processor.ts` implementation
264
+ - [ ] Command data fields map exclusively from fields available on the trigger event per slice.json — no invented mappings
265
+ - [ ] No filtering conditions were invented — all conditions come from slice.json `description` or `comments`
266
+ - [ ] No field names were assumed or guessed — if a field is not in slice.json, it is not in the code
@@ -0,0 +1,344 @@
1
+ ---
2
+ name: build-state-change
3
+ description: Implements an emmett state-change slice (command handler, tests, route) from a slice.json definition
4
+ ---
5
+
6
+ # Build State Change Slice
7
+
8
+ > Before doing anything else, read the slice definition from `.slices/{Context}/{slicename}/slice.json`. This file is the **source of truth** for all fields, events, and metadata. Never invent fields not defined there.
9
+
10
+ ---
11
+
12
+ ## What a State Change Slice is
13
+
14
+ A state-change slice processes a command using event sourcing. It:
15
+ 1. Loads the current aggregate state by replaying past events (`evolve`)
16
+ 2. Validates the command against that state (`decide`)
17
+ 3. Returns new events if valid, throws if not
18
+
19
+ ---
20
+
21
+ ## Step 1 — Read the slice.json
22
+
23
+ From the slice definition, extract:
24
+ - **sliceName** — the slice title (becomes the Command name)
25
+ - **context** — the bounded context (used to find `[Context]Events.ts`)
26
+ - **commands[]** — list of commands with their data fields
27
+ - **events[]** — list of events emitted by each command
28
+ - **specifications[]** — test scenarios (given/when/then)
29
+ > **Comments & description**: Each element (commands, events, readmodels, processors, screens, tables) carries a `comments: string[]` array (board comments on that node) and a `description` field. The slice itself also has `comments: string[]`. Use these as implementation hints — pass them as code comments, documentation, or validation logic where they add value. When done, resolve each used comment: `POST <BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes/<nodeId>/comments/<commentId>/resolve` (get comment IDs first via GET on the same path without the last two segments).
30
+
31
+
32
+ ---
33
+
34
+ ## Step 2 — Ensure the shared events union exists
35
+
36
+ Each context has one `[Context]Events.ts` file that exports a union of all event types.
37
+
38
+ File location: `src/slices/{context}/[Context]Events.ts` (or wherever the existing one lives — search for it).
39
+
40
+ ### Event type shape
41
+
42
+ ```typescript
43
+ import type {Event} from '@event-driven-io/emmett';
44
+
45
+ type CommonMeta = {
46
+ stream_name?: string;
47
+ userId?: string;
48
+ correlation_id?: string;
49
+ causation_id?: string;
50
+ };
51
+
52
+ export type {EventName} = Event<'{EventName}', {
53
+ // data fields from slice.json
54
+ }, CommonMeta>;
55
+
56
+ // Add the new event to the union
57
+ export type {Context}Events = /* existing events */ | {EventName};
58
+ ```
59
+
60
+ Add each new event type and update the union. Do NOT remove existing types.
61
+
62
+ ---
63
+
64
+ ## Step 3 — Create `{SliceName}Command.ts`
65
+
66
+ File: `src/slices/{context}/{SliceName}/{SliceName}Command.ts`
67
+
68
+ ### Full structure
69
+
70
+ ```typescript
71
+ import type {Command} from '@event-driven-io/emmett';
72
+ import {CommandHandler} from '@event-driven-io/emmett';
73
+ import {type {Context}Events} from '../{Context}Events';
74
+ import {findEventstore} from '../../../common/loadPostgresEventstore';
75
+
76
+ // 1. Command type — data fields come from slice.json commands[]
77
+ export type {SliceName}Command = Command<'{SliceName}', {
78
+ id: string;
79
+ // ... other data fields from the slice definition
80
+ }, {
81
+ correlation_id?: string;
82
+ causation_id?: string;
83
+ }>;
84
+
85
+ // 2. State — only fields needed for validation
86
+ export type {SliceName}State = {
87
+ // e.g. { processed: boolean } or { assignedIds: Set<string> }
88
+ // Use {} if no validation state is needed
89
+ };
90
+
91
+ export const {SliceName}InitialState = (): {SliceName}State => ({
92
+ // initial values
93
+ });
94
+
95
+ // 3. Evolve — pure function, updates state from past events
96
+ export const evolve = (
97
+ state: {SliceName}State,
98
+ event: {Context}Events,
99
+ ): {SliceName}State => {
100
+ const {type} = event;
101
+
102
+ switch (type) {
103
+ case '{EmittedEventName}':
104
+ return {...state, /* update field */};
105
+ default:
106
+ return state;
107
+ }
108
+ };
109
+
110
+ // 4. Decide — validates command, returns events or throws
111
+ export const decide = (
112
+ command: {SliceName}Command,
113
+ state: {SliceName}State,
114
+ ): {Context}Events[] => {
115
+ // idempotency / business rule check
116
+ if (state.processed) {
117
+ throw {code: 'already_processed', message: 'Already processed'};
118
+ }
119
+
120
+ return [{
121
+ type: '{EmittedEventName}',
122
+ data: {
123
+ id: command.data.id,
124
+ // ... map all fields from command.data per slice.json
125
+ },
126
+ metadata: {
127
+ correlation_id: command.metadata?.correlation_id,
128
+ causation_id: command.metadata?.causation_id,
129
+ userId: command.data.userId,
130
+ },
131
+ }];
132
+ };
133
+
134
+ // 5. CommandHandler + exported handle function
135
+ const {SliceName}CommandHandler = CommandHandler<{SliceName}State, {Context}Events>({
136
+ evolve,
137
+ initialState: {SliceName}InitialState,
138
+ });
139
+
140
+ export const handle{SliceName} = async (id: string, command: {SliceName}Command) => {
141
+ const eventStore = await findEventstore();
142
+ const result = await {SliceName}CommandHandler(
143
+ eventStore,
144
+ id,
145
+ (state: {SliceName}State) => decide(command, state),
146
+ );
147
+ return {
148
+ nextExpectedStreamVersion: result.nextExpectedStreamVersion,
149
+ lastEventGlobalPosition: result.lastEventGlobalPosition,
150
+ };
151
+ };
152
+ ```
153
+
154
+ ### State complexity guide
155
+
156
+ | Scenario | State shape |
157
+ |----------|-------------|
158
+ | Simple create-once | `{ created: boolean }` |
159
+ | Idempotency by user | `{ processedUserIds: Set<string> }` |
160
+ | Count validation | `{ count: number; limit: number }` |
161
+ | No validation needed | `{}` (empty object) |
162
+
163
+ ---
164
+
165
+ ## Step 4 — Create `{SliceName}.test.ts`
166
+
167
+ File: `src/slices/{context}/{SliceName}/{SliceName}.test.ts`
168
+
169
+ Use `DeciderSpecification` for unit tests. Derive test scenarios from `specifications[]` in the slice.json.
170
+
171
+ ```typescript
172
+ import {DeciderSpecification} from '@event-driven-io/emmett';
173
+ import {
174
+ {SliceName}Command,
175
+ {SliceName}InitialState,
176
+ decide,
177
+ evolve,
178
+ } from './{SliceName}Command';
179
+ import {describe, it} from 'node:test';
180
+
181
+ describe('{SliceName} Specification', () => {
182
+ const given = DeciderSpecification.for({
183
+ decide,
184
+ evolve,
185
+ initialState: {SliceName}InitialState,
186
+ });
187
+
188
+ it('spec: {SliceName} - creates event on empty stream', () => {
189
+ const command: {SliceName}Command = {
190
+ type: '{SliceName}',
191
+ data: {
192
+ id: 'test-id',
193
+ // ... test values
194
+ },
195
+ metadata: {},
196
+ };
197
+
198
+ given([])
199
+ .when(command)
200
+ .then([{
201
+ type: '{EmittedEventName}',
202
+ data: {
203
+ id: 'test-id',
204
+ // ... expected event data
205
+ },
206
+ metadata: {},
207
+ }]);
208
+ });
209
+
210
+ it('spec: {SliceName} - throws when already processed', () => {
211
+ const command: {SliceName}Command = {
212
+ type: '{SliceName}',
213
+ data: {id: 'test-id'},
214
+ metadata: {},
215
+ };
216
+
217
+ given([{
218
+ type: '{EmittedEventName}',
219
+ data: {id: 'test-id'},
220
+ metadata: {},
221
+ }])
222
+ .when(command)
223
+ .thenThrows();
224
+ });
225
+ });
226
+ ```
227
+
228
+ Add one test per specification in the slice.json. If the spec has no precondition events, use `given([])`.
229
+
230
+ ---
231
+
232
+ ## Step 5 — Create `routes.ts`
233
+
234
+ File: `src/slices/{context}/{SliceName}/routes.ts`
235
+
236
+ > **Concrete example**: `src/slices/example/routes.ts` — shows the full pattern with `requireUser`, `assertNotEmpty`, error mapping, and OpenAPI annotations. Read it before implementing.
237
+
238
+ ```typescript
239
+ import {Request, Response, Router} from 'express';
240
+ import {WebApiSetup} from '@event-driven-io/emmett-expressjs';
241
+ import {requireUser} from '../../../supabase/requireUser';
242
+ import {{SliceName}Command, handle{SliceName}} from './{SliceName}Command';
243
+
244
+ export const api = (): WebApiSetup => (router: Router): void => {
245
+
246
+ router.post('/api/{slicename}/:id', async (req: Request, res: Response) => {
247
+ const auth = await requireUser(req, res);
248
+ if (auth.error) return;
249
+
250
+ const id = req.params.id;
251
+ const correlationId = req.header('correlation_id') ?? id;
252
+
253
+ try {
254
+ const command: {SliceName}Command = {
255
+ type: '{SliceName}',
256
+ data: {
257
+ id,
258
+ // ... map from req.body
259
+ },
260
+ metadata: {
261
+ correlation_id: correlationId,
262
+ causation_id: id,
263
+ },
264
+ };
265
+
266
+ const result = await handle{SliceName}(id, command);
267
+
268
+ res.set('correlation_id', correlationId);
269
+ res.set('causation_id', id);
270
+
271
+ return res.status(201).json({
272
+ ok: true,
273
+ next_expected_stream_version: result.nextExpectedStreamVersion?.toString(),
274
+ last_event_global_position: result.lastEventGlobalPosition?.toString(),
275
+ });
276
+ } catch (err: any) {
277
+ const errorMessage = errorMapping(err?.code);
278
+ if (errorMessage) {
279
+ return res.status(409).json({error: errorMessage});
280
+ }
281
+ console.error(err);
282
+ return res.status(500).json({ok: false, error: 'Server error'});
283
+ }
284
+ });
285
+ };
286
+
287
+ const errorMapping = (code: string): string | null => {
288
+ switch (code) {
289
+ case 'already_processed': return 'This action has already been performed.';
290
+ // add other error codes from slice.json specifications
291
+ default: return null;
292
+ }
293
+ };
294
+ ```
295
+
296
+ ---
297
+
298
+ ## Step 6 — Wire up the route
299
+
300
+ Find the application's router registration (usually `src/index.ts` or `src/app.ts`) and add:
301
+
302
+ ```typescript
303
+ import {api as {SliceName}Api} from './slices/{context}/{SliceName}/routes';
304
+
305
+ // inside the router setup:
306
+ {SliceName}Api()(router);
307
+ ```
308
+
309
+ ---
310
+
311
+ ## Key patterns
312
+
313
+ - **Metadata optional chaining**: always use `command.metadata?.correlation_id` (metadata may be absent in tests)
314
+ - **Throw with code**: `throw {code: 'snake_case_code', message: '...'}` — routes catch by `err?.code`
315
+ - **Idempotency in evolve**: track processed IDs in state, check in decide
316
+ - **Stream ID**: pass the aggregate ID as the first argument to `handle{SliceName}(id, command)` — the stream is `{context}-{id}`
317
+ - **No side effects in evolve**: evolve must be a pure function; all side effects go in decide or the route
318
+
319
+ ---
320
+
321
+ ## Files to create
322
+
323
+ ```
324
+ src/slices/{context}/{SliceName}/
325
+ ├── {SliceName}Command.ts ← command handler (decide/evolve/handle)
326
+ ├── {SliceName}.test.ts ← DeciderSpecification tests
327
+ └── routes.ts ← Express POST endpoint
328
+
329
+ src/slices/{context}/
330
+ └── {Context}Events.ts ← add new event types here (update union)
331
+ ```
332
+
333
+ ---
334
+
335
+ ## Final Verification: Does the Implementation Match slice.json?
336
+
337
+ Before marking this slice as `Done`, verify the implementation against slice.json:
338
+
339
+ - [ ] Every field in `commands[].data` has a corresponding field in the Command type — no invented fields, none missing
340
+ - [ ] Every event in `events[]` has a corresponding type in `{Context}Events.ts` — names match exactly
341
+ - [ ] Every field in each event's data has a corresponding field in the TypeScript event type
342
+ - [ ] Every entry in `specifications[]` maps to a test case in `{SliceName}.test.ts`
343
+ - [ ] No business rules, defaults, or constraints were added that do not appear in slice.json `description` or `comments`
344
+ - [ ] No field names were assumed or guessed — if a field is not in slice.json, it is not in the code