@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.
- package/README.md +4 -1
- package/cli.js +53 -1
- package/package.json +2 -2
- package/shared/build-kit/lib/checks/README.md +59 -0
- package/shared/build-kit/lib/ralph.js +110 -28
- package/shared/build-kit/lib/util/find-slice.cjs +59 -0
- package/shared/build-kit/ralph-claude.js +6 -2
- package/stacks/blank/templates/build-kit/lib/backend-prompt.md +106 -102
- package/stacks/blank/templates/build-kit/lib/prompt.md +102 -106
- package/stacks/kurrent/templates/.claude/skills/build-automation/SKILL.md +422 -0
- package/stacks/kurrent/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +19 -0
- package/stacks/kurrent/templates/.claude/skills/build-automation/references/idempotent-dispatch-patterns.md +65 -0
- package/stacks/kurrent/templates/.claude/skills/build-state-change/SKILL.md +418 -0
- package/stacks/kurrent/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +41 -0
- package/stacks/kurrent/templates/.claude/skills/build-state-change/references/integration-test-patterns.md +64 -0
- package/stacks/kurrent/templates/.claude/skills/build-state-view/SKILL.md +391 -0
- package/stacks/kurrent/templates/build-kit/CLAUDE.md +122 -0
- package/stacks/kurrent/templates/build-kit/lib/AGENT.md +73 -0
- package/stacks/kurrent/templates/build-kit/lib/backend-prompt.md +169 -0
- package/stacks/kurrent/templates/build-kit/lib/prompt.md +128 -0
- package/stacks/kurrent/templates/root/README.md +46 -0
- package/stacks/kurrent/templates/root/docker-compose.yml +45 -0
- package/stacks/kurrent/templates/root/mvnw +259 -0
- package/stacks/kurrent/templates/root/mvnw.cmd +149 -0
- package/stacks/kurrent/templates/root/pom.xml +152 -0
- package/stacks/kurrent/templates/root/src/main/java/com/example/quickstart/QuickstartApplication.java +12 -0
- package/stacks/kurrent/templates/root/src/main/java/com/example/quickstart/common/EventStore.java +91 -0
- package/stacks/kurrent/templates/root/src/main/java/com/example/quickstart/config/KurrentConfiguration.java +42 -0
- package/stacks/kurrent/templates/root/src/main/resources/application.properties +14 -0
- package/stacks/kurrent/templates/root/src/main/resources/static/index.html +11 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-core-rules/SKILL.md +4 -2
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/SKILL.md +2 -0
- package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +1 -1
- package/stacks/node/templates/build-kit/CLAUDE.md +22 -0
- package/stacks/node/templates/build-kit/lib/check-commit-scope.cjs +123 -0
- package/stacks/node/templates/build-kit/lib/checks/00-blocked-paths.cjs +28 -0
- package/stacks/node/templates/build-kit/lib/checks/10-slice-scope.cjs +29 -0
- package/stacks/node/templates/build-kit/lib/checks/20-append-only-migrations.cjs +20 -0
- package/stacks/node/templates/build-kit/lib/checks/30-test-file-present.cjs +44 -0
- package/stacks/node/templates/build-kit/lib/checks/40-no-invented-fields.cjs +92 -0
- package/stacks/node/templates/build-kit/lib/checks/50-spec-coverage.cjs +50 -0
- package/stacks/node/templates/build-kit/lib/checks/90-tsc-build.cjs +22 -0
- package/stacks/node/templates/root/.githooks/pre-commit +11 -0
- package/stacks/node/templates/root/README.md +67 -0
- package/stacks/node/templates/root/package.json +2 -1
- package/stacks/node/templates/root/setup-env.sh +7 -1
- package/stacks/opencqrs/templates/.claude/skills/build-automation/SKILL.md +434 -0
- package/stacks/opencqrs/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +19 -0
- package/stacks/opencqrs/templates/.claude/skills/build-automation/references/idempotent-dispatch-patterns.md +62 -0
- package/stacks/opencqrs/templates/.claude/skills/build-state-change/SKILL.md +413 -0
- package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +46 -0
- package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/rest-api-patterns.md +145 -0
- package/stacks/opencqrs/templates/.claude/skills/build-state-change/references/test-fixture-patterns.md +85 -0
- package/stacks/opencqrs/templates/.claude/skills/build-state-view/SKILL.md +354 -0
- package/stacks/opencqrs/templates/build-kit/CLAUDE.md +101 -0
- package/stacks/opencqrs/templates/build-kit/lib/AGENT.md +58 -0
- package/stacks/opencqrs/templates/build-kit/lib/backend-prompt.md +169 -0
- package/stacks/opencqrs/templates/build-kit/lib/prompt.md +128 -0
- package/stacks/opencqrs/templates/root/README.md +42 -0
- package/stacks/opencqrs/templates/root/docker-compose.yml +37 -0
- package/stacks/opencqrs/templates/root/mvnw +259 -0
- package/stacks/opencqrs/templates/root/mvnw.cmd +149 -0
- package/stacks/opencqrs/templates/root/pom.xml +139 -0
- package/stacks/opencqrs/templates/root/src/main/java/com/example/quickstart/QuickstartApplication.java +12 -0
- package/stacks/opencqrs/templates/root/src/main/java/com/example/quickstart/config/CqrsConfiguration.java +74 -0
- package/stacks/opencqrs/templates/root/src/main/resources/application.properties +23 -0
- package/stacks/opencqrs/templates/root/src/main/resources/schema.sql +19 -0
- package/stacks/opencqrs/templates/root/src/main/resources/static/index.html +11 -0
- package/stacks/supabase/templates/build-kit/CLAUDE.md +25 -0
- package/stacks/supabase/templates/build-kit/lib/check-commit-scope.cjs +126 -0
- package/stacks/supabase/templates/build-kit/lib/checks/00-blocked-paths.cjs +28 -0
- package/stacks/supabase/templates/build-kit/lib/checks/10-slice-scope.cjs +34 -0
- package/stacks/supabase/templates/build-kit/lib/checks/20-append-only-migrations.cjs +21 -0
- package/stacks/supabase/templates/build-kit/lib/checks/30-test-file-present.cjs +44 -0
- package/stacks/supabase/templates/build-kit/lib/checks/40-no-invented-fields.cjs +92 -0
- package/stacks/supabase/templates/build-kit/lib/checks/50-spec-coverage.cjs +50 -0
- package/stacks/supabase/templates/build-kit/lib/checks/90-tsc-build.cjs +22 -0
- package/stacks/supabase/templates/root/.githooks/pre-commit +11 -0
- package/stacks/supabase/templates/root/package.json +2 -1
- package/stacks/supabase/templates/root/setup-env.sh +7 -1
- package/stacks/umadb/templates/.claude/skills/build-automation/SKILL.md +313 -0
- package/stacks/umadb/templates/.claude/skills/build-automation/references/feature-flag-patterns.md +42 -0
- package/stacks/umadb/templates/.claude/skills/build-state-change/SKILL.md +376 -0
- package/stacks/umadb/templates/.claude/skills/build-state-change/references/feature-flag-patterns.md +42 -0
- package/stacks/umadb/templates/.claude/skills/build-state-change/references/umadb-query-patterns.md +78 -0
- package/stacks/umadb/templates/.claude/skills/build-state-view/SKILL.md +338 -0
- package/stacks/umadb/templates/build-kit/CLAUDE.md +94 -0
- package/stacks/umadb/templates/build-kit/lib/AGENT.md +47 -0
- package/stacks/umadb/templates/build-kit/lib/backend-prompt.md +169 -0
- package/stacks/umadb/templates/build-kit/lib/prompt.md +128 -0
- package/stacks/umadb/templates/root/.mvn/wrapper/maven-wrapper.properties +19 -0
- package/stacks/umadb/templates/root/README.md +48 -0
- package/stacks/umadb/templates/root/docker-compose.yml +29 -0
- package/stacks/umadb/templates/root/mvnw +259 -0
- package/stacks/umadb/templates/root/mvnw.cmd +149 -0
- package/stacks/umadb/templates/root/pom.xml +151 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/QuickstartApplication.java +12 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/config/UmaDbConfig.java +39 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/DecisionModelLoader.java +76 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/EventCodec.java +40 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/EventDispatcher.java +100 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/OptimisticConcurrencyException.java +14 -0
- package/stacks/umadb/templates/root/src/main/java/io/umadb/quickstart/eventstore/SliceEventListener.java +23 -0
- package/stacks/umadb/templates/root/src/main/resources/application.properties +17 -0
- package/stacks/umadb/templates/root/src/test/java/io/umadb/quickstart/testsupport/InMemoryUmaDbClient.java +135 -0
- 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
|
+
}
|
package/stacks/kurrent/templates/root/src/main/java/com/example/quickstart/common/EventStore.java
ADDED
|
@@ -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
|
|
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,
|
|
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.
|
package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/SKILL.md
CHANGED
|
@@ -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
|
|
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
|
```
|