@eventmodelers/cli 1.0.45 → 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 (102) 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/node/templates/build-kit/CLAUDE.md +22 -0
  32. package/stacks/node/templates/build-kit/lib/check-commit-scope.cjs +123 -0
  33. package/stacks/node/templates/build-kit/lib/checks/00-blocked-paths.cjs +28 -0
  34. package/stacks/node/templates/build-kit/lib/checks/10-slice-scope.cjs +29 -0
  35. package/stacks/node/templates/build-kit/lib/checks/20-append-only-migrations.cjs +20 -0
  36. package/stacks/node/templates/build-kit/lib/checks/30-test-file-present.cjs +44 -0
  37. package/stacks/node/templates/build-kit/lib/checks/40-no-invented-fields.cjs +92 -0
  38. package/stacks/node/templates/build-kit/lib/checks/50-spec-coverage.cjs +50 -0
  39. package/stacks/node/templates/build-kit/lib/checks/90-tsc-build.cjs +22 -0
  40. package/stacks/node/templates/root/.githooks/pre-commit +11 -0
  41. package/stacks/node/templates/root/package.json +2 -1
  42. package/stacks/node/templates/root/setup-env.sh +7 -1
  43. package/stacks/opencqrs/templates/.claude/skills/build-automation/SKILL.md +434 -0
  44. package/stacks/opencqrs/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +19 -0
  45. package/stacks/opencqrs/templates/.claude/skills/build-automation/references/idempotent-dispatch-patterns.md +62 -0
  46. package/stacks/opencqrs/templates/.claude/skills/build-state-change/SKILL.md +413 -0
  47. package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +46 -0
  48. package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/rest-api-patterns.md +145 -0
  49. package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/test-fixture-patterns.md +85 -0
  50. package/stacks/opencqrs/templates/.claude/skills/build-state-view/SKILL.md +354 -0
  51. package/stacks/opencqrs/templates/build-kit/CLAUDE.md +101 -0
  52. package/stacks/opencqrs/templates/build-kit/lib/AGENT.md +58 -0
  53. package/stacks/opencqrs/templates/build-kit/lib/backend-prompt.md +169 -0
  54. package/stacks/opencqrs/templates/build-kit/lib/prompt.md +128 -0
  55. package/stacks/opencqrs/templates/root/README.md +42 -0
  56. package/stacks/opencqrs/templates/root/docker-compose.yml +37 -0
  57. package/stacks/opencqrs/templates/root/mvnw +259 -0
  58. package/stacks/opencqrs/templates/root/mvnw.cmd +149 -0
  59. package/stacks/opencqrs/templates/root/pom.xml +139 -0
  60. package/stacks/opencqrs/templates/root/src/main/java/com/example/quickstart/QuickstartApplication.java +12 -0
  61. package/stacks/opencqrs/templates/root/src/main/java/com/example/quickstart/config/CqrsConfiguration.java +74 -0
  62. package/stacks/opencqrs/templates/root/src/main/resources/application.properties +23 -0
  63. package/stacks/opencqrs/templates/root/src/main/resources/schema.sql +19 -0
  64. package/stacks/opencqrs/templates/root/src/main/resources/static/index.html +11 -0
  65. package/stacks/supabase/templates/build-kit/CLAUDE.md +25 -0
  66. package/stacks/supabase/templates/build-kit/lib/check-commit-scope.cjs +126 -0
  67. package/stacks/supabase/templates/build-kit/lib/checks/00-blocked-paths.cjs +28 -0
  68. package/stacks/supabase/templates/build-kit/lib/checks/10-slice-scope.cjs +34 -0
  69. package/stacks/supabase/templates/build-kit/lib/checks/20-append-only-migrations.cjs +21 -0
  70. package/stacks/supabase/templates/build-kit/lib/checks/30-test-file-present.cjs +44 -0
  71. package/stacks/supabase/templates/build-kit/lib/checks/40-no-invented-fields.cjs +92 -0
  72. package/stacks/supabase/templates/build-kit/lib/checks/50-spec-coverage.cjs +50 -0
  73. package/stacks/supabase/templates/build-kit/lib/checks/90-tsc-build.cjs +22 -0
  74. package/stacks/supabase/templates/root/.githooks/pre-commit +11 -0
  75. package/stacks/supabase/templates/root/package.json +2 -1
  76. package/stacks/supabase/templates/root/setup-env.sh +7 -1
  77. package/stacks/umadb/templates/.claude/skills/build-automation/SKILL.md +313 -0
  78. package/stacks/umadb/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +42 -0
  79. package/stacks/umadb/templates/.claude/skills/build-state-change/SKILL.md +376 -0
  80. package/stacks/umadb/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +42 -0
  81. package/stacks/umadb/templates/.claude/skills/build-state-change/references/umadb-query-patterns.md +78 -0
  82. package/stacks/umadb/templates/.claude/skills/build-state-view/SKILL.md +338 -0
  83. package/stacks/umadb/templates/build-kit/CLAUDE.md +94 -0
  84. package/stacks/umadb/templates/build-kit/lib/AGENT.md +47 -0
  85. package/stacks/umadb/templates/build-kit/lib/backend-prompt.md +169 -0
  86. package/stacks/umadb/templates/build-kit/lib/prompt.md +128 -0
  87. package/stacks/umadb/templates/root/.mvn/wrapper/maven-wrapper.properties +19 -0
  88. package/stacks/umadb/templates/root/README.md +48 -0
  89. package/stacks/umadb/templates/root/docker-compose.yml +29 -0
  90. package/stacks/umadb/templates/root/mvnw +259 -0
  91. package/stacks/umadb/templates/root/mvnw.cmd +149 -0
  92. package/stacks/umadb/templates/root/pom.xml +151 -0
  93. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/QuickstartApplication.java +12 -0
  94. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/config/UmaDbConfig.java +39 -0
  95. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/DecisionModelLoader.java +76 -0
  96. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/EventCodec.java +40 -0
  97. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/EventDispatcher.java +100 -0
  98. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/OptimisticConcurrencyException.java +14 -0
  99. package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/SliceEventListener.java +23 -0
  100. package/stacks/umadb/templates/root/src/main/resources/application.properties +17 -0
  101. package/stacks/umadb/templates/root/src/test/java/io/umadb/quickstart/testsupport/InMemoryUmaDbClient.java +135 -0
  102. 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>
@@ -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
  ```
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Runner for the slice commit-scope guard. Loads every check module from
5
+ // ./checks/*.cjs and runs it against the currently staged changeset.
6
+ //
7
+ // Check interface (see ./checks/README.md for the full contract + a template):
8
+ // module.exports = {
9
+ // name: 'my-check', // short id, shown in violation output
10
+ // skipIfAlreadyFailing: false, // optional — skip this check once an earlier
11
+ // // one has already failed (use for slow checks)
12
+ // run(ctx) {
13
+ // return [{ path: 'some/file.ts', reason: 'why this is a problem' }];
14
+ // },
15
+ // };
16
+ // `run()` returns an array of violations (empty array/undefined/null = pass).
17
+ //
18
+ // `ctx` passed to every check:
19
+ // changes [{status, path}] — staged files (git status letter + path)
20
+ // touchesSlice true — this commit touches src/slices/{context}/{slice}/**
21
+ // (the runner already gates on this before loading checks)
22
+ // repoRoot absolute path to the repo root
23
+ // SLICE_PATTERN RegExp matching a path inside a slice's own folder
24
+ //
25
+ // Zero dependencies — plain Node, so it works from git's pre-commit hook
26
+ // (see ../../.githooks/pre-commit), from `npm run check:scope`, or from CI.
27
+ // Invoked as: node .build-kit/lib/check-commit-scope.cjs
28
+
29
+ const { execSync } = require('child_process');
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+
33
+ const SLICE_PATTERN = /^src\/slices\/[^/]+\/[^/]+\//;
34
+
35
+ function stagedChanges() {
36
+ const out = execSync('git diff --cached --name-status --no-renames', { encoding: 'utf8' });
37
+ return out
38
+ .split('\n')
39
+ .filter(Boolean)
40
+ .map((line) => {
41
+ const [status, ...rest] = line.split('\t');
42
+ return { status: status[0], path: rest.join('\t') };
43
+ });
44
+ }
45
+
46
+ function loadChecks() {
47
+ const checksDir = path.join(__dirname, 'checks');
48
+ if (!fs.existsSync(checksDir)) return [];
49
+ return fs
50
+ .readdirSync(checksDir)
51
+ .filter((f) => f.endsWith('.cjs'))
52
+ .sort() // numeric filename prefixes (00-, 10-, ...) control run order
53
+ .map((f) => {
54
+ let mod;
55
+ try {
56
+ mod = require(path.join(checksDir, f));
57
+ } catch (err) {
58
+ console.error(`check-commit-scope: failed to load checks/${f} — ${err.message}`);
59
+ return null;
60
+ }
61
+ if (typeof mod?.run !== 'function') {
62
+ console.error(`check-commit-scope: skipping checks/${f} — does not export { name, run(ctx) }`);
63
+ return null;
64
+ }
65
+ return { file: f, name: mod.name || f, run: mod.run, skipIfAlreadyFailing: !!mod.skipIfAlreadyFailing };
66
+ })
67
+ .filter(Boolean);
68
+ }
69
+
70
+ function main() {
71
+ let changes;
72
+ try {
73
+ changes = stagedChanges();
74
+ } catch (err) {
75
+ console.error('check-commit-scope: could not read staged changes —', err.message);
76
+ process.exit(1);
77
+ }
78
+
79
+ if (changes.length === 0) process.exit(0);
80
+
81
+ const touchesSlice = changes.some((c) => SLICE_PATTERN.test(c.path));
82
+ if (!touchesSlice) process.exit(0); // not a slice commit — nothing to enforce
83
+
84
+ const ctx = {
85
+ changes,
86
+ touchesSlice,
87
+ repoRoot: execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim(),
88
+ SLICE_PATTERN,
89
+ };
90
+
91
+ const checks = loadChecks();
92
+ const violations = [];
93
+ const claimedPaths = new Set(); // first check to flag a path wins — avoids repeat noise
94
+
95
+ for (const check of checks) {
96
+ if (check.skipIfAlreadyFailing && violations.length > 0) continue;
97
+
98
+ let result;
99
+ try {
100
+ result = check.run(ctx) || [];
101
+ } catch (err) {
102
+ violations.push({ path: '(check error)', reason: `[${check.name}] threw: ${err.message}` });
103
+ continue;
104
+ }
105
+
106
+ for (const v of result) {
107
+ if (claimedPaths.has(v.path)) continue;
108
+ claimedPaths.add(v.path);
109
+ violations.push({ path: v.path, reason: `[${check.name}] ${v.reason}` });
110
+ }
111
+ }
112
+
113
+ if (violations.length > 0) {
114
+ console.error('\n❌ commit blocked — slice commit-scope guard found issues:\n');
115
+ for (const v of violations) console.error(` - ${v.path} — ${v.reason}`);
116
+ console.error('\nSee .build-kit/lib/checks/ for what each check enforces.\n');
117
+ process.exit(1);
118
+ }
119
+
120
+ process.exit(0);
121
+ }
122
+
123
+ main();
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ // Rejects a slice commit that touches shared infra which must never change from
4
+ // slice work: the package manifest/lockfiles (no new/changed dependencies) and
5
+ // server.ts (routes/processors are auto-discovered — never wired there by hand).
6
+
7
+ const BLOCKED = [
8
+ {
9
+ pattern: /^(package(-lock)?\.json|pnpm-lock\.yaml|yarn\.lock|npm-shrinkwrap\.json)$/,
10
+ reason: 'dependency/package manifest changes are not allowed from a slice commit',
11
+ },
12
+ {
13
+ pattern: /^server\.ts$/,
14
+ reason: 'server.ts is shared infra (routes/processors are auto-discovered) — never touched by slice work',
15
+ },
16
+ ];
17
+
18
+ module.exports = {
19
+ name: 'blocked-paths',
20
+ run(ctx) {
21
+ const violations = [];
22
+ for (const { path: p } of ctx.changes) {
23
+ const hit = BLOCKED.find((b) => b.pattern.test(p));
24
+ if (hit) violations.push({ path: p, reason: hit.reason });
25
+ }
26
+ return violations;
27
+ },
28
+ };