@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,149 @@
1
+ <# : batch portion
2
+ @REM ----------------------------------------------------------------------------
3
+ @REM Licensed to the Apache Software Foundation (ASF) under one
4
+ @REM or more contributor license agreements. See the NOTICE file
5
+ @REM distributed with this work for additional information
6
+ @REM regarding copyright ownership. The ASF licenses this file
7
+ @REM to you under the Apache License, Version 2.0 (the
8
+ @REM "License"); you may not use this file except in compliance
9
+ @REM with the License. You may obtain a copy of the License at
10
+ @REM
11
+ @REM http://www.apache.org/licenses/LICENSE-2.0
12
+ @REM
13
+ @REM Unless required by applicable law or agreed to in writing,
14
+ @REM software distributed under the License is distributed on an
15
+ @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
+ @REM KIND, either express or implied. See the License for the
17
+ @REM specific language governing permissions and limitations
18
+ @REM under the License.
19
+ @REM ----------------------------------------------------------------------------
20
+
21
+ @REM ----------------------------------------------------------------------------
22
+ @REM Apache Maven Wrapper startup batch script, version 3.3.2
23
+ @REM
24
+ @REM Optional ENV vars
25
+ @REM MVNW_REPOURL - repo url base for downloading maven distribution
26
+ @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
27
+ @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
28
+ @REM ----------------------------------------------------------------------------
29
+
30
+ @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
31
+ @SET __MVNW_CMD__=
32
+ @SET __MVNW_ERROR__=
33
+ @SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
34
+ @SET PSModulePath=
35
+ @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
36
+ IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
37
+ )
38
+ @SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
39
+ @SET __MVNW_PSMODULEP_SAVE=
40
+ @SET __MVNW_ARG0_NAME__=
41
+ @SET MVNW_USERNAME=
42
+ @SET MVNW_PASSWORD=
43
+ @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
44
+ @echo Cannot start maven from wrapper >&2 && exit /b 1
45
+ @GOTO :EOF
46
+ : end batch / begin powershell #>
47
+
48
+ $ErrorActionPreference = "Stop"
49
+ if ($env:MVNW_VERBOSE -eq "true") {
50
+ $VerbosePreference = "Continue"
51
+ }
52
+
53
+ # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
54
+ $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
55
+ if (!$distributionUrl) {
56
+ Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
57
+ }
58
+
59
+ switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
60
+ "maven-mvnd-*" {
61
+ $USE_MVND = $true
62
+ $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
63
+ $MVN_CMD = "mvnd.cmd"
64
+ break
65
+ }
66
+ default {
67
+ $USE_MVND = $false
68
+ $MVN_CMD = $script -replace '^mvnw','mvn'
69
+ break
70
+ }
71
+ }
72
+
73
+ # apply MVNW_REPOURL and calculate MAVEN_HOME
74
+ # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
75
+ if ($env:MVNW_REPOURL) {
76
+ $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
77
+ $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
78
+ }
79
+ $distributionUrlName = $distributionUrl -replace '^.*/',''
80
+ $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
81
+ $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
82
+ if ($env:MAVEN_USER_HOME) {
83
+ $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
84
+ }
85
+ $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
86
+ $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
87
+
88
+ if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
89
+ Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
90
+ Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
91
+ exit $?
92
+ }
93
+
94
+ if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
95
+ Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
96
+ }
97
+
98
+ # prepare tmp dir
99
+ $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
100
+ $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
101
+ $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
102
+ trap {
103
+ if ($TMP_DOWNLOAD_DIR.Exists) {
104
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
105
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
106
+ }
107
+ }
108
+
109
+ New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
110
+
111
+ # Download and Install Apache Maven
112
+ Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
113
+ Write-Verbose "Downloading from: $distributionUrl"
114
+ Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
115
+
116
+ $webclient = New-Object System.Net.WebClient
117
+ if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
118
+ $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
119
+ }
120
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
121
+ $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
122
+
123
+ # If specified, validate the SHA-256 sum of the Maven distribution zip file
124
+ $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
125
+ if ($distributionSha256Sum) {
126
+ if ($USE_MVND) {
127
+ Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
128
+ }
129
+ Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
130
+ if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
131
+ Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
132
+ }
133
+ }
134
+
135
+ # unzip and move
136
+ Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
137
+ Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
138
+ try {
139
+ Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
140
+ } catch {
141
+ if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
142
+ Write-Error "fail to move MAVEN_HOME"
143
+ }
144
+ } finally {
145
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
146
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
147
+ }
148
+
149
+ Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
@@ -0,0 +1,152 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4
+ <modelVersion>4.0.0</modelVersion>
5
+ <groupId>com.example.quickstart</groupId>
6
+ <artifactId>quickstart</artifactId>
7
+ <name>Kurrent Quickstart</name>
8
+ <version>0.0.1-SNAPSHOT</version>
9
+
10
+ <properties>
11
+ <java.version>21</java.version>
12
+ <kurrentdb.version>1.2.1</kurrentdb.version>
13
+ <spring-boot.version>4.1.0</spring-boot.version>
14
+ <!-- Must track whatever version kurrentdb-client itself pulls in transitively (grpc-api is
15
+ runtime-scope only there) — check `mvn dependency:tree | grep grpc` after bumping
16
+ kurrentdb.version, and bump this to match. -->
17
+ <grpc.version>1.80.0</grpc.version>
18
+ </properties>
19
+
20
+ <dependencies>
21
+ <dependency>
22
+ <groupId>org.springframework.boot</groupId>
23
+ <artifactId>spring-boot-starter-webmvc</artifactId>
24
+ </dependency>
25
+
26
+ <!-- Classic Jackson 2 (com.fasterxml.jackson.core), for hand-serializing event payloads
27
+ (LibraryEventTypes and friends) with the same ObjectMapper the kurrentdb-client's own
28
+ samples use. Spring Boot 4 moved its own internal JSON handling to Jackson 3
29
+ (tools.jackson.core) and only pulls classic Jackson 2 in at *runtime* scope, transitively
30
+ via kurrentdb-client itself — not on the compile classpath by default. Declared explicitly,
31
+ version still resolved from Spring Boot's dependency-management BOM. -->
32
+ <dependency>
33
+ <groupId>com.fasterxml.jackson.core</groupId>
34
+ <artifactId>jackson-databind</artifactId>
35
+ </dependency>
36
+
37
+ <!-- Read model persistence: Spring Data JPA + PostgreSQL, default for all projections -->
38
+ <dependency>
39
+ <groupId>org.springframework.boot</groupId>
40
+ <artifactId>spring-boot-starter-data-jpa</artifactId>
41
+ </dependency>
42
+ <dependency>
43
+ <groupId>org.postgresql</groupId>
44
+ <artifactId>postgresql</artifactId>
45
+ <scope>runtime</scope>
46
+ </dependency>
47
+
48
+ <dependency>
49
+ <groupId>org.springframework.boot</groupId>
50
+ <artifactId>spring-boot-starter-actuator</artifactId>
51
+ </dependency>
52
+
53
+ <!-- KurrentDB Java client (plain SDK — no Spring Boot auto-configuration of its own,
54
+ see config.KurrentConfiguration for the beans this project wires by hand) -->
55
+ <dependency>
56
+ <groupId>io.kurrent</groupId>
57
+ <artifactId>kurrentdb-client</artifactId>
58
+ <version>${kurrentdb.version}</version>
59
+ </dependency>
60
+
61
+ <!-- io.grpc.Status/StatusRuntimeException — needed at compile scope to detect an
62
+ ALREADY_EXISTS conflict when idempotently creating a persistent-subscription group
63
+ (see build-state-view/build-automation). Only pulled in transitively by
64
+ kurrentdb-client at *runtime* scope otherwise. -->
65
+ <dependency>
66
+ <groupId>io.grpc</groupId>
67
+ <artifactId>grpc-api</artifactId>
68
+ <version>${grpc.version}</version>
69
+ </dependency>
70
+
71
+ <!-- Auto-starts docker-compose.yml services (KurrentDB, Postgres) on app run -->
72
+ <dependency>
73
+ <groupId>org.springframework.boot</groupId>
74
+ <artifactId>spring-boot-docker-compose</artifactId>
75
+ <scope>runtime</scope>
76
+ <optional>true</optional>
77
+ </dependency>
78
+
79
+ <!-- Testing -->
80
+ <dependency>
81
+ <groupId>org.springframework.boot</groupId>
82
+ <artifactId>spring-boot-starter-test</artifactId>
83
+ <scope>test</scope>
84
+ </dependency>
85
+ <!-- Spring Boot 4 test slices: @DataJpaTest and @WebMvcTest each moved to their own artifact -->
86
+ <dependency>
87
+ <groupId>org.springframework.boot</groupId>
88
+ <artifactId>spring-boot-data-jpa-test</artifactId>
89
+ <scope>test</scope>
90
+ </dependency>
91
+ <dependency>
92
+ <groupId>org.springframework.boot</groupId>
93
+ <artifactId>spring-boot-webmvc-test</artifactId>
94
+ <scope>test</scope>
95
+ </dependency>
96
+ <dependency>
97
+ <groupId>org.junit.jupiter</groupId>
98
+ <artifactId>junit-jupiter</artifactId>
99
+ <scope>test</scope>
100
+ </dependency>
101
+ <dependency>
102
+ <groupId>org.springframework.boot</groupId>
103
+ <artifactId>spring-boot-testcontainers</artifactId>
104
+ <scope>test</scope>
105
+ </dependency>
106
+ <dependency>
107
+ <groupId>org.testcontainers</groupId>
108
+ <artifactId>testcontainers</artifactId>
109
+ <scope>test</scope>
110
+ </dependency>
111
+ <dependency>
112
+ <groupId>org.testcontainers</groupId>
113
+ <artifactId>testcontainers-postgresql</artifactId>
114
+ <scope>test</scope>
115
+ </dependency>
116
+ <dependency>
117
+ <groupId>org.testcontainers</groupId>
118
+ <artifactId>testcontainers-junit-jupiter</artifactId>
119
+ <scope>test</scope>
120
+ </dependency>
121
+ </dependencies>
122
+
123
+ <build>
124
+ <plugins>
125
+ <plugin>
126
+ <groupId>org.apache.maven.plugins</groupId>
127
+ <artifactId>maven-compiler-plugin</artifactId>
128
+ <configuration>
129
+ <source>${java.version}</source>
130
+ <target>${java.version}</target>
131
+ <parameters>true</parameters>
132
+ </configuration>
133
+ </plugin>
134
+ <plugin>
135
+ <groupId>org.springframework.boot</groupId>
136
+ <artifactId>spring-boot-maven-plugin</artifactId>
137
+ </plugin>
138
+ </plugins>
139
+ </build>
140
+
141
+ <dependencyManagement>
142
+ <dependencies>
143
+ <dependency>
144
+ <groupId>org.springframework.boot</groupId>
145
+ <artifactId>spring-boot-dependencies</artifactId>
146
+ <version>${spring-boot.version}</version>
147
+ <type>pom</type>
148
+ <scope>import</scope>
149
+ </dependency>
150
+ </dependencies>
151
+ </dependencyManagement>
152
+ </project>
@@ -0,0 +1,12 @@
1
+ package com.example.quickstart;
2
+
3
+ import org.springframework.boot.SpringApplication;
4
+ import org.springframework.boot.autoconfigure.SpringBootApplication;
5
+
6
+ @SpringBootApplication
7
+ public class QuickstartApplication {
8
+
9
+ public static void main(String[] args) {
10
+ SpringApplication.run(QuickstartApplication.class, args);
11
+ }
12
+ }
@@ -0,0 +1,91 @@
1
+ package com.example.quickstart.common;
2
+
3
+ import io.kurrent.dbclient.*;
4
+ import org.springframework.stereotype.Component;
5
+
6
+ import java.util.List;
7
+ import java.util.concurrent.ExecutionException;
8
+
9
+ /**
10
+ * Thin wrapper around {@link KurrentDBClient} shared by every write slice — reads a stream into a
11
+ * plain list (or empty, if the stream doesn't exist yet) and appends events, translating the client's
12
+ * checked/wrapped exceptions into unchecked ones so slice code doesn't repeat this boilerplate.
13
+ *
14
+ * <p>This project has no CQRS framework layer (unlike the OpenCQRS/Axon kits) — this class, plus each
15
+ * slice's own {@code decide}/{@code evolve} functions, is the entire "framework". See
16
+ * {@code .build-kit/CLAUDE.md} for the full convention.
17
+ */
18
+ @Component
19
+ public class EventStore {
20
+
21
+ private final KurrentDBClient client;
22
+
23
+ public EventStore(KurrentDBClient client) {
24
+ this.client = client;
25
+ }
26
+
27
+ /**
28
+ * Reads every event in {@code streamId}, forwards from the start. Returns
29
+ * {@link StreamEvents#empty()} if the stream doesn't exist yet — this is the normal, expected case
30
+ * for a creation command, not an error.
31
+ */
32
+ public StreamEvents read(String streamId) {
33
+ try {
34
+ ReadResult result = client.readStream(streamId, ReadStreamOptions.get().forwards().fromStart()).get();
35
+ return new StreamEvents(result.getEvents(), result.getLastStreamPosition());
36
+ } catch (ExecutionException e) {
37
+ if (e.getCause() instanceof StreamNotFoundException) {
38
+ return StreamEvents.empty();
39
+ }
40
+ throw new EventStoreException(e.getCause());
41
+ } catch (InterruptedException e) {
42
+ Thread.currentThread().interrupt();
43
+ throw new EventStoreException(e);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Appends {@code events} to {@code streamId} under the given {@code expectedState} (see
49
+ * {@link StreamState#noStream()}/{@link StreamState#streamRevision(long)}/etc). Throws
50
+ * {@link WrongExpectedVersionException} directly (unwrapped) on an optimistic-concurrency
51
+ * conflict — callers decide for themselves whether that's a real conflict (map it to HTTP 409) or
52
+ * an expected redelivery to swallow (see build-automation's SKILL.md).
53
+ */
54
+ public WriteResult append(String streamId, StreamState expectedState, List<EventData> events) {
55
+ try {
56
+ AppendToStreamOptions options = AppendToStreamOptions.get().streamState(expectedState);
57
+ return client.appendToStream(streamId, options, events.toArray(new EventData[0])).get();
58
+ } catch (ExecutionException e) {
59
+ if (e.getCause() instanceof WrongExpectedVersionException wrongExpectedVersion) {
60
+ throw wrongExpectedVersion;
61
+ }
62
+ throw new EventStoreException(e.getCause());
63
+ } catch (InterruptedException e) {
64
+ Thread.currentThread().interrupt();
65
+ throw new EventStoreException(e);
66
+ }
67
+ }
68
+
69
+ public record StreamEvents(List<ResolvedEvent> events, long lastStreamPosition) {
70
+ private static final long NO_STREAM_POSITION = -1;
71
+
72
+ public static StreamEvents empty() {
73
+ return new StreamEvents(List.of(), NO_STREAM_POSITION);
74
+ }
75
+
76
+ public boolean isEmpty() {
77
+ return events.isEmpty();
78
+ }
79
+
80
+ /** The {@link StreamState} to append under, given this stream's current position. */
81
+ public StreamState expectedState() {
82
+ return isEmpty() ? StreamState.noStream() : StreamState.streamRevision(lastStreamPosition);
83
+ }
84
+ }
85
+
86
+ public static class EventStoreException extends RuntimeException {
87
+ public EventStoreException(Throwable cause) {
88
+ super(cause);
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,42 @@
1
+ package com.example.quickstart.config;
2
+
3
+ import io.kurrent.dbclient.ConnectionStringParsingException;
4
+ import io.kurrent.dbclient.KurrentDBClient;
5
+ import io.kurrent.dbclient.KurrentDBClientSettings;
6
+ import io.kurrent.dbclient.KurrentDBConnectionString;
7
+ import io.kurrent.dbclient.KurrentDBPersistentSubscriptionsClient;
8
+ import org.springframework.beans.factory.annotation.Value;
9
+ import org.springframework.context.annotation.Bean;
10
+ import org.springframework.context.annotation.Configuration;
11
+
12
+ /**
13
+ * The KurrentDB Java client ({@code kurrentdb-client}) ships no Spring Boot auto-configuration of its
14
+ * own (unlike OpenCQRS's {@code framework-spring-boot-starter}) — it is a plain SDK. These beans are
15
+ * this project's own wiring, not a vendor-provided starter.
16
+ */
17
+ @Configuration
18
+ public class KurrentConfiguration {
19
+
20
+ @Value("${kurrentdb.connection-string}")
21
+ private String connectionString;
22
+
23
+ /**
24
+ * A single client instance is safe to share as a singleton across the whole application — it does
25
+ * not need to be opened/closed per request (see the client's own getting-started guide).
26
+ */
27
+ @Bean(destroyMethod = "shutdown")
28
+ public KurrentDBClient kurrentDBClient() throws ConnectionStringParsingException {
29
+ KurrentDBClientSettings settings = KurrentDBConnectionString.parseOrThrow(connectionString);
30
+ return KurrentDBClient.create(settings);
31
+ }
32
+
33
+ /**
34
+ * Separate client for managing/consuming persistent subscriptions — used by build-state-view and
35
+ * build-automation slices, not by build-state-change (which only appends/reads streams directly).
36
+ */
37
+ @Bean(destroyMethod = "shutdown")
38
+ public KurrentDBPersistentSubscriptionsClient kurrentDBPersistentSubscriptionsClient() throws ConnectionStringParsingException {
39
+ KurrentDBClientSettings settings = KurrentDBConnectionString.parseOrThrow(connectionString);
40
+ return KurrentDBPersistentSubscriptionsClient.create(settings);
41
+ }
42
+ }
@@ -0,0 +1,14 @@
1
+ spring.application.name=quickstart
2
+ server.port=8080
3
+
4
+ kurrentdb.connection-string=${KURRENTDB_CONNECTION_STRING:kurrentdb://localhost:2113?tls=false}
5
+
6
+ spring.datasource.url=jdbc:postgresql://localhost:5432/quickstart
7
+ spring.datasource.username=quickstart
8
+ spring.datasource.password=quickstart
9
+ spring.jpa.hibernate.ddl-auto=update
10
+
11
+ spring.docker.compose.enabled=true
12
+ spring.docker.compose.file=docker-compose.yml
13
+
14
+ management.endpoint.health.show-components=always
@@ -0,0 +1,11 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Kurrent Quickstart</title>
6
+ </head>
7
+ <body>
8
+ <h1>Kurrent Quickstart</h1>
9
+ <p>Backend is running. Slices built from the event model board are wired up under their own REST paths.</p>
10
+ </body>
11
+ </html>
@@ -41,7 +41,7 @@ automation slice = EVENT(s) → AUTOMATION → COMMAND → EVENT(s)
41
41
 
42
42
  A slice never mixes a COMMAND and a READMODEL — those are two different slice types (`state-change` and `state-view`), even when they belong to the same feature. Slices are independently deployable and only communicate with each other via events.
43
43
 
44
- Slices are not placed directly — they're **derived** from a completed model. Every COMMAND on the board implies a `state-change` slice, every READMODEL implies a `state-view` slice, every AUTOMATION implies an `automation` slice. `eventmodeling-slicing-event-models` makes that implicit structure explicit on the board once modeling is done; it never invents a slice that isn't already implied by an element that's there.
44
+ Slices are not placed directly — they're **derived** from a completed model. Every COMMAND on the board implies a `state-change` slice, every READMODEL implies a `state-view` slice, every AUTOMATION implies an `automation` slice — **except** a `linkedTo` copy (see Linked Copies below), which implies no slice of its own: it's the same underlying COMMAND/EVENT/READMODEL reused elsewhere on the board, not a second independently-deployable thing, so only the origin node's column gets a `SLICE_BORDER`. `eventmodeling-slicing-event-models` makes that implicit structure explicit on the board once modeling is done; it never invents a slice that isn't already implied by an element that's there, and skips columns whose only COMMAND/READMODEL is a linked copy.
45
45
 
46
46
  ## Elements
47
47
 
@@ -116,7 +116,7 @@ When an element's natural column is already occupied by something else, insert a
116
116
 
117
117
  A later EVENT sometimes needs to change data an earlier READMODEL already shows on a SCREEN — e.g. a cancellation affecting an "active items" view placed several columns earlier. Wiring that later EVENT straight back into the existing READMODEL is never the fix: it's a backward connection, and the platform only accepts a backward `EVENT → READMODEL` edge when the READMODEL already has a `READMODEL → AUTOMATION` edge (the todo-list pattern — see Translation Chain below). A READMODEL that only feeds a SCREEN never qualifies for that exemption.
118
118
 
119
- Instead, place a **new READMODEL node** in a new column **immediately after** the later event's column, connect that event forward into it, and give it a matching SCREEN in that same new column — same screen name/title as the earlier one, showing the updated data, optionally re-marked/highlighted via `html-screen`'s Marks feature to call out what changed. Leave the earlier READMODEL/SCREEN instance exactly as it is it's still what that earlier point in the timeline correctly showed. Never delete it, and never wire the new instance back to it: this isn't the `linkedTo` copy mechanism below (that's for reusing one fact across timelines) — it's a genuinely new node representing a genuinely later state.
119
+ Instead, place a **new READMODEL node** in a new column **immediately after** the later event's column, then link it to the earlier READMODEL instance via the `linkedTo` copy mechanism below (`link_element`) — it *is* the same read model, carried forward to a later state, not an unrelated new one, so it uses the same copy mechanism as any other reused fact. Connect the later event forward into the linked copy, then update whichever field(s) that event actually changes (e.g. `status`) on the copy — the fields it inherited from the link that the new event doesn't touch stay as copied. Give it a matching SCREEN in that same new column — same screen name/title as the earlier one, showing the updated data, optionally re-marked/highlighted via `html-screen`'s Marks feature to call out what changed. A SCREEN can never itself be a `linkedTo` copy (the mechanism only supports COMMAND/EVENT/READMODEL), so the new SCREEN is placed as a plain node matching the earlier one's title, not linked. Leave the earlier READMODEL/SCREEN instance exactly as it is — it's still what that earlier point in the timeline correctly showed. Never delete it.
120
120
 
121
121
  See `eventmodeling-identifying-outputs` Step 5g for the full placement/wiring mechanics, and `place-element` Step 6 for the column-insertion mechanics.
122
122
 
@@ -126,6 +126,8 @@ A **linked copy** is a COMMAND/EVENT/READMODEL node that mirrors another node el
126
126
 
127
127
  To make one: place a normal new node of the same type (COMMAND/EVENT/READMODEL only) at the target spot, then link it to the origin (`link_element`, or the REST `.../nodes/:nodeId/link` fallback). This replaces the new node's `meta` with a full copy of the origin's and sets `meta.linkedTo` to the origin's node id — the authoritative pointer (a `data.linkedTo` also exists but is a rendering mirror only, not the source of truth). Wire the resulting copy to its own neighbors normally afterward.
128
128
 
129
+ A linked copy is allowed to diverge from its origin afterward in the specific field(s) a newly-connected event actually drives — e.g. the "Updating a Read Model That Already Feeds a Screen" copy above, where `status` is updated post-link to the event's new value. The link establishes "this is the same underlying fact, not a duplicate" at creation time; it doesn't freeze the copy's fields identical to the origin's forever.
130
+
129
131
  Never delete the original (the node with no `linkedTo`) once copies of it exist — copies reference it, and removing it breaks every copy. `eventmodeling-checking-completeness` already treats any `linkedTo`-marked node as an intentional copy, never a duplicate or missing-slice gap to flag.
130
132
 
131
133
  See `place-element` Step 6a for the mechanical linking steps.
@@ -314,7 +314,7 @@ For each view screen S that queries this read model as its primary read model:
314
314
 
315
315
  **View screens normally share the column of the (primary) read model they display** — either because they were placed there in Step 3, or because the read model was placed into the screen's own column just now (per the check above). A screen's own position is never moved to resolve a read model placement conflict — when the shared column isn't available, the **read model** gets a new column immediately before the screen's, not the other way around (this preserves the screen's narrative order from Step 3).
316
316
 
317
- **The same rule applies to `EVENT → READMODEL`** (shared statement: `eventmodeling-core-rules`'s "Updating a Read Model That Already Feeds a Screen"). If a later event needs to update data a read model already feeds to a SCREEN, do not connect that later event back into the existing read model — the platform only accepts an `EVENT → READMODEL` backward connection when the read model already has a `READMODEL → AUTOMATION` edge (the todo-list pattern from Step 4b, `eventmodeling-designing-automation-chains`). For any read model feeding a SCREEN, resolve the update the same way Step 5c resolves multi-component screens: place a **new copy of the read model** in a new column immediately after the later event's column, connect the later event forward into that copy, and place a matching copy of the same screen there — same title, updated data, optionally re-marked/highlighted via `html-screen`'s Marks feature. Never link the new copy back to the earlier read model instance.
317
+ **The same rule applies to `EVENT → READMODEL`** (shared statement: `eventmodeling-core-rules`'s "Updating a Read Model That Already Feeds a Screen"). If a later event needs to update data a read model already feeds to a SCREEN, do not connect that later event back into the existing read model — the platform only accepts an `EVENT → READMODEL` backward connection when the read model already has a `READMODEL → AUTOMATION` edge (the todo-list pattern from Step 4b, `eventmodeling-designing-automation-chains`). For any read model feeding a SCREEN, resolve the update the same way Step 5c resolves multi-component screens: place a **new READMODEL node** in a new column immediately after the later event's column, then link it to the earlier read model instance (`link_element` — it's the same read model carried forward, not an unrelated new one; see `eventmodeling-core-rules`'s Linked Copies section). Connect the later event forward into the linked copy, then update the field(s) that event actually changes. Place a matching SCREEN in that same new column — same title, updated data, optionally re-marked/highlighted via `html-screen`'s Marks feature; a SCREEN can't be a `linkedTo` copy itself (COMMAND/EVENT/READMODEL only), so place it as a plain node matching the earlier screen's title.
318
318
 
319
319
  ### Step 5h — Wire connections after placing each READMODEL (and its SCREEN)
320
320
 
@@ -122,7 +122,7 @@ The timeline must always progress left-to-right — this is the goal to design t
122
122
 
123
123
  **`EVENT → READMODEL` has exactly one exception, and it is narrow.** A read model that already carries a `READMODEL → AUTOMATION` edge — i.e. a todo-list read model feeding an automation, per `eventmodeling-designing-automation-chains` (Step 4b) — may also be fed by a later-column event closing an item it opened earlier. That accumulator shape is what the todo-list pattern exists for, and it is confirmed against the platform API (`learn-eventmodelers-api` §3 — `POST .../connections`). **Outside that one case, the platform rejects the connection, for good reason:** without it, a read model would become a moving target for whatever screen or scenario later reaches back into it.
124
124
 
125
- For every other read model — in particular one feeding a SCREEN rather than an AUTOMATION — never connect a later event back into it, no matter how convenient. If a later event needs to update what a screen already shows, resolve it the same way Step 5c resolves multi-component screens: place a **new copy of the read model** in a new column immediately after the later event's column, connect the later event forward into that copy, and place a matching copy of the same screen there (same title, updated data, optionally re-marked/highlighted per `html-screen`'s Marks feature). Never link the new copy back to the earlier instance.
125
+ For every other read model — in particular one feeding a SCREEN rather than an AUTOMATION — never connect a later event back into it, no matter how convenient. If a later event needs to update what a screen already shows, resolve it the same way Step 5c resolves multi-component screens: place a **new READMODEL node** in a new column immediately after the later event's column, link it to the earlier instance (`link_element` — it's the same read model, not an unrelated new one; see `eventmodeling-core-rules`'s Linked Copies section), connect the later event forward into the linked copy, update the field(s) that event actually changes, and place a matching copy of the same screen there (same title, updated data, optionally re-marked/highlighted per `html-screen`'s Marks feature a SCREEN can't itself be linked, so it's a plain node matching the earlier one's title).
126
126
 
127
127
  A wide fan-in read model (many connected events, one column) is a different problem with a different fix — see "one read model per component" and the >3-events heuristic above. It is never a justification for a backward arrow. The one real signal to treat as a prunable connection regardless of column position is a **connected event that isn't actually used by any field** on the read model.
128
128
 
@@ -39,6 +39,8 @@ Walk the timeline column by column:
39
39
  - Every **READMODEL** → one `state-view` slice, named after the read model
40
40
  - Every **AUTOMATION** → one `automation` slice, named after the automation (or the command it issues)
41
41
 
42
+ **Skip any column whose only COMMAND/READMODEL node has `meta.linkedTo` set** — it's a linked copy of a node elsewhere on the board (see `eventmodeling-core-rules`'s Linked Copies and Slices sections), not a second independently-deployable thing. Only the origin node's own column gets a slice.
43
+
42
44
  ---
43
45
 
44
46
  ## Step 1: Resolve the Timeline
@@ -226,7 +226,7 @@ Use this REST call (see the curl fallback below) when you don't already have fre
226
226
  | Any | Same element type | Stop and tell the user — true conflict, no safe default. |
227
227
  | Any | Different type but not a known pairing | Stop and tell the user. |
228
228
 
229
- **A later EVENT that needs to update a READMODEL already feeding a SCREEN is not a Step 6 occupancy case — it's a backward-connection trap.** Do not `set_connection` that EVENT into the existing READMODEL, even if the existing READMODEL's column is reachable. See `eventmodeling-core-rules`'s "Updating a Read Model That Already Feeds a Screen": place a **new** READMODEL (and a matching SCREEN) in a new column immediately after the event's column instead, and connect the event forward into that new pair.
229
+ **A later EVENT that needs to update a READMODEL already feeding a SCREEN is not a Step 6 occupancy case — it's a backward-connection trap.** Do not `set_connection` that EVENT into the existing READMODEL, even if the existing READMODEL's column is reachable. See `eventmodeling-core-rules`'s "Updating a Read Model That Already Feeds a Screen": place a **new** READMODEL in a new column immediately after the event's column instead, link it to the existing READMODEL as a `linkedTo` copy (Step 6a below — it's the same read model, not an unrelated new one), connect the event forward into the linked copy, and give it a matching SCREEN in the same column (a plain node, not linked — SCREEN isn't a linkable type).
230
230
 
231
231
  **Insert immediately after** means: create the new column right after the current one, not by appending to the end. This keeps the read model visually adjacent to the event that drives it.
232
232
 
@@ -57,6 +57,28 @@ When asked to build a slice, always follow this flow:
57
57
 
58
58
  After you are done, automatically run the tests for the slice that was edited.
59
59
 
60
+ ## Commit Scope Guard
61
+
62
+ A pre-commit hook (`.githooks/pre-commit`, installed via `init --hooks` — see its own project's setup)
63
+ runs `.build-kit/lib/check-commit-scope.cjs` on every commit that touches `src/slices/{context}/{slice}/`.
64
+ It loads every check under `.build-kit/lib/checks/` and rejects the commit if any of them find a problem:
65
+
66
+ - **blocked-paths** — `package.json`/lockfiles and `server.ts` are never touched by slice work
67
+ - **slice-scope** — everything staged must be inside the slice folder or a documented exception:
68
+ `src/slices/{context}/{Context}Events.ts` or `src/common/loadPostgresEventstore.ts`
69
+ - **append-only-migrations** — `migrations/V{n}__*.sql` may only be **added**, never edited
70
+ - **test-file-present** — a changed Command/Projection/processor file needs a sibling `*.test.ts`
71
+ - **no-invented-fields** — heuristic: flags a field used in code that isn't declared anywhere in
72
+ `.build-kit/.slices/{context}/{slice}/slice.json`
73
+ - **spec-coverage** — heuristic: the test file needs at least as many `it(...)` blocks as slice.json
74
+ has `specifications[]` entries
75
+ - **tsc-build** — `npx tsc --noEmit` must still pass
76
+
77
+ If a commit is rejected, split it — commit the out-of-scope file separately from the slice work, or add
78
+ the missing test/fix the field — rather than passing `--no-verify`. Run `npm run check:scope` any time
79
+ you want to check staged files before committing. To add a new check, read
80
+ `.build-kit/lib/checks/README.md` and drop in a file following its interface — no other wiring needed.
81
+
60
82
  ## Example Slice Structure
61
83
 
62
84
  ```