chore: remove sample portfolio module

This commit is contained in:
DongHyeonka
2026-08-13 21:47:15 +09:00
parent e552e317d6
commit 697fc740e6
321 changed files with 190 additions and 18961 deletions
-7
View File
@@ -178,13 +178,6 @@ gates:
workflow: object-storage-qualification.yml
job: minio-managed-contract
execution: explicit
- id: poster-image-migration
release_blocking: true
mechanism: gradle-custom-task
ref: posterImageMigrationTest
workflow: object-storage-qualification.yml
job: poster-image-v7-migration
execution: explicit
- id: object-storage-minio-managed-fault
release_blocking: conditional
mechanism: gradle-custom-task
+3 -2
View File
@@ -25,10 +25,11 @@ fi
readonly REPO_ROOT
readonly MATRIX="${REPO_ROOT}/.github/ci-gate-matrix.yml"
# Deliberately a literal: a gate silently appearing or disappearing is the drift this lint exists to
# catch, so growing the matrix is an explicit edit here. 38 as of the HTTP Client platform hardening,
# catch, so growing the matrix is an explicit edit here. 37 after removing the sample-only Poster
# migration gate; the HTTP Client platform hardening
# which registered httpclient-spring62-runtime as a delegated-pending control — the 6.2 *runtime*
# claim, distinct from the API-surface scan that was standing in for it.
readonly EXPECTED_GATE_COUNT=38
readonly EXPECTED_GATE_COUNT=37
if [[ ! -f "${MATRIX}" ]]; then
printf '::error::gate-matrix-lint: missing %s\n' "${MATRIX}" >&2
+1 -1
View File
@@ -26,7 +26,7 @@ readonly EXPECTED_WORKFLOW_LOCK=(
'ad84000efc438ee7439517b8f85819e62b13dab0aa4f94066c2905060f3bb581 .github/workflows/httpclient-release.yml'
'59cb3a0ffc687a15eefe96bc5e3a70d42be78e1cc85d2e7f7880dac6124ca4c7 .github/workflows/jpa-r2-evidence.yml'
'5be7e931db749029d89787da042d6d7cf8e683d60698bd8a2993c29db26355fb .github/workflows/link-check.yml'
'64245586cd5936f1a5647b57f2cd9acd316f96fd75f713b1890decb812e7d5fe .github/workflows/object-storage-qualification.yml'
'2f1c46cc8b7b39890e5debe90f78d19b9347ed53f499b282539dae88aefdd574 .github/workflows/object-storage-qualification.yml'
'cbc104ea486c746229895e804e3be7716e056a02cce0588c537bce9f442f8b38 .github/workflows/redis-sdk-topology.yml'
)
readonly EXPECTED_WRAPPER_PROPERTIES=(
@@ -19,26 +19,6 @@ env:
TESTCONTAINERS_REUSE_ENABLE: "false"
jobs:
poster-image-v7-migration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
- name: Validate Gradle wrapper
id: gradle-wrapper-validation
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
with:
distribution: temurin
java-version: "21.0.11+10"
cache: gradle
cache-dependency-path: |
src/**/*.gradle
src/**/gradle-wrapper.properties
src/**/gradle.lockfile
- name: Run non-skipping Poster image migration qualification
working-directory: src
run: ./gradlew :sample-portfolio:posterImageMigrationTest --no-daemon --stacktrace
minio-managed-contract:
runs-on: ubuntu-latest
steps:
+6 -8
View File
@@ -4,7 +4,7 @@
이 저장소는 단순한 예제 블로그 애플리케이션이 아니라, Java 21 + Spring Boot 4.0.0 + Gradle 멀티모듈 기반의 Clean Architecture 템플릿이다.
기본 패키지는 `dev.caskeleton`이며, 예시 도메인은 production 모듈이 아니라 `sample-portfolio` 모듈(WorkLog 엔지니어링 작업 기록 게시판)에 격리한다. 새 프로젝트를 시작할 때는 도메인 이름, 패키지, 엔티티, 유스케이스를 교체할 수 있지만, 모듈 경계와 의존성 방향은 유지해야 한다.
기본 패키지는 `dev.caskeleton`이며 구체적인 예시 도메인은 제거되어 있다. 새 프로젝트를 시작할 때는 production 모듈에 도메인, 엔티티, 유스케이스를 추가할 수 있지만, 모듈 경계와 의존성 방향은 유지해야 한다.
## Prime Directive
@@ -49,8 +49,8 @@ root `CLAUDE.md`는 이 목록의 동기화된 요약이다. 두 문서가 어
## Gradle 정책 권위
- `src/config/architecture/modules.json`: 정확히 19개 leaf의 ID, repository-relative 소스 경로,
Gradle path, 허용 production project dependency edge, composition root의 실제 runtime
- `src/config/architecture/modules.json`: 정확히 18개 leaf의 ID, repository-relative 소스 경로,
Gradle path, 허용 production project dependency edge, production composition root의 실제 runtime
membership
- `src/settings.gradle`: registry를 fail-closed로 검증하고 등록된 Gradle project를 include/mapping
- `src/build.gradle`: 같은 registry를 읽는 `verifyCleanArchitectureDependencies`와 그 밖의
@@ -102,7 +102,7 @@ commit 정책은 모든 플랫폼에서 `human-only`이며 agent는 stage/commit
## 모듈 책임
19개 leaf 모듈의 ID, 실제 소스 경로, Gradle path, 허용 production 의존성, runtime membership은
18개 leaf 모듈의 ID, 실제 소스 경로, Gradle path, 허용 production 의존성, runtime membership은
`src/config/architecture/modules.json`이 SSOT다. focused test는 소유 leaf의 `gradle_path`에서
파생한다. 이 문서는 leaf 목록을 복제하지 않고 family 책임만 정의한다. 작업 파일에서는 가장 가까운
`src/**/CLAUDE.md`를 함께 읽는다.
@@ -118,7 +118,6 @@ commit 정책은 모든 플랫폼에서 `human-only`이며 agent는 stage/commit
file server, HTTP client, identifier 능력을 port 뒤에서 구현한다. adapter 간 허용 edge는
registry만 따른다.
- `shared-contract`: skeleton-wide 운영 계약. business/domain 개념 저장 금지.
- `sample-portfolio`: 샘플/fixture consumer. production leaf가 의존하면 안 된다.
- `app-bootstrap`: Spring Boot entrypoint와 composition root. 비즈니스 유스케이스 금지.
## 의존성 방향
@@ -129,7 +128,6 @@ family 수준 기본 방향:
app-bootstrap -> adapter:inbound:* -> application-core -> domain-core
app-bootstrap -> adapter:outbound:* -> application-core -> domain-core
runtime modules -> shared-contract
sample-portfolio -> registered runtime leaves (fixture consumer only)
```
개별 edge는 `src/config/architecture/modules.json``allowed_dependencies`가 유일한 목록이다.
@@ -182,7 +180,7 @@ cd src
```
소유 leaf의 정확한 Gradle path는 `src/config/architecture/modules.json`에서 읽고 focused test
명령을 파생한다. root 문서에 19개 명령 목록을 복제하지 않는다.
명령을 파생한다. root 문서에 18개 명령 목록을 복제하지 않는다.
## 설정과 런타임
@@ -208,7 +206,7 @@ Docker/runtime 규칙:
1. `src/settings.gradle``rootProject.name`을 새 프로젝트명으로 바꾼다.
2. Java package `dev.caskeleton`을 새 organization/project package로 바꾼다.
3. `CaSkeletonApplication` 이름을 새 애플리케이션 이름으로 바꾼다.
4. production 모듈에는 목표 도메인의 entity, repository port, use case, adapter만 추가하고, 예시 코드는 `sample-portfolio`에 격리한다.
4. production 모듈에는 목표 도메인의 entity, repository port, use case, adapter만 추가한다.
5. 모듈 이름과 경계는 유지한다.
6. Docker image/application 이름을 새 프로젝트 기준으로 수정한다.
7. `.env`, `.env.local`, `application.yml`의 예시 값을 새 런타임 요구사항에 맞춘다.
+3 -4
View File
@@ -21,9 +21,9 @@ If this summary drifts from `AGENTS.md`, `AGENTS.md` wins and this summary must
## Gradle policy authorities
- `src/config/architecture/modules.json`: exactly 19 leaf identities, repository-relative source
- `src/config/architecture/modules.json`: exactly 18 leaf identities, repository-relative source
paths, Gradle paths, allowed production project dependency edges, and the exact runtime
memberships of both composition roots.
membership of the production composition root.
- `src/settings.gradle`: fail-closed registry validation, project inclusion, and directory mapping.
- `src/build.gradle`: `verifyCleanArchitectureDependencies` and the other architecture-wide
verification tasks.
@@ -43,7 +43,7 @@ count.
## Module families
`src/config/architecture/modules.json` owns the complete 19-leaf list. Root guidance summarizes
`src/config/architecture/modules.json` owns the complete 18-leaf list. Root guidance summarizes
families; the nearest `src/**/CLAUDE.md` owns local rules.
| Family | Responsibility | Stable dependency direction |
@@ -54,7 +54,6 @@ families; the nearest `src/**/CLAUDE.md` owns local rules.
| `adapter:outbound:persistence-*` | JPA/PostgreSQL and MongoDB persistence adapters | application/domain/shared contracts as registered |
| `adapter:outbound:*` | support, messaging, cache, notification, storage, file, HTTP client, identifier capabilities | application/domain/shared and registered support edge |
| `shared-contract` | Skeleton-wide operational contracts | Java stdlib only |
| `sample-portfolio` | Fixture/reference consumer | registered runtime leaves; never a production dependency |
| `app-bootstrap` | Spring Boot entrypoint and composition root | registered runtime leaves |
Never infer an individual leaf's Gradle path, allowed dependency, or test command from this table.
+7 -9
View File
@@ -4,7 +4,7 @@ Initialized from `clean-architecture-backend-template` revision
`0a6dd0e419620683de48f69b5c6d22d9964b6f44` as a tracked snapshot. Template
updates are applied explicitly and recorded in `template.lock.json`.
ca-skeleton은 Java 21, Spring Boot 4.0.0, Gradle 멀티모듈 기반의 Clean Architecture 백엔드 템플릿입니다. fork해서 도메인·패키지·엔티티·유스케이스만 교체하면 새 서비스를 시작할 수 있고, 모듈 경계와 의존 방향은 그대로 유지합니다. 기본 패키지는 `dev.caskeleton`이며, 예시 도메인은 production 모듈이 아니라 `sample-portfolio` 모듈(WorkLog 작업 기록 게시판)에 격리합니다.
ca-skeleton은 Java 21, Spring Boot 4.0.0, Gradle 멀티모듈 기반의 Clean Architecture 백엔드 템플릿입니다. fork해서 도메인·패키지·엔티티·유스케이스를 추가하면 새 서비스를 시작할 수 있고, 모듈 경계와 의존 방향은 그대로 유지합니다. 기본 패키지는 `dev.caskeleton`이며, 구체적인 예시 도메인은 제거되어 있습니다.
이 문서는 전체 구조와 첫 실행만 다룹니다. 모듈별 상세 규칙과 설계 근거는 각 모듈의 README와 `CLAUDE.md`가, 빌드·환경 변수 상세는 [src/README.md](src/README.md)가 소유합니다.
@@ -16,7 +16,6 @@ ca-skeleton은 Java 21, Spring Boot 4.0.0, Gradle 멀티모듈 기반의 Clean A
app-bootstrap -> adapter:inbound:* -> application-core -> domain-core
app-bootstrap -> adapter:outbound:* -> application-core -> domain-core
모든 런타임 모듈 -> shared-contract
sample-portfolio -> 등록된 런타임 리프 (fixture 소비자 전용)
```
family 수준의 책임은 다음과 같습니다.
@@ -30,9 +29,8 @@ family 수준의 책임은 다음과 같습니다.
| `adapter:outbound:*` | support(공유 베이스)·messaging·cache·notification·object storage·file·HTTP client·identifier 능력을 port 뒤에서 구현. 외부 연동 어댑터(messaging·cache·notification·HTTP client)는 기본 비활성 |
| `shared-contract` | skeleton 전역 운영 계약. business/domain 개념 저장 금지 |
| `app-bootstrap` | Spring Boot entrypoint와 composition root |
| `sample-portfolio` | WorkLog 예시 도메인(fixture/reference). production이 의존하지 않음 |
정확한 19개 leaf 목록과 각 leaf의 Gradle path·소스 경로·허용 production 의존 edge는
정확한 18개 leaf 목록과 각 leaf의 Gradle path·소스 경로·허용 production 의존 edge는
[src/config/architecture/modules.json](src/config/architecture/modules.json)이 SSOT입니다. focused
test는 해당 Gradle path에서 `./gradlew <gradle-path>:test --console=plain` 형태로 파생하며, root
문서나 기억에서 개별 leaf edge를 추론하지 않습니다.
@@ -46,7 +44,7 @@ cd src
./gradlew bootstrap
```
`bootstrap`은 compile 검사, PostgreSQL Compose 기동, 애플리케이션 이미지 build·기동(startup Flyway 포함), sample 격리 검증, `GET /api/healthcheck` HTTP smoke를 순서대로 실행합니다. 각 단계가 별도 Gradle task라 실패 단계가 task 이름으로 드러납니다. 기동을 확인하려면 health endpoint를 호출합니다.
`bootstrap`은 compile 검사, PostgreSQL Compose 기동, 애플리케이션 이미지 build·기동(startup Flyway 포함), `GET /api/healthcheck` HTTP smoke를 순서대로 실행합니다. 각 단계가 별도 Gradle task라 실패 단계가 task 이름으로 드러납니다. 기동을 확인하려면 health endpoint를 호출합니다.
```bash
curl -fsS http://localhost:8080/api/healthcheck
@@ -91,10 +89,10 @@ cd src
애플리케이션 이름 등 나머지 rename 단계는 위 체크리스트를 따릅니다.
3. `CaSkeletonApplication`을 새 애플리케이션 이름으로 바꾸고, 목표 도메인의 엔티티·repository port·유스케이스·adapter를 production 모듈에 추가합니다. 예시 코드는 `sample-portfolio`에만 둡니다.
3. `CaSkeletonApplication`을 새 애플리케이션 이름으로 바꾸고, 목표 도메인의 엔티티·repository port·유스케이스·adapter를 production 모듈에 추가합니다.
4. 모듈 이름과 경계는 그대로 유지합니다.
검증은 sample-on과 sample-off 모두 통과시킵니다.
검증은 전체 테스트와 sample-off 재유입 방지 계약을 모두 통과시킵니다.
```bash
cd src
@@ -102,7 +100,7 @@ cd src
./gradlew :app-bootstrap:sampleOffTest
```
`sample-portfolio`는 템플릿이 유지하는 fixture/reference 모듈이라 production 모듈이 의존하지 않고, runtime에 sample bean이나 endpoint를 넣지 않습니다. 다운스트림 fork에서 fixture가 더 필요 없을 때만 sample-off 테스트를 통과시킨 뒤 정리합니다.
`sampleOffTest`는 삭제된 샘플 타입이 production bootstrap classpath에 다시 들어오지 않는지 검증합니다.
## 아키텍처 규칙과 검증
@@ -129,7 +127,7 @@ cd src
## 더 알아보기
- 빌드·검증 게이트·환경 변수 상세: [src/README.md](src/README.md)
- 모듈 레지스트리(19개 leaf SSOT): [src/config/architecture/modules.json](src/config/architecture/modules.json)
- 모듈 레지스트리(18개 leaf SSOT): [src/config/architecture/modules.json](src/config/architecture/modules.json)
- 에이전트·기여자 작업 규칙: [AGENTS.md](AGENTS.md) · [CLAUDE.md](CLAUDE.md)
- 빌드·릴리스 공급망 파이프라인은 현재 Mode B 복구 범위에 포함되지 않았다. 현재 저장소가
제공하는 canonical workflow는 품질·의존성 취약점·링크 검사이며, release/publish 자동화는 별도
+5
View File
@@ -3063,6 +3063,11 @@ env_keys:
required_test: cache-contract:redis-soft-hard-ttl-order
- name: APP_CACHE_REDIS_TTL_JITTER
# DEPRECATED 2026-08-13: removing the installed sample portfolio also removed the only
# application.yml binding for this key; app-bootstrap has no TTL-jitter consumer. The row is
# retained so deployments still setting it can be warned until its scheduled removal.
deprecated_orphaned: true
removal_deadline: 2026-11-30
type: float
default: 0.10
allowed_values: null
-129
View File
@@ -1,129 +0,0 @@
# syntax=docker/dockerfile:1.7-labs@sha256:b99fecfe00268a8b556fad7d9c37ee25d716ae08a5d7320e6d51c4dd83246894
# =============================================================================
# sample-portfolio standalone demo image — twin of src/Dockerfile.
#
# Builds and runs the REFERENCE app (SamplePortfolioApplication), not the
# production composition root (CaSkeletonApplication) that src/Dockerfile builds.
# The sample is the demo-friendly entrypoint: its application.yml self-provides
# defaults for every env placeholder, so the only external dependency it needs
# to boot is a reachable PostgreSQL (datasource + Flyway sample migrations).
#
# Build (no build-args required — this is a disposable demo, not a release artifact):
# docker build -f src/Dockerfile.sample src/ -t ca-sample:local
#
# Run (point APP_DATASOURCE_URL at a reachable Postgres; localhost default shown):
# docker run --rm -p 8080:8080 -p 9001:9001 \
# -e APP_DATASOURCE_URL=jdbc:postgresql://host.docker.internal:5432/ca_skeleton \
# ca-sample:local
#
# The multi-stage build, --parents descriptor glob, STRICT lock verification,
# non-root user, read-only-root-fs writable mounts, and JVM container ergonomics
# are all identical to src/Dockerfile — only the bootJar target differs. Keep the
# two files' builder stages in sync.
# =============================================================================
# Demo defaults so the image builds with zero build-args. Two format constraints from the root
# build.gradle configuration guard (feature-build-release-supply-chain-contract D1/D9):
# - RELEASE_VERSION must be bare MAJOR.MINOR.PATCH — no pre-release/build suffix (build.gradle L21).
# The "-sample" marker therefore lives only on BUILD_VERSION, which is a label, not a gradle prop.
# - GIT_SHA must be 7-40 hex chars (build.gradle L35); 0000000 is the placeholder.
ARG RELEASE_VERSION=0.0.0
ARG BUILD_VERSION=0.0.0-sample
ARG GIT_SHA=0000000
ARG SOURCE_URL=https://example.invalid/ca-tmpl-sample
# ---- Stage 1: builder -------------------------------------------------------
# Uses the full JDK only in the build stage, never in the final image.
FROM eclipse-temurin:21-jdk-jammy@sha256:801b7e1a9c4befaf82bf9a2a58025ef43a7694bbc84779187ad0524d84742772 AS builder
ARG RELEASE_VERSION
ARG GIT_SHA
WORKDIR /build/src
# Copy the Gradle wrapper and every module's build descriptor + dependency lockfile FIRST,
# so the expensive dependency-resolution layer is cached and only re-runs when a build.gradle
# or gradle.lockfile changes (D8). `--parents` preserves each file's directory structure, so a
# single structure-preserving glob replaces a per-module COPY list: new modules are picked up
# automatically and this stage never drifts out of sync with settings.gradle.
# (Requires the labs Dockerfile frontend — see the `# syntax` directive at the top of this file.)
COPY gradlew ./
COPY gradle/ gradle/
COPY config/ ./config/
COPY --parents settings.gradle build.gradle **/build.gradle **/gradle.lockfile ./
# Resolve every module configuration in STRICT mode (no --write-locks in a demo build either).
RUN test -n "${RELEASE_VERSION}" \
&& test -n "${GIT_SHA}" \
&& ./gradlew verifyDependencyLocks --no-daemon --quiet \
-PreleaseVersion="${RELEASE_VERSION}" -PgitRevision="${GIT_SHA}"
# Copy full source and stage the executable sample JAR at Gradle's declared Docker output path.
COPY . .
RUN ./gradlew :sample-portfolio:stageDockerJar --no-daemon -x test \
-PreleaseVersion="${RELEASE_VERSION}" -PgitRevision="${GIT_SHA}"
# ---- Stage 2: runtime image -------------------------------------------------
# JRE-only slim image (no full JDK in the demo image either).
FROM eclipse-temurin:21-jre-jammy@sha256:199aebeb3adcde4910695cdebfe782ada38dadb6cc8013159b58d3724451befd AS runtime
ARG BUILD_VERSION
ARG GIT_SHA
ARG SOURCE_URL
# OCI image labels. Unlike the release image (src/Dockerfile), the demo image does NOT hard-fail
# on missing metadata — the ARG defaults above keep it buildable with no build-args.
LABEL org.opencontainers.image.title="caskeleton-sample" \
org.opencontainers.image.description="ca-tmpl sample-portfolio reference/demo application" \
org.opencontainers.image.source="${SOURCE_URL}" \
org.opencontainers.image.revision="${GIT_SHA}" \
org.opencontainers.image.version="${BUILD_VERSION}"
# ---- Locale / timezone ------------------------------------------------------
ENV TZ=UTC \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8
# ---- Writable HOME under read-only root fs ----------------------------------
ENV HOME=/tmp
# ---- JVM ergonomics ---------------------------------------------------------
# Identical to src/Dockerfile: container-aware heap, fail-fast on OOM, heap dump to a
# writable mount, and Tomcat temp redirected to /tmp for a read-only root filesystem.
ENV JAVA_TOOL_OPTIONS="\
-XX:MaxRAMPercentage=75 \
-XX:+UseContainerSupport \
-XX:+ExitOnOutOfMemoryError \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/tmp/heap \
-Dserver.tomcat.basedir=/tmp"
# ---- Filesystem layout (read-only root filesystem) --------------------------
# At runtime /var/tmp/heap and /tmp MUST be writable mounts (tmpfs/emptyDir).
RUN mkdir -p /var/tmp/heap && chmod 1777 /var/tmp/heap
# ---- Non-root user ----------------------------------------------------------
RUN groupadd --system --gid 1000 app \
&& useradd --system --uid 1000 --gid app --no-create-home --shell /usr/sbin/nologin app
WORKDIR /app
COPY --from=builder --chown=app:app /build/src/sample-portfolio/build/docker/application.jar app.jar
USER app
# ---- Ports ------------------------------------------------------------------
# 8080 — application HTTP port
# 9001 — management / actuator port
EXPOSE 8080 9001
# ---- Health check -----------------------------------------------------------
# Actuator readiness probe on the management port (9001). Ignored by Kubernetes,
# which uses its own probes — kept for docker/compose parity with src/Dockerfile.
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
CMD wget --no-verbose --tries=1 --spider \
http://localhost:9001/actuator/health/readiness || exit 1
# ---- Entrypoint -------------------------------------------------------------
# mainClass (SamplePortfolioApplication) is baked into the bootJar manifest.
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
+5 -9
View File
@@ -17,7 +17,7 @@
| 게이트 | 하는 일 |
| --- | --- |
| `verifyCleanArchitectureDependencies` | 모듈 간 의존 방향이 허용된 범위 안에 있는지 검사 |
| `verifyRuntimeModuleMembership` | registry의 composition root membership과 실제 main project dependency가 정확히 일치하는지 검사 |
| `verifyRuntimeModuleMembership` | registry의 production composition root membership과 실제 main project dependency가 정확히 일치하는지 검사 |
| `verifyEnvKeys` | `env-keys.yaml``application.yml``src/.env` 가 어긋나지 않는지 검사 |
| `verifyOneTypePerFile` | 파일당 public 최상위 타입 1개, 파일명 == 타입명인지 검사 |
| `verifyTrivyignore` | `.trivyignore.yaml` 의 Trivy suppression 이 사유·만료일을 갖추고 만료/기한초과가 아닌지 검사 |
@@ -26,7 +26,7 @@
### Local bootstrap
`./gradlew bootstrap``bootstrapCompile``bootstrapDependencies`
`bootstrapMigrateAndStart` `bootstrapSampleContract` `bootstrapSmoke`를 순서대로 실행합니다.
`bootstrapMigrateAndStart``bootstrapSmoke`를 순서대로 실행합니다.
DB와 app lifecycle은 저장소 루트의 base/local Compose 조합이 소유하며, app startup Flyway가
끝나 public health endpoint가 준비되어야 다음 단계로 넘어갑니다. `src/.env`는 env 설정의
SSOT이고 bootstrap이 별도 env template을 만들지 않습니다.
@@ -88,10 +88,10 @@ vendor/build나 container base image까지 byte-for-byte 같음을 주장하지
### `verifyRuntimeModuleMembership`
- **하는 일.** 같은 registry의 `runtime_compositions`와 각 leaf의 `runtime_memberships`를 읽어
`app-bootstrap`/`sample-portfolio`의 실제 `api`/`implementation`/`compileOnly`/`runtimeOnly`
`app-bootstrap`의 실제 `api`/`implementation`/`compileOnly`/`runtimeOnly`
project dependency와 정확히 대조합니다.
- **opt-in의 의미.** membership이 빈 GraphQL/gRPC/WebSocket/Mongo leaf는 독립 빌드 대상이지만
shipped runtime에는 없습니다. app-bootstrap의 `conditionalTransportTest` test-only classpath는
- **opt-in의 의미.** membership이 빈 GraphQL/gRPC/WebSocket/Mongo leaf는 독립 빌드 대상이지만
production runtime에는 없습니다. app-bootstrap의 `conditionalTransportTest` test-only classpath는
실제 채택 전에 세 inbound transport를 함께 qualification하기 위한 evidence composition입니다.
- **변경 규칙.** production edge를 추가하거나 제거할 때 `allowed_dependencies`,
`runtime_memberships`, 실제 Gradle dependency를 같은 변경에서 갱신하지 않으면 `check`가 실패합니다.
@@ -282,10 +282,6 @@ ca-skeleton:
- 기존 `APP_OUTBOUND_HTTP_*``app.outbound.http.*`는 canonical 설정이 아닙니다. 루트 `src/.env`,
`app-bootstrap`의 application YAML, env-key registry에서 제거됐으며 canonical composition에
입력하면 상태와 무관하게 기동을 거부합니다.
- 다만 `sample-portfolio`의 application YAML에는 legacy facade를 시연하기 위해 15개 키가 남아
있습니다. 이 모듈은 fixture/reference consumer이고 production 의존성이 아니며, 그 YAML은
`verifyEnvKeys`가 검사하는 세 파일에 포함되지 않습니다. "제거됐다"는 문장이 저장소 전체를
가리킨다고 읽히지 않도록 범위를 명시합니다.
- legacy JDK facade가 필요한 fork만 canonical composition 밖에서
`OutboundHttpSettings.bindLegacy(Binder)`와 legacy configuration을 명시적으로 import합니다.
timeout/retry/CB/response-size 설정은 그 migration API 내부 계약일 뿐 canonical provider
+1 -1
View File
@@ -20,7 +20,7 @@ Package root: `dev.caskeleton.adapter.inbound.graphql`.
자동 합성/바인딩하도록 얹는 얇은 계층이다.
- feature-agnostic: `classpath:graphql/**` 스키마와 모든 `@Controller` `@QueryMapping`/
`@MutationMapping` 을 generic 하게 합성한다. **WorkLog 등 구체 기능을 이름으로 알지 않는다.**
- classpath opt-in: 현재 `app-bootstrap`/`sample-portfolio` production runtime 은 이 leaf 를
- classpath opt-in: 현재 `app-bootstrap` production runtime 은 이 leaf 를
의존하지 않는다. 실제 채택 시 composition root 가 GraphQL leaf 와 인증/인가·CORS 정책,
GraphiQL/introspection 운영 설정을 함께 명시해야 한다.
+5 -6
View File
@@ -19,18 +19,17 @@ Spring for GraphQL 은 schema-first 다. 빈 스키마로는 부팅이 실패하
## 기능(feature)은 어떻게 기여하는가 — machinery/feature 분리
스켈레톤은 **WorkLog 를 이름으로 알지 못한다.** 향후 composition root 가 이 모듈을 classpath 에
스켈레톤은 구체적인 제품 기능을 이름으로 알지 못한다. 향후 composition root 가 이 모듈을 classpath 에
명시적으로 채택하고 feature 를 추가하면 Spring for GraphQL 이 다음 두 축으로 합성할 수 있다:
- **스키마**: `classpath:graphql/**/*.graphqls` 를 전부 병합한다. sample 모듈의
향후 `worklog.graphqls` 같은 feature 스키마는 스켈레톤의 `skeleton.graphqls` 와 합쳐진다.
- **스키마**: `classpath:graphql/**/*.graphqls` 를 전부 병합한다. 제품 모듈의 feature 스키마는
스켈레톤의 `skeleton.graphqls` 와 합쳐진다.
- **resolver(핸들러)**: 컨텍스트의 모든 `@Controller``@QueryMapping`/`@MutationMapping`
메서드를 바인딩한다. 향후 feature 의 GraphQL controller 는 스켈레톤을 수정하지 않고 등록할 수
있다.
현재 `app-bootstrap``sample-portfolio` production runtime 은 이 leaf 를 의존하지 않는다.
즉 이 모듈은 **classpath opt-in**며, 현재 sample 에 feature GraphQL 스키마/controller 가 있다
뜻이 아니다. leaf 자체는 최소 health 스키마로 독립 기동할 수 있다.
현재 `app-bootstrap` production runtime 은 이 leaf 를 의존하지 않는다. 즉 이 모듈은
**classpath opt-in**다. leaf 자체는 최소 health 스키마로 독립 기동할 수 있다.
## 에러 매핑 — web `GlobalExceptionHandler` / gRPC 인터셉터의 GraphQL 형제
+5 -8
View File
@@ -58,8 +58,8 @@ also drives the runtime patterns reference implementations must follow.
`Optional<T>` wrappers on request records.
- **B3 — Mapper-internal failures.** Map record canonical-constructor
`IllegalArgumentException`, MapStruct generated NPE, ACL normalization
failures, etc. by throwing `MappingException` (sample implementation in
`sample-portfolio`); the global handler routes it to `MAPPING_FAILED` (HTTP 400),
failures, etc. by throwing `MappingException`; the global handler routes it to
`MAPPING_FAILED` (HTTP 400),
never to `BAD_PARAMETER` or `INTERNAL_ERROR`. Plain `IllegalArgumentException`
remains `BAD_PARAMETER` for non-mapper callers.
- **B4 — Validation layering.** Class-level Bean Validation constraints belong
@@ -93,16 +93,13 @@ Domain `@RestControllerAdvice` in a consuming module must be annotated
`@Order(Ordered.HIGHEST_PRECEDENCE)` (or otherwise ordered ahead of this
module's base `GlobalExceptionHandler`), because the base handler's catch-all
`@ExceptionHandler(Exception.class)` would otherwise resolve domain exceptions
to `INTERNAL_ERROR`. See `sample-portfolio`'s `DomainExceptionHandler` for the
pattern.
to `INTERNAL_ERROR`.
The base operational handler (`error/GlobalExceptionHandler`), the error-code
contract (`dev.caskeleton.shared.error.ApiErrorCode` + `OperationalError`), the
`error/ErrorResponseFactory`, and the `envelope/EnvelopeBodyAdvice` now live in
production modules (`adapter:inbound:web` / `shared-contract`), so the running application
provides them without depending on `sample-portfolio`. Domain-specific exception
handlers and error codes live in the consuming module (see sample's
`DomainExceptionHandler` / `PortfolioErrorCode`).
production modules (`adapter:inbound:web` / `shared-contract`). Domain-specific exception
handlers and error codes live in the consuming module.
## Schema / serialization contract
+1 -2
View File
@@ -225,8 +225,7 @@ commit-aware response wrapper 계약을 구현한다. 또한 이 모듈은 HTML
### EnvelopeBodyAdvice
- 컨트롤러는 도메인/DTO 타입을 반환하고, 이 advice 가 와이어 형태를 항상
`{success, data | error, traceId}` 로 보장한다.
- 위치: sample 모듈이 아니라 adapter-web. 실행 앱은 adapter-web 에 의존하지만 sample-portfolio 에는 의존하지
않으므로, 응답 래핑이 실제로 동작하려면 여기 있어야 한다.
- 위치: adapter-web. 실행 앱이 이 모듈을 의존하므로 응답 래핑이 실제 runtime에 적용된다.
### CacheControlFilter
- 스켈레톤 기본 HTTP 캐시 정책.
+1 -1
View File
@@ -66,7 +66,7 @@ The broadcaster sends only when exactly one projector matches. Zero or duplicate
projection failures, invalid event types, null fields, and over-limit maps are dropped. The wire
envelope contains only `type`, bounded string `fields`, and `occurredAt`.
No current `sample-portfolio` feature publishes a WebSocket event or contributes a projector.
No current production feature publishes a WebSocket event or contributes a projector.
That is a future adoption example, not an existing runtime feature.
## Evidence and limits
+2 -2
View File
@@ -5,8 +5,8 @@ live in [CLAUDE.md](CLAUDE.md); this document records why the P1 boundary has th
## Opt-in instead of accidental exposure
The leaf is built and tested but is not part of `app-bootstrap` or `sample-portfolio` production
runtime membership. Even after a future composition adds it, `ca-skeleton.websocket.enabled=false`
The leaf is built and tested but is not part of `app-bootstrap` production runtime membership.
Even after a future composition adds it, `ca-skeleton.websocket.enabled=false`
keeps its configuration and broadcaster absent. Enabling requires explicit non-wildcard origins,
so merely adding the artifact cannot expose a wildcard STOMP broker.
+1 -1
View File
@@ -42,7 +42,7 @@ Package root: `dev.caskeleton.adapter.outbound.fileserver`. Driven (outbound) ad
## Forbidden
- Inbound adapters, sibling outbound adapters, persistence, `app-bootstrap`, `sample-portfolio`
- Inbound adapters, sibling outbound adapters, persistence, or `app-bootstrap`
(ArchUnit `OUTBOUND_ADAPTERS_*` family rules).
- Leaking filesystem, stream, framework, or provider types across `FilePublicationPort`.
- Advertising `FILE_AND_DIRECTORY_SYNC` as physical device/controller/replica/site power-loss
+1 -2
View File
@@ -37,8 +37,7 @@ build choices) live in [README.md](README.md).
- Persistence or web technology (JPA/Hibernate/Spring Data/Spring Web) — ArchUnit
`identifier_adapter_does_not_depend_on_other_adapters_or_bootstrap`;
`.claude/hooks/ca_import_gate.py` G4 가 쓰기 시점에 차단.
- inbound adapters, persistence adapters, other outbound leaves, `app-bootstrap`,
`sample-portfolio`.
- inbound adapters, persistence adapters, other outbound leaves, or `app-bootstrap`.
- External IO (HTTP / messaging / cache / DB) — that belongs in `adapter-outbound`.
## Test
+1 -1
View File
@@ -34,7 +34,7 @@ readiness live in [README.md](README.md) and
## Forbidden
- Inbound adapters, sibling outbound adapters, persistence, app-bootstrap, or sample-portfolio
- Inbound adapters, sibling outbound adapters, persistence, or app-bootstrap
dependencies.
- Provider keys, paths, locators, SDK types, Spring types, or control-record types leaking into
application-core.
@@ -68,10 +68,9 @@ Adapters that serve one optional capability carry that capability's switch, unli
module. The `fileserver` package is the current case: every `Jpa*` adapter there is annotated
`@ConditionalOnProperty(prefix = "app.fileserver-platform", name = "enabled", havingValue = "true")`.
Without the gate a composition root that merely includes this module builds those beans, and each of
them needs collaborators only the Fileserver configuration supplies — which is how `sample-portfolio`
came to fail on a `FileStateMachine` it has no use for. A store for a capability nobody enabled
should not exist.
Without the gate a composition root that merely includes this module builds those beans, although
their collaborators exist only when Fileserver configuration is selected. A store for a capability
nobody enabled should not exist.
## Allowed
@@ -174,7 +173,7 @@ framework-neutral `shared.error.PersistenceFailureException` carrying one of the
Audit metadata (`created_at` / `updated_at` / `created_by` / `updated_by`, D3) is an
infrastructure concern that must never reach `domain-core` (D2). It lives only on the
`audit/AuditableEntity` `@MappedSuperclass`; a domain aggregate persistence entity opts in
by extending it (D6 — e.g. the sample `WorkLogEntity`). The domain aggregate itself carries
by extending it (D6). The domain aggregate itself carries
zero audit fields, enforced by ArchUnit `domain_is_pure` (no `jakarta.persistence..`) plus
`domain_entities_do_not_carry_audit_fields` (no `createdAt`/`updatedAt`/`createdBy`/`updatedBy`
fields under `..domain..`).
@@ -11,8 +11,7 @@ import org.junit.jupiter.api.Test;
* (feature-persistence-auditing-contract D1/D3/D4/D5).
*
* <p>Exercised through a tiny concrete subclass because {@link AuditableEntity} is a
* {@code @MappedSuperclass} (JPA cannot instantiate it standalone, and the production subclass
* {@code WorkLogEntity} lives in the sample module).
* {@code @MappedSuperclass} (JPA cannot instantiate it standalone).
*/
class AuditableEntityTest {
@@ -32,7 +32,7 @@ Data MongoDB infrastructure. Design rationale lives in [README.md](README.md).
## Forbidden
- Inbound adapters, sibling outbound adapters, `app-bootstrap`, `sample-portfolio` (ArchUnit
- Inbound adapters, sibling outbound adapters, or `app-bootstrap` (ArchUnit
`OUTBOUND_ADAPTERS_*` family rules).
- Shipping placeholder `Example*` document, repository, record, or adapter types in production.
- Adding idempotency/outbox/lock on Mongo without a separately approved contract.
+1 -1
View File
@@ -20,7 +20,7 @@ Package root: `dev.caskeleton.bootstrap`.
## Allowed
- Runtime leaves whose dependency edge is explicitly allowed and whose `runtime_memberships`
includes `app-bootstrap` in `src/config/architecture/modules.json`; do not duplicate the 19-leaf
includes `app-bootstrap` in `src/config/architecture/modules.json`; do not duplicate the 18-leaf
dependency list here.
- Spring Boot startup/runtime dependencies.
- ArchUnit in tests.
+3 -7
View File
@@ -4,14 +4,14 @@
이 모듈은 비즈니스 로직을 담지 않는다. Spring Boot 기동, 런타임 설정 바인딩, 기본 runtime
모듈 간 최종 와이어링, 그리고 composition classpath를 대상으로 한 중앙 아키텍처 테스트만 둔다.
19개 leaf 전체의 프로젝트 edge와 composition root의 실제 runtime membership은 JSON registry를
18개 leaf 전체의 프로젝트 edge와 production composition root의 실제 runtime membership은 JSON registry를
읽는 Gradle gate가 별도로 검사한다. 허용/금지 의존, 책임 범위, 테스트 명령 같은 **모듈 규칙**의
SSOT 는 [CLAUDE.md](CLAUDE.md) 다.
기본 composition은 `build.gradle`의 main project dependency와 registry의
`runtime_memberships=["app-bootstrap"]`가 정확히 일치해야 한다. GraphQL, gRPC, WebSocket,
MongoDB leaf의 membership은 비어 있어 두 shipped runtime에 포함되지 않는다. object storage는
sample runtime에만 있고 file server는 기본 app runtime에 이미 포함된다.
MongoDB와 object storage leaf의 membership은 비어 있어 production runtime에 포함되지 않는다.
file server는 기본 app runtime에 이미 포함된다.
`conditionalTransportTest`는 GraphQL/gRPC/WebSocket 세 leaf를 test-only classpath에서 함께
resolve하는 명시적 qualification composition이다. main `implementation`/`runtimeOnly` edge가 아니므로
@@ -870,10 +870,6 @@ ArchUnit 규칙이 **금지**하는 타입을 fixture 가 일부러 import 해
- `spring-cloud-context` — `@RefreshScope` 금지(no-refresh-scope) fixture 용. Spring Cloud 는
production 에 없고 규칙은 애너테이션을 FQN 문자열로 참조한다. BOM 이 spring-cloud 좌표를 관리하지
않아 버전을 명시 고정한다.
- **`testImplementation project(':sample-portfolio')`** — ArchUnit 이 템플릿 reference 구현을 분석하려고
test 경로에만 둔다. **production 은 절대 sample-portfolio 에 의존 금지** —
`production_code_does_not_depend_on_sample_portfolio` 규칙이 강제한다.
### 빌드 설정
- **test JVM UTC 고정(`-Duser.timezone=UTC`)** — `RuntimeHealthLifecycleContractTest`가
`TimeZone.getDefault().getID() == "UTC"`를 단언해, 호스트 로케일과 무관하게 타임스탬프 산술이
-38
View File
@@ -4,25 +4,6 @@ apply plugin: 'org.springframework.boot'
apply from: "${rootProject.projectDir}/gradle/strict-qualification-test.gradle"
// The sample fixture (sampleFixture -> sample-portfolio -> objectstorage) pulls software.amazon.awssdk:s3
// onto the sample-on test classpath; its version is managed by the AWS SDK v2 BOM (NOT the Spring Boot
// BOM). Import that BOM at this module's scope so the transitive s3 dependency resolves for the ArchUnit
// sample-on analysis. AWS-SDK version SSOT = ext.awsSdkVersion.
dependencyManagement {
imports {
mavenBom "software.amazon.awssdk:bom:${awsSdkVersion}"
}
}
// feature-sample-removal-adoption-contract D7 — ordinary tests may inspect the sample fixture,
// while sampleOffTest compiles the same core suite without the sample project on either classpath.
configurations {
sampleFixture {
canBeConsumed = false
canBeResolved = false
}
}
sourceSets {
sampleOffTest {
java.srcDirs = sourceSets.test.java.srcDirs
@@ -44,8 +25,6 @@ sourceSets {
}
configurations {
testCompileClasspath.extendsFrom sampleFixture
testRuntimeClasspath.extendsFrom sampleFixture
sampleOffTestImplementation.extendsFrom testImplementation
sampleOffTestCompileOnly.extendsFrom testCompileOnly
sampleOffTestRuntimeOnly.extendsFrom testRuntimeOnly
@@ -115,8 +94,6 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-json'
// test-only: parses checked-in Messaging capability registries for the fail-closed drift gate.
testImplementation 'org.yaml:snakeyaml'
// sample-on only: ArchUnit analyses the reference impl. sampleOffTest intentionally omits it.
sampleFixture project(':sample-portfolio')
// test-only: ArchUnit violation fixtures intentionally import forbidden types. See README.
testCompileOnly 'org.springframework:spring-tx'
// test-only: streaming/websocket violation fixtures import these forbidden packages. See README.
@@ -213,21 +190,6 @@ tasks.named('check') {
dependsOn tasks.named('functionalTest')
}
// feature-developer-experience-contract D2/D7 delegation: the DX entrypoint verifies production
// isolation and the sample-on build contract. sample-off is the separate sampleOffTest task.
tasks.register('bootstrapSampleContract', Test) {
group = 'developer experience'
description = 'Runs the delegated sample production-isolation/build contract for bootstrap.'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform()
filter {
includeTestsMatching 'dev.caskeleton.bootstrap.contract.SampleRemovalSmokeContractTest'
}
outputs.upToDateWhen { false }
jvmArgs '-Duser.timezone=UTC'
}
bootJar {
mainClass = 'dev.caskeleton.bootstrap.CaSkeletonApplication'
}
+26 -179
View File
@@ -1,42 +1,29 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
aopalliance:aopalliance:1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
aopalliance:aopalliance:1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0=compileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
ch.qos.logback:logback-classic:1.5.21=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
ch.qos.logback:logback-classic:1.5.34=sampleFixture
ch.qos.logback:logback-core:1.5.21=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
ch.qos.logback:logback-core:1.5.34=sampleFixture
com.approvaltests:approvaltests-util:31.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.approvaltests:approvaltests:31.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.ethlo.time:itu:1.14.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-annotations:2.20=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-annotations:2.21=sampleFixture
com.fasterxml.jackson.core:jackson-core:2.20.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-core:2.21.4=sampleFixture
com.fasterxml.jackson.core:jackson-databind:2.20.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-databind:2.21.4=sampleFixture
com.fasterxml.jackson.dataformat:jackson-dataformat-toml:2.21.4=sampleFixture
com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.20.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.21.4=sampleFixture
com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.21.4=sampleFixture
com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.20.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.21.4=sampleFixture
com.fasterxml.jackson.module:jackson-module-parameter-names:2.21.4=sampleFixture
com.fasterxml.jackson:jackson-bom:2.20.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson:jackson-bom:2.21.4=sampleFixture
com.fasterxml:classmate:1.7.1=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml:classmate:1.7.3=sampleFixture
com.github.ben-manes.caffeine:caffeine:3.2.3=annotationProcessor,conditionalTransportTestAnnotationProcessor,functionalTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.github.docker-java:docker-java-api:3.7.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.docker-java:docker-java-transport-zerodep:3.7.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.docker-java:docker-java-transport:3.7.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.f4b6a3:uuid-creator:6.1.1=sampleFixture,testRuntimeClasspath
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,conditionalTransportTestAnnotationProcessor,functionalTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
com.github.spotbugs:spotbugs-annotations:4.10.2=spotbugs
com.github.spotbugs:spotbugs-annotations:4.8.6=compileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
com.github.spotbugs:spotbugs:4.10.2=spotbugs
com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
com.github.stephenc.jcip:jcip-annotations:1.0-1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
com.google.android:annotations:4.1.1.4=conditionalTransportTestRuntimeClasspath
com.google.api.grpc:proto-google-common-protos:2.41.0=conditionalTransportTestRuntimeClasspath
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,conditionalTransportTestAnnotationProcessor,functionalTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
@@ -72,24 +59,19 @@ com.networknt:json-schema-validator:3.0.2=productionRuntimeClasspath,runtimeClas
com.nimbusds:content-type:2.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.nimbusds:lang-tag:1.7=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.nimbusds:nimbus-jose-jwt:10.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.nimbusds:nimbus-jose-jwt:9.37.4=sampleFixture
com.nimbusds:oauth2-oidc-sdk:11.26.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.puppycrawl.tools:checkstyle:13.5.0=checkstyle
com.squareup.okhttp3:okhttp-jvm:5.2.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.squareup.okhttp3:okhttp:4.12.0=sampleFixture
com.squareup.okhttp3:okhttp:5.2.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.squareup.okio:okio-jvm:3.16.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.squareup.okio:okio-jvm:3.6.0=sampleFixture
com.squareup.okio:okio:3.16.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.squareup.okio:okio:3.6.0=sampleFixture
com.sun.istack:istack-commons-runtime:4.1.2=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.sun.istack:istack-commons-runtime:4.1.2=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit-junit5-api:1.3.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit-junit5-engine-api:1.3.0=sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit-junit5-engine:1.3.0=sampleOffTestRuntimeClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit-junit5:1.3.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.tngtech.archunit:archunit:1.3.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.vaadin.external.google:android-json:0.0.20131108.vaadin1=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.zaxxer:HikariCP:6.3.3=sampleFixture
com.zaxxer:HikariCP:7.0.2=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
commons-beanutils:commons-beanutils:1.11.0=checkstyle
commons-codec:commons-codec:1.19.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -98,16 +80,16 @@ commons-io:commons-io:2.20.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeC
commons-io:commons-io:2.21.0=spotbugs
commons-logging:commons-logging:1.3.5=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
info.picocli:picocli:4.7.7=checkstyle
io.github.cdimascio:dotenv-java:3.0.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.cdimascio:dotenv-java:3.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,conditionalTransportTestAnnotationProcessor,functionalTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,conditionalTransportTestAnnotationProcessor,functionalTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
io.github.resilience4j:resilience4j-bulkhead:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-circuitbreaker:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-core:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-micrometer:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-ratelimiter:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-retry:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-timelimiter:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-bulkhead:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-circuitbreaker:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-core:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-micrometer:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-ratelimiter:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-retry:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.github.resilience4j:resilience4j-timelimiter:2.2.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.grpc:grpc-api:1.68.1=conditionalTransportTestRuntimeClasspath
io.grpc:grpc-context:1.68.1=conditionalTransportTestRuntimeClasspath
io.grpc:grpc-core:1.68.1=conditionalTransportTestRuntimeClasspath
@@ -118,21 +100,13 @@ io.grpc:grpc-services:1.68.1=conditionalTransportTestRuntimeClasspath
io.grpc:grpc-stub:1.68.1=conditionalTransportTestRuntimeClasspath
io.grpc:grpc-util:1.68.1=conditionalTransportTestRuntimeClasspath
io.lettuce:lettuce-core:6.8.1.RELEASE=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.micrometer:context-propagation:1.1.4=sampleFixture
io.micrometer:context-propagation:1.2.0=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-commons:1.15.12=sampleFixture
io.micrometer:micrometer-commons:1.16.0=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-core:1.15.12=sampleFixture
io.micrometer:micrometer-core:1.16.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-jakarta9:1.15.12=sampleFixture
io.micrometer:micrometer-jakarta9:1.16.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-observation:1.15.12=sampleFixture
io.micrometer:micrometer-observation:1.16.0=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-registry-prometheus:1.15.12=sampleFixture
io.micrometer:micrometer-registry-prometheus:1.16.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-tracing-bridge-otel:1.5.12=sampleFixture
io.micrometer:micrometer-tracing-bridge-otel:1.6.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-tracing:1.5.12=sampleFixture
io.micrometer:micrometer-tracing:1.6.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-buffer:4.2.17.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-codec-base:4.2.17.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
@@ -142,11 +116,8 @@ io.netty:netty-codec-dns:4.2.17.Final=productionRuntimeClasspath,runtimeClasspat
io.netty:netty-codec-http2:4.2.17.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-codec-http3:4.2.17.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-codec-http:4.2.17.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-codec-marshalling:4.2.17.Final=testRuntimeClasspath
io.netty:netty-codec-native-quic:4.2.17.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-codec-protobuf:4.2.17.Final=testRuntimeClasspath
io.netty:netty-codec-socks:4.2.17.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-codec:4.2.17.Final=testRuntimeClasspath
io.netty:netty-common:4.2.17.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-handler-proxy:4.2.17.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-handler:4.2.17.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
@@ -158,113 +129,78 @@ io.netty:netty-transport-classes-epoll:4.2.17.Final=productionRuntimeClasspath,r
io.netty:netty-transport-native-epoll:4.2.17.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-transport-native-unix-common:4.2.17.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.netty:netty-transport:4.2.17.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.opentelemetry.semconv:opentelemetry-semconv:1.32.0=sampleFixture
io.opentelemetry.semconv:opentelemetry-semconv:1.37.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-api:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-api:1.55.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-common:1.55.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-context:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-context:1.55.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-exporter-common:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-exporter-common:1.55.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-exporter-otlp-common:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-exporter-otlp-common:1.55.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-exporter-otlp:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-exporter-otlp:1.55.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.55.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-extension-trace-propagators:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-extension-trace-propagators:1.55.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk-common:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-sdk-common:1.55.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:1.55.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk-logs:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-sdk-logs:1.55.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk-metrics:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-sdk-metrics:1.55.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk-trace:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-sdk-trace:1.55.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk:1.49.0=sampleFixture
io.opentelemetry:opentelemetry-sdk:1.55.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.perfmark:perfmark-api:0.27.0=conditionalTransportTestRuntimeClasspath
io.projectreactor.netty:reactor-netty-core:1.3.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.projectreactor.netty:reactor-netty-http:1.3.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.projectreactor:reactor-core:3.7.19=sampleFixture
io.projectreactor:reactor-core:3.8.0=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-config:1.3.10=sampleFixture
io.prometheus:prometheus-metrics-config:1.4.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-core:1.3.10=sampleFixture
io.prometheus:prometheus-metrics-core:1.4.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-exposition-formats:1.3.10=sampleFixture
io.prometheus:prometheus-metrics-exposition-formats:1.4.3=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-exposition-textformats:1.3.10=sampleFixture
io.prometheus:prometheus-metrics-exposition-textformats:1.4.3=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-model:1.3.10=sampleFixture
io.prometheus:prometheus-metrics-model:1.4.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-tracer-common:1.3.10=sampleFixture
io.prometheus:prometheus-metrics-tracer-common:1.4.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
io.smallrye:jandex:3.2.0=sampleFixture
io.swagger.core.v3:swagger-annotations-jakarta:2.2.29=sampleFixture
io.swagger.core.v3:swagger-annotations-jakarta:2.2.38=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.swagger.core.v3:swagger-core-jakarta:2.2.29=sampleFixture
io.swagger.core.v3:swagger-core-jakarta:2.2.38=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
io.swagger.core.v3:swagger-models-jakarta:2.2.29=sampleFixture
io.swagger.core.v3:swagger-models-jakarta:2.2.38=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
jakarta.activation:jakarta.activation-api:2.1.4=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.annotation:jakarta.annotation-api:2.1.1=sampleFixture
jakarta.activation:jakarta.activation-api:2.1.4=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.annotation:jakarta.annotation-api:3.0.0=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.inject:jakarta.inject-api:2.0.1=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
jakarta.persistence:jakarta.persistence-api:3.1.0=sampleFixture
jakarta.inject:jakarta.inject-api:2.0.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
jakarta.persistence:jakarta.persistence-api:3.2.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.transaction:jakarta.transaction-api:2.0.1=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.validation:jakarta.validation-api:3.0.2=sampleFixture
jakarta.transaction:jakarta.transaction-api:2.0.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.validation:jakarta.validation-api:3.1.1=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.websocket:jakarta.websocket-api:2.2.0=sampleOffTestCompileClasspath,testCompileClasspath
jakarta.ws.rs:jakarta.ws.rs-api:4.0.0=sampleOffTestCompileClasspath,testCompileClasspath
jakarta.xml.bind:jakarta.xml.bind-api:4.0.4=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.xml.bind:jakarta.xml.bind-api:4.0.5=sampleFixture
javax.inject:javax.inject:1=annotationProcessor,conditionalTransportTestAnnotationProcessor,functionalTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
jaxen:jaxen:2.0.0=spotbugs
me.paulschwarz:spring-dotenv:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
me.paulschwarz:spring-dotenv:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy-agent:1.17.8=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.17.8=conditionalTransportTestCompileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.17.8=conditionalTransportTestCompileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.java.dev.jna:jna:5.18.1=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.logstash.logback:logstash-logback-encoder:8.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.minidev:accessors-smart:2.6.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.minidev:json-smart:2.6.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.sf.saxon:Saxon-HE:12.9=checkstyle,spotbugs
org.antlr:antlr4-runtime:4.13.2=checkstyle,productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.antlr:antlr4-runtime:4.13.2=checkstyle,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.bcel:bcel:6.12.0=spotbugs
org.apache.commons:commons-compress:1.28.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.commons:commons-lang3:3.20.0=checkstyle,productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
org.apache.commons:commons-lang3:3.20.0=checkstyle,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
org.apache.commons:commons-text:1.15.0=spotbugs
org.apache.commons:commons-text:1.3=checkstyle
org.apache.httpcomponents.client5:httpclient5:5.5.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.apache.httpcomponents.core5:httpcore5-h2:5.3.6=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.apache.httpcomponents.core5:httpcore5:5.3.6=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.apache.httpcomponents:httpclient:4.5.13=checkstyle,testRuntimeClasspath
org.apache.httpcomponents:httpcore:4.4.16=checkstyle,testRuntimeClasspath
org.apache.httpcomponents:httpclient:4.5.13=checkstyle
org.apache.httpcomponents:httpcore:4.4.16=checkstyle
org.apache.kafka:kafka-clients:4.1.1=sampleOffTestCompileClasspath,testCompileClasspath
org.apache.logging.log4j:log4j-api:2.24.3=sampleFixture
org.apache.logging.log4j:log4j-api:2.25.2=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
org.apache.logging.log4j:log4j-core:2.25.2=spotbugs
org.apache.logging.log4j:log4j-to-slf4j:2.24.3=sampleFixture
org.apache.logging.log4j:log4j-to-slf4j:2.25.2=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.maven.doxia:doxia-core:1.12.0=checkstyle
org.apache.maven.doxia:doxia-logging-api:1.12.0=checkstyle
org.apache.maven.doxia:doxia-module-xdoc:1.12.0=checkstyle
org.apache.maven.doxia:doxia-sink-api:1.12.0=checkstyle
org.apache.tomcat.embed:tomcat-embed-core:10.1.55=sampleFixture
org.apache.tomcat.embed:tomcat-embed-core:11.0.14=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-el:10.1.55=sampleFixture
org.apache.tomcat.embed:tomcat-embed-el:11.0.14=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-websocket:10.1.55=sampleFixture
org.apache.tomcat.embed:tomcat-embed-websocket:11.0.14=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.xbean:xbean-reflect:3.7=checkstyle
org.apiguardian:apiguardian-api:1.1.2=conditionalTransportTestCompileClasspath,functionalTestCompileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
org.aspectj:aspectjweaver:1.9.25=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.aspectj:aspectjweaver:1.9.25.1=sampleFixture
org.assertj:assertj-core:3.27.6=conditionalTransportTestCompileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.awaitility:awaitility:4.3.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.checkerframework:checker-qual:3.42.0=conditionalTransportTestRuntimeClasspath
@@ -275,7 +211,7 @@ org.codehaus.plexus:plexus-component-annotations:2.1.0=checkstyle
org.codehaus.plexus:plexus-container-default:2.1.0=checkstyle
org.codehaus.plexus:plexus-utils:3.3.0=checkstyle
org.dom4j:dom4j:2.2.0=spotbugs
org.eclipse.angus:angus-activation:2.0.3=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.eclipse.angus:angus-activation:2.0.3=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.eclipse.jetty.compression:jetty-compression-common:12.1.4=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.eclipse.jetty.compression:jetty-compression-gzip:12.1.4=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-alpn-client:12.1.4=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
@@ -284,32 +220,19 @@ org.eclipse.jetty:jetty-http:12.1.4=productionRuntimeClasspath,runtimeClasspath,
org.eclipse.jetty:jetty-io:12.1.4=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-util:12.1.4=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.flywaydb:flyway-core:11.14.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.flywaydb:flyway-core:11.7.2=sampleFixture
org.flywaydb:flyway-database-postgresql:11.14.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.flywaydb:flyway-database-postgresql:11.7.2=sampleFixture
org.glassfish.jaxb:jaxb-core:4.0.6=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.glassfish.jaxb:jaxb-core:4.0.9=sampleFixture
org.glassfish.jaxb:jaxb-runtime:4.0.6=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.glassfish.jaxb:jaxb-runtime:4.0.9=sampleFixture
org.glassfish.jaxb:txw2:4.0.6=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.glassfish.jaxb:txw2:4.0.9=sampleFixture
org.hamcrest:hamcrest:3.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.hdrhistogram:HdrHistogram:2.2.2=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.hibernate.common:hibernate-commons-annotations:7.0.3.Final=sampleFixture
org.hdrhistogram:HdrHistogram:2.2.2=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.hibernate.models:hibernate-models:1.0.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.hibernate.orm:hibernate-core:6.6.53.Final=sampleFixture
org.hibernate.orm:hibernate-core:7.1.8.Final=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.hibernate.validator:hibernate-validator:8.0.3.Final=sampleFixture
org.hibernate.validator:hibernate-validator:9.0.1.Final=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.javassist:javassist:3.28.0-GA=checkstyle
org.jboss.logging:jboss-logging:3.6.1.Final=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jboss.logging:jboss-logging:3.6.3.Final=sampleFixture
org.jetbrains.kotlin:kotlin-stdlib-common:1.9.25=sampleFixture
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.25=sampleFixture
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.25=sampleFixture
org.jetbrains.kotlin:kotlin-stdlib:1.9.25=sampleFixture
org.jetbrains.kotlin:kotlin-stdlib:2.2.21=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.jetbrains:annotations:13.0=productionRuntimeClasspath,runtimeClasspath,sampleFixture
org.jetbrains:annotations:13.0=productionRuntimeClasspath,runtimeClasspath
org.jetbrains:annotations:17.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jspecify:jspecify:1.0.0=annotationProcessor,checkstyle,compileClasspath,conditionalTransportTestAnnotationProcessor,conditionalTransportTestCompileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestAnnotationProcessor,functionalTestCompileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestAnnotationProcessor,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:6.0.1=conditionalTransportTestCompileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -322,11 +245,11 @@ org.junit.platform:junit-platform-launcher:6.0.1=conditionalTransportTestRuntime
org.junit.platform:junit-platform-testkit:6.0.1=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit:junit-bom:6.0.1=conditionalTransportTestCompileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit:junit-bom:6.1.0=spotbugs
org.latencyutils:LatencyUtils:2.0.3=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.latencyutils:LatencyUtils:2.0.3=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.mockito:mockito-core:5.20.0=mockitoAgent,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-junit-jupiter:5.20.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.objenesis:objenesis:3.3=sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.openapitools:jackson-databind-nullable:0.2.6=productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.openapitools:jackson-databind-nullable:0.2.6=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=conditionalTransportTestCompileClasspath,conditionalTransportTestRuntimeClasspath,functionalTestCompileClasspath,functionalTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.osgi:org.osgi.annotation.bundle:2.0.0=compileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
org.osgi:org.osgi.annotation.versioning:1.1.2=compileClasspath,sampleOffTestCompileClasspath,testCompileClasspath
@@ -339,26 +262,18 @@ org.ow2.asm:asm-util:9.10.1=spotbugs
org.ow2.asm:asm:9.10.1=spotbugs
org.ow2.asm:asm:9.7.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.pcollections:pcollections:4.0.1=annotationProcessor,conditionalTransportTestAnnotationProcessor,functionalTestAnnotationProcessor,sampleOffTestAnnotationProcessor,testAnnotationProcessor
org.postgresql:postgresql:42.7.11=sampleFixture
org.postgresql:postgresql:42.7.8=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.reactivestreams:reactive-streams:1.0.4=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleFixture,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.reactivestreams:reactive-streams:1.0.4=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.reflections:reflections:0.10.2=checkstyle
org.rnorth.duct-tape:duct-tape:1.0.8=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.skyscreamer:jsonassert:1.5.3=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.slf4j:jul-to-slf4j:2.0.17=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.slf4j:jul-to-slf4j:2.0.18=sampleFixture
org.slf4j:slf4j-api:2.0.17=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,spotbugs,spotbugsSlf4j,testCompileClasspath,testRuntimeClasspath
org.slf4j:slf4j-api:2.0.18=sampleFixture
org.slf4j:slf4j-simple:2.0.17=checkstyle,spotbugsSlf4j
org.springdoc:springdoc-openapi-starter-common:2.8.6=sampleFixture
org.springdoc:springdoc-openapi-starter-common:3.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.6=sampleFixture
org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-actuator-autoconfigure:3.5.16=sampleFixture
org.springframework.boot:spring-boot-actuator-autoconfigure:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-actuator:3.5.16=sampleFixture
org.springframework.boot:spring-boot-actuator:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-autoconfigure:3.5.16=sampleFixture
org.springframework.boot:spring-boot-autoconfigure:4.0.0=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-configuration-processor:4.0.0=annotationProcessor
org.springframework.boot:spring-boot-data-commons:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -384,37 +299,26 @@ org.springframework.boot:spring-boot-security-oauth2-resource-server:4.0.0=produ
org.springframework.boot:spring-boot-security:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-servlet:4.0.0=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-sql:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-actuator:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter-actuator:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-data-jpa:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter-data-jpa:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-flyway:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-graphql:4.0.0=conditionalTransportTestRuntimeClasspath
org.springframework.boot:spring-boot-starter-jackson-test:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-jackson:4.0.0=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-jdbc:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter-jdbc:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-json:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter-json:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-logging:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter-logging:4.0.0=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-micrometer-metrics:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-oauth2-resource-server:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter-oauth2-resource-server:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-security:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter-security:4.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-test:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-tomcat-runtime:4.0.0=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-tomcat:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter-tomcat:4.0.0=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-validation:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter-validation:4.0.0=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-web:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter-web:4.0.0=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-webmvc-test:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-webmvc:4.0.0=conditionalTransportTestRuntimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-websocket:4.0.0=conditionalTransportTestRuntimeClasspath
org.springframework.boot:spring-boot-starter:3.5.16=sampleFixture
org.springframework.boot:spring-boot-starter:4.0.0=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-test-autoconfigure:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-test:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -425,62 +329,36 @@ org.springframework.boot:spring-boot-web-server:4.0.0=conditionalTransportTestRu
org.springframework.boot:spring-boot-webmvc-test:4.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-webmvc:4.0.0=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-websocket:4.0.0=conditionalTransportTestRuntimeClasspath
org.springframework.boot:spring-boot:3.5.16=sampleFixture
org.springframework.boot:spring-boot:4.0.0=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.cloud:spring-cloud-context:4.1.4=sampleOffTestCompileClasspath,testCompileClasspath
org.springframework.data:spring-data-commons:3.5.13=sampleFixture
org.springframework.data:spring-data-commons:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.data:spring-data-jpa:3.5.13=sampleFixture
org.springframework.data:spring-data-jpa:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.graphql:spring-graphql:2.0.0=conditionalTransportTestRuntimeClasspath
org.springframework.integration:spring-integration-core:6.5.10=sampleFixture
org.springframework.integration:spring-integration-core:7.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.integration:spring-integration-jdbc:6.5.10=sampleFixture
org.springframework.integration:spring-integration-jdbc:7.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.retry:spring-retry:2.0.13=sampleFixture
org.springframework.security:spring-security-config:6.5.11=sampleFixture
org.springframework.security:spring-security-config:7.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-core:6.5.11=sampleFixture
org.springframework.security:spring-security-core:7.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-crypto:6.5.11=sampleFixture
org.springframework.security:spring-security-crypto:7.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-oauth2-client:7.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-oauth2-core:6.5.11=sampleFixture
org.springframework.security:spring-security-oauth2-core:7.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-oauth2-jose:6.5.11=sampleFixture
org.springframework.security:spring-security-oauth2-jose:7.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.security:spring-security-oauth2-resource-server:6.5.11=sampleFixture
org.springframework.security:spring-security-oauth2-resource-server:7.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework.security:spring-security-test:7.0.0=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-web:6.5.11=sampleFixture
org.springframework.security:spring-security-web:7.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.session:spring-session-core:4.0.0=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework:spring-aop:6.2.19=sampleFixture
org.springframework:spring-aop:7.0.1=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-aspects:6.2.19=sampleFixture
org.springframework:spring-aspects:7.0.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-beans:6.2.19=sampleFixture
org.springframework:spring-beans:7.0.1=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-context:6.2.19=sampleFixture
org.springframework:spring-context:7.0.1=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-core:6.2.19=sampleFixture
org.springframework:spring-core:7.0.1=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-expression:6.2.19=sampleFixture
org.springframework:spring-expression:7.0.1=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-jcl:6.2.19=sampleFixture
org.springframework:spring-jdbc:6.2.19=sampleFixture
org.springframework:spring-jdbc:7.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-messaging:6.2.19=sampleFixture
org.springframework:spring-messaging:7.0.1=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-orm:6.2.19=sampleFixture
org.springframework:spring-orm:7.0.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-test:7.0.1=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-tx:6.2.19=sampleFixture
org.springframework:spring-tx:7.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-web:6.2.19=sampleFixture
org.springframework:spring-web:7.0.1=compileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-webflux:7.0.1=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
org.springframework:spring-webmvc:6.2.19=sampleFixture
org.springframework:spring-webmvc:7.0.1=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-websocket:7.0.1=conditionalTransportTestRuntimeClasspath,sampleOffTestCompileClasspath,testCompileClasspath
org.testcontainers:testcontainers-database-commons:2.0.2=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -490,39 +368,8 @@ org.testcontainers:testcontainers-postgresql:2.0.2=sampleOffTestCompileClasspath
org.testcontainers:testcontainers:2.0.2=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.xmlresolver:xmlresolver:5.3.3=checkstyle,spotbugs
org.xmlunit:xmlunit-core:2.10.4=sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.yaml:snakeyaml:2.4=sampleFixture
org.yaml:snakeyaml:2.5=compileClasspath,conditionalTransportTestCompileClasspath,conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
redis.clients.authentication:redis-authx-core:0.1.1-beta2=productionRuntimeClasspath,runtimeClasspath,sampleOffTestRuntimeClasspath,testRuntimeClasspath
software.amazon.awssdk:annotations:2.30.0=testRuntimeClasspath
software.amazon.awssdk:apache-client:2.30.0=testRuntimeClasspath
software.amazon.awssdk:arns:2.30.0=testRuntimeClasspath
software.amazon.awssdk:auth:2.30.0=testRuntimeClasspath
software.amazon.awssdk:aws-core:2.30.0=testRuntimeClasspath
software.amazon.awssdk:aws-query-protocol:2.30.0=testRuntimeClasspath
software.amazon.awssdk:aws-xml-protocol:2.30.0=testRuntimeClasspath
software.amazon.awssdk:checksums-spi:2.30.0=testRuntimeClasspath
software.amazon.awssdk:checksums:2.30.0=testRuntimeClasspath
software.amazon.awssdk:crt-core:2.30.0=testRuntimeClasspath
software.amazon.awssdk:endpoints-spi:2.30.0=testRuntimeClasspath
software.amazon.awssdk:http-auth-aws-eventstream:2.30.0=testRuntimeClasspath
software.amazon.awssdk:http-auth-aws:2.30.0=testRuntimeClasspath
software.amazon.awssdk:http-auth-spi:2.30.0=testRuntimeClasspath
software.amazon.awssdk:http-auth:2.30.0=testRuntimeClasspath
software.amazon.awssdk:http-client-spi:2.30.0=testRuntimeClasspath
software.amazon.awssdk:identity-spi:2.30.0=testRuntimeClasspath
software.amazon.awssdk:json-utils:2.30.0=testRuntimeClasspath
software.amazon.awssdk:metrics-spi:2.30.0=testRuntimeClasspath
software.amazon.awssdk:netty-nio-client:2.30.0=testRuntimeClasspath
software.amazon.awssdk:profiles:2.30.0=testRuntimeClasspath
software.amazon.awssdk:protocol-core:2.30.0=testRuntimeClasspath
software.amazon.awssdk:regions:2.30.0=testRuntimeClasspath
software.amazon.awssdk:retries-spi:2.30.0=testRuntimeClasspath
software.amazon.awssdk:retries:2.30.0=testRuntimeClasspath
software.amazon.awssdk:s3:2.30.0=testRuntimeClasspath
software.amazon.awssdk:sdk-core:2.30.0=testRuntimeClasspath
software.amazon.awssdk:third-party-jackson-core:2.30.0=testRuntimeClasspath
software.amazon.awssdk:utils:2.30.0=testRuntimeClasspath
software.amazon.eventstream:eventstream:1.0.1=testRuntimeClasspath
tools.jackson.core:jackson-core:3.0.2=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson.core:jackson-databind:3.0.2=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson:jackson-bom:3.0.2=conditionalTransportTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,sampleOffTestCompileClasspath,sampleOffTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -502,11 +502,8 @@ ca-skeleton:
# alternative). Keys are RAW IdP role names (no ROLE_ prefix — that prefix only
# exists on Spring authorities, not on the principal's raw role set), looked up
# case-insensitively. Values are explicitly enumerated `resource:action` permissions
# (no wildcards — least-privilege, OWASP-AUTHZ-C4). The values below are the
# sample-portfolio demonstration (§5): `user` may read/write, only `admin` may close.
role-permissions:
user: worklog:read,worklog:write
admin: worklog:read,worklog:write,worklog:close
# (no wildcards — least-privilege, OWASP-AUTHZ-C4). Product-specific roles belong here.
role-permissions: {}
cors:
# true | false
enabled: ${APP_SECURITY_CORS_ENABLED}
@@ -233,38 +233,6 @@ class CleanArchitectureTest {
+ "outbound adapters")
.allowEmptyShould(true);
@ArchTest
static final ArchRule SAMPLE_APPLICATION_HAS_NO_SLF4J =
noClasses()
.that()
.resideInAPackage("dev.caskeleton.sample.portfolio.application..")
.should()
.dependOnClassesThat()
.resideInAnyPackage("org.slf4j..")
.as(
"SAMPLE_APPLICATION_HAS_NO_SLF4J: sample application collaborators read "
+ "correlation context through application-core CorrelationIdPort; inbound "
+ "adapters own MDC")
.allowEmptyShould(true);
@ArchTest
static final ArchRule SAMPLE_APPLICATION_LEGACY_OBJECT_STORAGE_IMPORT_IS_FROZEN =
noClasses()
.that()
.resideInAPackage("dev.caskeleton.sample.portfolio.application..")
.and()
.doNotHaveFullyQualifiedName(
"dev.caskeleton.sample.portfolio.application.poster.UploadPosterImageUseCase")
.should()
.dependOnClassesThat()
.resideInAPackage("dev.caskeleton.application.storage")
.as(
"Only the characterized UploadPosterImageUseCase may temporarily import the legacy "
+ "ObjectStoragePort/StoredObject contract. New sample business code must use "
+ "the semantic objectstorage ports. Task 23 moves compatibility into the exact "
+ "legacy slice; Task 24 separately reviews the administrative migration seam.")
.allowEmptyShould(true);
@ArchTest
static final ArchRule APPLICATION_DOES_NOT_USE_SPRING_TRANSACTIONAL_ANNOTATION =
noClasses()
@@ -1844,7 +1812,7 @@ class CleanArchitectureTest {
.haveRawType(assignableTo(ResourceId.class))
.as(
"D17 NO_LONG_ID_PK: a domain entity 'id' field must be a ResourceId value object"
+ " (e.g. WorkLogId), never Long/long/int/Integer"
+ " (e.g. ArticleId), never Long/long/int/Integer"
+ " (feature-resource-identifier-contract D17). JPA @Id UUID columns in"
+ " ..adapter.outbound.persistence.. are out of scope — they store the UUID as"
+ " the PostgreSQL native uuid type per D10.")
@@ -1863,7 +1831,7 @@ class CleanArchitectureTest {
.as(
"D5/D17 NO_UUID_RANDOM_IN_CONTROLLER: controllers and the application layer "
+ "(use cases / services) must not generate resource identifiers directly "
+ "(UUID.randomUUID / UuidCreator) — inject the domain WorkLogIdFactory port "
+ "(UUID.randomUUID / UuidCreator) — inject a domain identifier factory port "
+ "instead (feature-resource-identifier-contract D5/D17). Scope follows the "
+ "D17 decision text (controller / service / use case); request/trace-id "
+ "generation in web filters is a distinct concern owned by D18 / "
@@ -24,9 +24,9 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
class ConditionalTransportQualificationContractTest {
// 29 pre-existing controls plus the eight release-blocking HTTP Client Platform gates
// 28 pre-existing controls plus the nine HTTP Client Platform gates
// registered in .github/ci-gate-matrix.yml (design §38).
private static final int EXPECTED_GATE_COUNT = 38;
private static final int EXPECTED_GATE_COUNT = 37;
/**
* Filler gates a well-formed fixture needs beside its one target gate.
@@ -74,7 +74,6 @@ class ConditionalTransportQualificationContractTest {
"filesystem-vulnerability-scan",
"documentation-links",
"object-storage-minio-managed-contract",
"poster-image-migration",
"object-storage-minio-managed-fault",
"object-storage-aws-protected-qualification",
"redis-sdk-support-matrix",
@@ -474,16 +473,12 @@ class ConditionalTransportQualificationContractTest {
}
assertThat(ids).containsExactlyInAnyOrderElementsOf(EXPECTED_GATE_IDS);
Map<?, ?> posterGate =
gates.stream()
.map(Map.class::cast)
.filter(gate -> "poster-image-migration".equals(gate.get("id")))
.findFirst()
.orElseThrow(() -> new AssertionError("missing Poster image migration gate"));
assertThat(requireString(posterGate, "ref")).isEqualTo("posterImageMigrationTest");
assertThat(requireString(posterGate, "workflow")).isEqualTo("object-storage-qualification.yml");
assertThat(requireString(posterGate, "job")).isEqualTo("poster-image-v7-migration");
assertThat(requireString(posterGate, "execution")).isEqualTo("explicit");
String objectStorageWorkflow =
Files.readString(
repositoryRoot().resolve(".github/workflows/object-storage-qualification.yml"));
assertThat(objectStorageWorkflow)
.doesNotContain("poster-image-v7-migration")
.doesNotContain(":sample-portfolio:posterImageMigrationTest");
}
private static void assertRejectedAsNotExplicit(ScriptResult result) {
@@ -20,10 +20,7 @@ import org.junit.jupiter.api.Test;
*
* <ul>
* <li>Gates 19 are the base contracts verified by this test.
* <li>Gate #10 (OpenAPI drift) lives in {@code sample-portfolio} and is verified by its own
* module test. It is not loadable from {@code app-bootstrap}'s classpath and is therefore
* intentionally excluded here.
* <li>Gate #11 (sample removal smoke) is {@code
* <li>Gate #10 (sample removal smoke) is {@code
* dev.caskeleton.bootstrap.contract.SampleRemovalSmokeContractTest}, which is included in the
* 9-base enumeration below as the envelope/schema gate covers it (see concern mapping).
* </ul>
@@ -81,21 +81,20 @@ class DeveloperExperienceContractTest {
}
@Test
void bootstrapIsOneOrderedFiveStageGradleEntrypoint() throws IOException {
void bootstrapIsOneOrderedFourStageGradleEntrypoint() throws IOException {
String build = read("src/build.gradle");
assertThat(build)
.contains("tasks.register('bootstrapCompile')")
.contains("tasks.register('bootstrapDependencies'")
.contains("tasks.register('bootstrapMigrateAndStart'")
.contains("tasks.register('bootstrapSampleContract'")
.contains("tasks.register('bootstrapSmoke'")
.contains("tasks.register('bootstrap')");
.contains("tasks.register('bootstrap')")
.doesNotContain("tasks.register('bootstrapSampleContract'");
assertThat(build)
.contains("bootstrapDependencies.configure { dependsOn bootstrapCompile }")
.contains("bootstrapMigrateAndStart.configure { dependsOn bootstrapDependencies }")
.contains("bootstrapSampleContract.configure { dependsOn bootstrapMigrateAndStart }")
.contains("bootstrapSmoke.configure { dependsOn bootstrapSampleContract }");
.contains("bootstrapSmoke.configure { dependsOn bootstrapMigrateAndStart }");
}
@Test
@@ -807,17 +806,12 @@ class DeveloperExperienceContractTest {
@Test
void dockerDependencyCacheStagesDeclareTheCompleteConfigurationRegistry() throws IOException {
for (String dockerfile : new String[] {"src/Dockerfile", "src/Dockerfile.sample"}) {
assertDockerDependencyCacheContract(dockerfile, read(dockerfile));
}
assertDockerDependencyCacheContract("src/Dockerfile", read("src/Dockerfile"));
}
@Test
void dockerBuildsConsumeOnlyGradleStagedExecutableJars() throws IOException {
Map<String, String> executableModules =
Map.of(
"src/Dockerfile", "app-bootstrap",
"src/Dockerfile.sample", "sample-portfolio");
Map<String, String> executableModules = Map.of("src/Dockerfile", "app-bootstrap");
executableModules.forEach(
(dockerfile, module) -> {
@@ -10,26 +10,23 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.yaml.snakeyaml.Yaml;
/**
* Gate #11 (sample-removal smoke) of the contract-verification suite. The skeleton ships a
* reference domain ({@code sample-portfolio}) that remains in the template as a fixture. Production
* code and the sample-off contract build must not depend on it. A downstream adopter may delete the
* module only after the sample-off build is green.
* Gate #10 (sample-removal smoke) of the contract-verification suite. The skeleton's concrete
* reference domain has been removed. Production code and the sample-off contract build must remain
* independent of it so deleted sample code cannot re-enter the build or runtime by accident.
*
* <p>The contract is a build/test classpath matrix, not a Spring runtime profile:
*
* <ol>
* <li>production modules reference the sample only through the app-bootstrap sample fixture
* configuration;
* <li>the concrete sample module, Dockerfile, registry leaf, and build dependency are absent;
* <li>app-bootstrap core test sources compile without importing sample types;
* <li>the sample-off source set removes the fixture from its compile/runtime classpaths;
* <li>sample-on and sample-off are both release-blocking CI jobs.
* <li>the sample-off source set proves the minimal bootstrap classpath remains sample-free;
* <li>sample-off remains a release-blocking CI job.
* </ol>
*
* <p>The build-graph scan is the no-datasource proxy for "core app/context boots green without the
@@ -39,40 +36,49 @@ import org.yaml.snakeyaml.Yaml;
*/
class SampleRemovalSmokeContractTest {
/** A Gradle dependency line: {@code <config> project(':sample-portfolio')}. */
private static final Pattern SAMPLE_DEP =
Pattern.compile("^\\s*([A-Za-z]+)\\s+project\\(\\s*['\"]:sample-portfolio['\"]\\s*\\)");
@Test
void productionModulesReferenceSampleOnlyAsTestFixtureDependency() throws IOException {
void concreteSampleModuleRegistryBuildEvidenceAndDockerfileAreAbsent() throws IOException {
RepositoryContractResources resources = RepositoryContractResources.fromSystemProperty();
Path root = resources.repositoryRoot();
assertThat(root.resolve("src/sample-portfolio/build.gradle")).doesNotExist();
assertThat(root.resolve("src/sample-portfolio/src/main")).doesNotExist();
assertThat(root.resolve("src/Dockerfile.sample")).doesNotExist();
List<String> violations = new ArrayList<>();
for (String sourcePath : registeredProductionSourcePaths(resources)) {
for (String sourcePath : registeredSourcePathsWithoutSample(resources)) {
Path buildFile = resources.requireTrackedFile(sourcePath + "/build.gradle");
for (String line : Files.readAllLines(buildFile)) {
Matcher m = SAMPLE_DEP.matcher(line);
if (m.find() && !m.group(1).startsWith("test") && !m.group(1).equals("sampleFixture")) {
violations.add(
sourcePath
+ ": '"
+ line.trim()
+ "' (configuration '"
+ m.group(1)
+ "' is not test-scoped)");
if (SAMPLE_DEP.matcher(line).find()) {
violations.add(sourcePath + ": '" + line.trim() + "'");
}
}
}
assertThat(violations)
.as(
"sample-portfolio must be a test-only dependency of every production module so "
+ "the sample can be removed without breaking the production build/runtime")
.as("no registered module may retain a Gradle dependency on the deleted sample")
.isEmpty();
String rootBuild = Files.readString(resources.requireTrackedFile("src/build.gradle"));
assertThat(rootBuild)
.doesNotContain(":sample-portfolio:messagingSampleContractQualificationTest")
.doesNotContain(
"sample-portfolio/src/main/resources/contracts/messaging/portfolio.worklog.reserved/");
String application =
Files.readString(
resources.requireTrackedFile("src/app-bootstrap/src/main/resources/application.yml"));
assertThat(application)
.contains("role-permissions: {}")
.doesNotContain("user: worklog:read,worklog:write")
.doesNotContain("admin: worklog:read,worklog:write,worklog:close");
}
private static List<String> registeredProductionSourcePaths(RepositoryContractResources resources)
throws IOException {
private static List<String> registeredSourcePathsWithoutSample(
RepositoryContractResources resources) throws IOException {
Path registry = resources.requireTrackedFile("src/config/architecture/modules.json");
final Map<String, Object> root;
try (InputStream input = Files.newInputStream(registry)) {
@@ -81,14 +87,17 @@ class SampleRemovalSmokeContractTest {
@SuppressWarnings("unchecked")
List<Map<String, Object>> modules = (List<Map<String, Object>>) root.get("modules");
assertThat(modules).as("architecture registry must declare its leaf modules").isNotEmpty();
assertThat(modules.stream().filter(row -> "sample-portfolio".equals(row.get("id"))).count())
.as("architecture registry must declare exactly one sample fixture leaf")
.isEqualTo(1);
assertThat(root.get("runtime_compositions"))
.as("only the production composition remains")
.isEqualTo(List.of("app-bootstrap"));
assertThat(modules).noneMatch(row -> "sample-portfolio".equals(row.get("id")));
assertThat(modules)
.allMatch(
row ->
!((List<?>) row.get("allowed_dependencies")).contains("sample-portfolio")
&& !((List<?>) row.get("runtime_memberships")).contains("sample-portfolio"));
return modules.stream()
.filter(row -> !"sample-portfolio".equals(row.get("id")))
.map(row -> (String) row.get("source_path"))
.toList();
return modules.stream().map(row -> (String) row.get("source_path")).toList();
}
@Test
@@ -117,9 +126,7 @@ class SampleRemovalSmokeContractTest {
}
assertThat(imports)
.as(
"app-bootstrap core tests must compile without sample-portfolio; sample-specific "
+ "contract checks belong in the sample module")
.as("app-bootstrap core tests must compile without imports from the deleted sample module")
.isEmpty();
}
@@ -129,8 +136,8 @@ class SampleRemovalSmokeContractTest {
Path buildFile = resources.requireTrackedFile("src/app-bootstrap/build.gradle");
String build = Files.readString(buildFile);
assertThat(build).contains("sampleFixture project(':sample-portfolio')");
assertThat(build)
.doesNotContain("sampleFixture")
.contains("java.srcDirs = sourceSets.test.java.srcDirs")
.contains("java.srcDir 'src/sampleOffTest/java'")
.contains("tasks.register('sampleOffCompile')")
@@ -31,9 +31,9 @@ import org.junit.jupiter.api.Test;
* <li><strong>Red test:</strong> loads only the violation fixture ({@code
* PiiViolationEventFixture} — a {@code @DomainEvent} with an {@code email} field) and asserts
* the rule fires.
* <li><strong>Green test:</strong> loads all production + sample classes (excluding the
* violations package) and asserts the rule does NOT fire — i.e., no real event class carries
* a PII field name.
* <li><strong>Green test:</strong> loads all production classes (excluding the violations
* package) and asserts the rule does NOT fire — i.e., no real event class carries a PII field
* name.
* </ul>
*/
class EventPayloadPiiContractTest {
@@ -99,11 +99,11 @@ class EventPayloadPiiContractTest {
// ---- Green test: production classes must pass the rule ----
/**
* Production + sample-portfolio classes excluding the violations fixture package.
* Production classes excluding the violations fixture package.
*
* <p>Scans {@code dev.caskeleton} broadly (covers domain-core, application-core,
* adapter-outbound, sample-portfolio all on the test classpath) but excludes the {@code
* violations} package which intentionally contains broken fixtures.
* <p>Scans {@code dev.caskeleton} broadly (covers domain-core, application-core, adapter-outbound
* modules on the test classpath) but excludes the {@code violations} package which intentionally
* contains broken fixtures.
*/
private static final JavaClasses PRODUCTION_CLASSES =
new ClassFileImporter()
@@ -88,8 +88,7 @@ class DistributedLockProviderContractTest {
sharedDataSource = hikari(pg);
// Flyway migrates V1 (idempotency), V2 (work_log from sample-portfolio on
// app-bootstrap test classpath), V3 (outbox), V4 (INT_LOCK).
// Flyway migrates the production schemas before lock table support is exercised.
Flyway.configure()
.dataSource(sharedDataSource)
.locations("classpath:db/migration/postgresql")
@@ -54,10 +54,7 @@ final class OutboxContainerTestSupport {
"Docker not available — skipping Testcontainers-based outbox integration test");
}
/**
* Runs Flyway migrations against the given DataSource. All three migrations (V1 idempotency, V2
* work_log from sample-portfolio on app-bootstrap test classpath, V3 outbox_event) are applied.
*/
/** Runs all production Flyway migrations against the given DataSource. */
static void migrate(DataSource dataSource) {
Flyway.configure()
.dataSource(dataSource)
@@ -28,20 +28,15 @@ import org.springframework.web.bind.annotation.RestController;
/**
* Regression guard for the skeleton's operational envelope/error contract.
*
* <p>The audit motivation: this contract used to live only in the disposable {@code
* sample-portfolio} module, which {@code app-bootstrap} does NOT depend on at runtime ({@code
* bootJar} mainClass {@code CaSkeletonApplication}). The contract beans now live in {@code
* adapter-web} (a production dependency of {@code app-bootstrap}). This test pins that {@link
* CaSkeletonApplication}'s component scan ({@code scanBasePackages = "dev.caskeleton"}) actually
* wires them into the running application, so a future change cannot silently drop the contract
* from the real app again.
* <p>The contract beans live in {@code adapter-web}, a production dependency of {@code
* app-bootstrap}. This test pins that {@link CaSkeletonApplication}'s component scan ({@code
* scanBasePackages = "dev.caskeleton"}) actually wires them into the running application, so a
* future change cannot silently drop the contract from the real app again.
*
* <p>Why a {@code @WebMvcTest} slice rather than a full {@code @SpringBootTest}: a full boot of
* {@code CaSkeletonApplication} requires the ~50 {@code ${...}} env vars wired in {@code
* application.yml} (datasource, OIDC issuer, etc.) from {@code src/.env}, and {@code
* sample-portfolio} on the test classpath drags in Spring Data JPA / Security / OAuth2
* autoconfiguration plus JPA entities and repositories that need a live datasource and JWT issuer.
* The MVC slice gives a real, {@code CaSkeletonApplication}-anchored web context without that cost.
* application.yml} (datasource, OIDC issuer, etc.) from {@code src/.env}. The MVC slice gives a
* real, {@code CaSkeletonApplication}-anchored web context without that cost.
*
* <p>The slice scans {@code CaSkeletonApplication}'s package for {@code @ControllerAdvice} beans,
* so it picks up the advices via the same component-scan path the running app uses. The raw-{@code
+3 -4
View File
@@ -212,15 +212,14 @@ The read side has two equally-valid shapes; pick per read, do not force one:
| Shape | Returns | When | How |
|---|---|---|---|
| **Through-aggregate** (default for simple reads) | domain aggregate via a `*Repository` port | read shape == write aggregate **and** the aggregate is the minimal invariant boundary (no lazy collections needed) | `QueryUseCase` → repository port → `WorkLog` |
| **Projection (CQRS-lite)** | application-layer projection DTO via a `*QueryPort` | read shape ≠ write, or to skip aggregate hydration / lazy-collection joins | `QueryUseCase``*QueryPort``WorkLogSummary` (record); query via JPQL `SELECT new` / JdbcTemplate |
| **Through-aggregate** (default for simple reads) | domain aggregate via a `*Repository` port | read shape == write aggregate **and** the aggregate is the minimal invariant boundary (no lazy collections needed) | `QueryUseCase` → repository port → aggregate |
| **Projection (CQRS-lite)** | application-layer projection DTO via a `*QueryPort` | read shape ≠ write, or to skip aggregate hydration / lazy-collection joins | `QueryUseCase``*QueryPort`summary record; query via JPQL `SELECT new` / JdbcTemplate |
- **D1 — purity guardrail (core, machine-enforced):** a read port whose simple name ends
with `QueryPort` MUST return application-layer projection DTOs only — never a domain
aggregate, JPA entity, or web type, **including through generic type arguments**
(`List<DomainType>`). Enforced by ArchUnit `query_ports_do_not_leak_domain_jpa_or_web_types`.
Projection usage itself is **opt-in**, not a forced default; the demo lives in
`sample-portfolio` (`WorkLogSummaryQueryPort` / `WorkLogSummary`).
Projection usage itself is **opt-in**, not a forced default.
- **D3 — Strict ceremony:** every read goes through a `QueryUseCase` bean. There is no thin
web→read-port path — that would bypass the mandatory `@UseCaseCapability` fitness function.
- **D4 — transaction:** reads default to `TransactionPort.inRead`. A no-tx (autocommit) read
@@ -11,7 +11,7 @@ import java.util.Optional;
*
* <p><strong>Legacy compatibility only.</strong> This contract preserves caller-keyed overwrite and
* whole-{@code byte[]} materialization semantics in a separate legacy namespace while stored data
* and the sample endpoint migrate. New code must use the semantic ports under {@code
* and external consumers migrate. New code must use the semantic ports under {@code
* dev.caskeleton.application.objectstorage}. Production activation must be removed after the
* additive API/data migration, dual-read observation, external-consumer approval, and zero-usage
* gates pass; no calendar removal date is implied here.
+9 -21
View File
@@ -708,8 +708,7 @@ def writeMessagingEvidence = {
schema: messagingSha256FileSet(
'ca-skeleton.messaging.evidence.schema-set.v1',
[
file('shared-contract/src/main/resources/contracts/messaging/envelope/v1.schema.json'),
file('sample-portfolio/src/main/resources/contracts/messaging/portfolio.worklog.reserved/v1.schema.json')
file('shared-contract/src/main/resources/contracts/messaging/envelope/v1.schema.json')
] + fileTree(
'adapter/outbound/messaging/src/main/resources/contracts/messaging/meta/draft-2020-12'
).files.toList()),
@@ -788,7 +787,6 @@ def verifyMessagingContracts = tasks.register('verifyMessagingContracts') {
dependsOn validateMessagingJsonSchemaV1EvidenceManifestSchema
dependsOn ':application-core:messagingApplicationContractQualificationTest'
dependsOn ':shared-contract:messagingSharedSchemaQualificationTest'
dependsOn ':sample-portfolio:messagingSampleContractQualificationTest'
dependsOn ':adapter:outbound:messaging:messagingCompiledContractsQualificationTest'
dependsOn ':adapter:outbound:messaging:messagingJsonSchemaV1QualificationTest'
dependsOn ':adapter:outbound:messaging:verifyJsonSchemaRuntimeGraph'
@@ -797,11 +795,10 @@ def verifyMessagingContracts = tasks.register('verifyMessagingContracts') {
doLast {
writeMessagingEvidence(
'verifyMessagingContracts',
['application', 'shared', 'sample', 'compiled', 'json-schema'],
['application', 'shared', 'compiled', 'json-schema'],
[
':application-core:messagingApplicationContractQualificationTest',
':shared-contract:messagingSharedSchemaQualificationTest',
':sample-portfolio:messagingSampleContractQualificationTest',
':adapter:outbound:messaging:messagingCompiledContractsQualificationTest',
':adapter:outbound:messaging:messagingJsonSchemaV1QualificationTest',
'verifyMessagingContracts'
@@ -853,7 +850,7 @@ def composeCommand = ['docker', 'compose', '-f', baseComposeFile.absolutePath,
def bootstrapCompile = tasks.register('bootstrapCompile') {
group = 'developer experience'
description = 'Stage 1/5: compiles every main and test source set as a local sanity check.'
description = 'Stage 1/4: compiles every main and test source set as a local sanity check.'
dependsOn subprojects.findAll { it.childProjects.isEmpty() }.collect { it.tasks.named('compileTestJava') }
}
@@ -876,7 +873,7 @@ bootstrapDockerPreflight.configure { dependsOn bootstrapCompile }
def bootstrapDependencies = tasks.register('bootstrapDependencies', Exec) {
group = 'developer experience'
description = 'Stage 2/5: starts the local PostgreSQL dependency and waits for readiness.'
description = 'Stage 2/4: starts the local PostgreSQL dependency and waits for readiness.'
commandLine composeCommand + ['up', '-d', '--wait', 'db']
}
bootstrapDependencies.configure { dependsOn bootstrapCompile }
@@ -884,21 +881,14 @@ bootstrapDependencies.configure { dependsOn bootstrapDockerPreflight }
def bootstrapMigrateAndStart = tasks.register('bootstrapMigrateAndStart', Exec) {
group = 'developer experience'
description = 'Stage 3/5: builds/starts the app; startup Flyway must finish before health is ready.'
description = 'Stage 3/4: builds/starts the app; startup Flyway must finish before health is ready.'
commandLine composeCommand + ['up', '-d', '--build', '--wait', 'app']
}
bootstrapMigrateAndStart.configure { dependsOn bootstrapDependencies }
def bootstrapSampleContract = tasks.register('bootstrapSampleContract') {
group = 'developer experience'
description = 'Stage 4/5: verifies the delegated sample production-isolation/build contract.'
dependsOn project(':app-bootstrap').tasks.named('bootstrapSampleContract')
}
bootstrapSampleContract.configure { dependsOn bootstrapMigrateAndStart }
def bootstrapSmoke = tasks.register('bootstrapSmoke') {
group = 'developer experience'
description = 'Stage 5/5: requires HTTP 200 and status=UP from GET /api/healthcheck.'
description = 'Stage 4/4: requires HTTP 200 and status=UP from GET /api/healthcheck.'
doLast {
URI endpoint = URI.create('http://localhost:8080/api/healthcheck')
long deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(60)
@@ -929,11 +919,11 @@ def bootstrapSmoke = tasks.register('bootstrapSmoke') {
"bootstrapSmoke: /api/healthcheck did not become healthy within 60s; last result: ${lastFailure}")
}
}
bootstrapSmoke.configure { dependsOn bootstrapSampleContract }
bootstrapSmoke.configure { dependsOn bootstrapMigrateAndStart }
tasks.register('bootstrap') {
group = 'developer experience'
description = 'Runs the complete five-stage local bootstrap contract.'
description = 'Runs the complete four-stage local bootstrap contract.'
dependsOn bootstrapSmoke
}
@@ -1124,8 +1114,7 @@ tasks.register('verifyCleanArchitectureDependencies') {
if (moduleName != 'sample-portfolio' && actual.contains('sample-portfolio')) {
throw new GradleException(
"Module ':${moduleName}' has a forbidden production dependency on " +
"':sample-portfolio'. The sample module may only be consumed through " +
"non-production fixture configurations."
"deleted ':sample-portfolio' (negative re-entry guard)."
)
}
@@ -1134,7 +1123,6 @@ tasks.register('verifyCleanArchitectureDependencies') {
throw new GradleException(
"Module ':${moduleName}' has forbidden project dependencies ${forbidden}. " +
"Allowed dependencies are ${allowed}. " +
"Production modules must not depend on ':sample-portfolio'; " +
"all project edges must be explicitly registered."
)
}
+8 -34
View File
@@ -1,7 +1,6 @@
{
"runtime_compositions": [
"app-bootstrap",
"sample-portfolio"
"app-bootstrap"
],
"modules": [
{
@@ -10,8 +9,7 @@
"source_path": "src/domain-core",
"allowed_dependencies": [],
"runtime_memberships": [
"app-bootstrap",
"sample-portfolio"
"app-bootstrap"
]
},
{
@@ -20,8 +18,7 @@
"source_path": "src/shared-contract",
"allowed_dependencies": [],
"runtime_memberships": [
"app-bootstrap",
"sample-portfolio"
"app-bootstrap"
]
},
{
@@ -33,8 +30,7 @@
"shared-contract"
],
"runtime_memberships": [
"app-bootstrap",
"sample-portfolio"
"app-bootstrap"
]
},
{
@@ -60,8 +56,7 @@
"shared-contract"
],
"runtime_memberships": [
"app-bootstrap",
"sample-portfolio"
"app-bootstrap"
]
},
{
@@ -83,8 +78,7 @@
"application-core"
],
"runtime_memberships": [
"app-bootstrap",
"sample-portfolio"
"app-bootstrap"
]
},
{
@@ -107,9 +101,7 @@
"application-core",
"shared-contract"
],
"runtime_memberships": [
"sample-portfolio"
]
"runtime_memberships": []
},
{
"id": "adapter-outbound-cache-redis",
@@ -177,8 +169,7 @@
"shared-contract"
],
"runtime_memberships": [
"app-bootstrap",
"sample-portfolio"
"app-bootstrap"
]
},
{
@@ -235,23 +226,6 @@
"runtime_memberships": [
"app-bootstrap"
]
},
{
"id": "sample-portfolio",
"gradle_path": ":sample-portfolio",
"source_path": "src/sample-portfolio",
"allowed_dependencies": [
"domain-core",
"application-core",
"adapter-outbound-persistence-jpa",
"adapter-outbound-identifier",
"adapter-outbound-objectstorage",
"adapter-inbound-web",
"shared-contract"
],
"runtime_memberships": [
"sample-portfolio"
]
}
]
}
+4 -7
View File
@@ -17,15 +17,12 @@
### `ResourceId` — 모든 식별자가 구현하는 마커 인터페이스
시스템의 모든 리소스 식별자 값 객체(예: `WorkLogId`)가 구현하는 공통 인터페이스다.
시스템의 모든 리소스 식별자 값 객체(예: `ArticleId`)가 구현하는 공통 인터페이스다.
식별자의 실제 문자열 값은 `value()` 하나로 노출한다.
- **왜 `sealed` 로 닫지 않았나.** 원래 레퍼런스 설계는 `permits WorkLogId` 처럼 허용 구현을
못 박는 닫힌 집합(`sealed`)으로 만들려 했다. 그런데 `WorkLogId` 같은 실제 식별자는
`sample-portfolio`(샘플 코드)에 있고, 이 마커는 재사용 가능한 `domain-core`에 있다.
`permits` 절을 쓰는 순간 `domain-core``sample-portfolio`를 의존하게 되는데, 이건
모듈 의존성 규칙과 `production_code_does_not_depend_on_sample_portfolio` ArchUnit 규칙이
둘 다 금지하는 방향이다. 그래서 `sealed`을 포기했다.
- **왜 `sealed` 로 닫지 않았나.** 실제 식별자 구현은 각 제품 도메인이 소유하고, 이 마커는
재사용 가능한 `domain-core`에 있다. `permits` 절로 구체 구현을 못 박으면 `domain-core`
바깥 도메인 구현을 의존하게 되어 모듈 의존성 규칙을 거스른다. 그래서 `sealed`을 포기했다.
- **그럼 "닫힌 집합" 보장은 어디서 하나.** `sealed`이 줬을 보장(아무 타입이나 식별자로
쓰지 못하게 하는 것)은 대신 빌드 타임의 `no_long_id_pk` ArchUnit 규칙(D17)이 강제한다 —
도메인의 모든 `id` 필드는 반드시 `ResourceId` 구현이어야 하고, 날것의 `Long` PK 는 금지된다.
Binary file not shown.
-53
View File
@@ -1,53 +0,0 @@
# sample-portfolio — fixture / reference consumer
## Registered identity
- Module ID: `sample-portfolio`
- Gradle path: `:sample-portfolio`
- Focused test (derived from Gradle path): `./gradlew :sample-portfolio:test --console=plain`
- Runtime baseline: Java 21; repository framework baseline: Spring Boot 4.0.0.
- Registry SSOT: `src/config/architecture/modules.json`.
Package root: `dev.caskeleton.sample.portfolio`.
코드 주석에서 덜어낸 **설계 결정의 근거**는 [README.md](README.md) 의 "설계 결정 참조" 절이 모아둔다 (이 문서는 모듈 규칙 SSOT).
## Responsibility
- Reference implementation for template adopters: the WorkLog domain slice, layered
exactly like a consuming project — `domain/worklog` (`WorkLog`, `WorkLogId`,
`Period`, `WorkLogRepository`), `application/` (commands, queries, use cases,
ports such as `RepoStatsPort`), and reference adapters under `adapter/web`,
`adapter/persistence`, `adapter/outbound/repostats` (`RepoStatsPortClient` +
`RepoStatsAclMapper`), `adapter/identifier` (`UuidWorkLogIdFactory`).
- Contract-test fixtures the app-bootstrap verification suite analyses
(`DomainExceptionHandler`, `PortfolioErrorCode`, wire/contract tests).
- Disposable messaging contract fixture: the exact `WorkLogReservedPayload` record, its static
application-core contract contribution, sample-owned schema/digest and golden vectors. It is not
runtime-discovered, and validator compatibility remains unproven until messaging Task 6.
- Sample application collaborators consume invocation context through application-core ports and
must not import SLF4J/MDC.
## Allowed
- Runtime leaves explicitly allowed for this fixture consumer by the `sample-portfolio` entry in
`src/config/architecture/modules.json`; do not duplicate the 19-leaf list here.
## Forbidden
- Production code depending on this module — ArchUnit
`production_code_does_not_depend_on_sample_portfolio`. `app-bootstrap` references
it as `testImplementation` ONLY so the ArchUnit suite can analyse the reference
implementation ([app-bootstrap/build.gradle](../app-bootstrap/build.gradle) L2124).
- Production-required behaviour living here: deleting this module must not break the
production build or runtime (the base `GlobalExceptionHandler`, envelope, and
error-code contract live in `adapter:inbound:web` / `shared-contract`).
- Treating a sample schema/contribution as an automatically discovered production registry entry,
or placing JSON mapper, physical topic, Kafka or bootstrap-server concerns in the contribution.
## Test
```bash
cd src
./gradlew :sample-portfolio:test
```
-477
View File
@@ -1,477 +0,0 @@
# sample-portfolio — WorkLog 포트폴리오 게시판 (예시 도메인)
이 모듈은 ca-skeleton 템플릿이 유지하는 **fixture/reference 예시 도메인**입니다. production 모듈
(`domain-core`/`application-core`/`adapter-*`/`shared-contract`/`app-bootstrap`)의
경계와 운영 계약을 "어떻게 쓰는지" 보여줍니다. 새 프로젝트는 이 모듈을 import하지 않고 자신의
도메인을 production 모듈에 추가합니다. 템플릿에서는 이 모듈을 유지하며, 다운스트림 fork는
`./gradlew :app-bootstrap:sampleOffTest` 통과 후에만 선택적으로 정리할 수 있습니다.
## 도메인: WorkLog (엔지니어링 작업 기록 게시판)
메인 화면 = `GET /work-logs` 목록. 각 항목은 직접 수행한 작업입니다.
| 필드 | 설명 |
| --- | --- |
| title | 작업 제목 |
| category | INFRASTRUCTURE / DATABASE / BACKEND / PLATFORM |
| summary / content | 요약 / 본문 |
| techStack[] | 사용 기술 |
| links[] | 관련 링크 (repo/blog) |
| period | 기간 (end=null → 진행 중) |
## 엔드포인트
| Method | Path | 설명 | 시연 계약 |
| --- | --- | --- | --- |
| GET | /work-logs | 목록 (메인) | Envelope wrap |
| GET | /work-logs/{id} | 상세 | 도메인 예외→404 envelope |
| POST | /work-logs | 생성 | Bean Validation @GroupSequence (B4), unknown-field 거부 (B1), mapper 실패→MAPPING_FAILED (B3) |
| PATCH | /work-logs/{id} | 부분수정 | JsonNullable 3-state (B2) |
| DELETE | /work-logs/{id} | 삭제 | 204 |
| POST | /work-logs/import | 일괄 등록 | BulkEnvelope 부분실패 (B8) |
## 샘플 데이터 (curl 예시)
```bash
# 1) 인프라 인증·인가 위임 (Keycloak + k3s + Vault)
curl -X POST localhost:8080/work-logs -H 'Content-Type: application/json' -d '{
"title": "Keycloak + k3s + Vault 기반 인증·인가 위임",
"category": "INFRASTRUCTURE",
"summary": "인증/인가를 Keycloak으로 위임, k3s + Vault로 시크릿·구성 관리",
"content": "...",
"techStack": ["keycloak", "k3s", "vault"],
"links": ["https://example.com/infra"],
"periodStart": "2025-01-01"
}'
# 2) DB 쿼리 튜닝
curl -X POST localhost:8080/work-logs -H 'Content-Type: application/json' -d '{
"title": "DB 쿼리 튜닝",
"category": "DATABASE",
"summary": "slow query 분석 및 인덱스/실행계획 개선",
"content": "...",
"techStack": ["postgresql"],
"links": [],
"periodStart": "2025-02-01"
}'
```
## 계층 (adapter-mirrored)
- `domain/worklog` — WorkLog/WorkCategory/Period/RepoStats/WorkLogRepository (POJO)
- `application/worklog` — *UseCase (CommandUseCase/QueryUseCase + @UseCaseCapability)
- `adapter/web` — controller/dto/mapper/error
- `adapter/persistence` — entity/repository/mapper
- `adapter/outbound/repostats` — B7 ACL 예시
---
# 설계 결정 참조 (코드 주석에서 이전)
여기 아래는 예전에 각 클래스의 주석/JavaDoc 에 흩어져 있던 **"왜 이렇게 짰는가"** 설명을
한곳에 모은 것입니다. 코드에는 "무엇을 하는지"만 짧게 남기고, 그 배경·결정·트레이드오프는
이 문서로 옮겼습니다. 코드를 읽다 "왜 이렇게 했지?"가 궁금할 때 보세요.
읽는 법:
- 모듈 규칙(허용/금지 의존, 테스트 명령)의 기준은 [CLAUDE.md](CLAUDE.md) 입니다. 이 문서는
규칙이 아니라 **결정의 근거**를 모은 참조 기록입니다.
- 본문은 코드가 어떤 약속을 지키려고 존재하는지 설명합니다. 추적용 꼬리표는 코드 주석에
남기지 않고, 필요한 배경은 여기서 문장으로 풀어 설명합니다.
## domain — 도메인 (순수 POJO, 프레임워크 의존 없음)
### WorkLog (애그리거트 루트)
- WorkLog 애그리거트의 루트입니다. **상태를 바꾸는 길은 의도가 드러나는 메서드뿐**입니다
(`rename`, `recategorize`, `startProgress`, `close` …). 공개 `setXxx` 세터를 두지 않은 이유는,
세터를 열어두면 불변식(invariant)을 건너뛴 채로 객체를 망가뜨릴 수 있기 때문입니다. 모든
변경 경로가 불변식 검사를 거치도록 강제합니다.
- 불변식을 어기면 `WorkLogInvariantException` 을 던집니다. 이 예외는 안전한 사유(reason)만
담고, 로그 문자열이나 운영 에러 코드/HTTP 상태는 전혀 모릅니다 — 그 변환은 상위
(application/web)의 몫입니다.
- **id 는 도메인이 직접 만들지 않습니다.** `WorkLog.create(...)` 는 이미 만들어진
`WorkLogId` 를 받기만 합니다. UUIDv7 생성은 유스케이스가 `WorkLogIdFactory` 포트를 통해
하고, 도메인 안에는 `UUID.randomUUID()` 나 id 생성 라이브러리가 들어오지 않습니다(서버가 id
를 정해주는 계약).
- `version` 필드(낙관적 락 버전)의 역할: 새로 만든(아직 저장 안 된) 객체는 `null` 이고,
저장소가 값을 채우고 증가시킵니다. 영속 계층은 이 `null` 여부로 "INSERT 인지 UPDATE 인지"를
구분하고, 웹 경계에서는 이 값이 HTTP `ETag` / `If-Match` 의 출처가 됩니다. 즉 DB 가
낙관적 락 충돌로 잡아내는 그 충돌을, 웹에서는 HTTP 412 로 표현합니다.
- `title` 규칙: null 이면 안 되고(`NullPointerException`), 공백만 있어도 안 됩니다
(`TITLE_BLANK`). 공백 금지는 **도메인 불변식**이라, 웹 경계의 `@NotBlank`(형식 검사)와는
별개로 한 번 더 지킵니다 — 웹을 거치지 않는 호출자가 있어도 규칙이 새지 않도록.
### 값 객체 — WorkLogId / Period / WorkLogOwner
- **WorkLogId**: 36자 canonical UUID(RFC 9562 UUIDv7). 순수 값 객체라 형식(정규형)만 검증하고,
id 생성 라이브러리에는 의존하지 않습니다. 대소문자 정규화는 이 타입을 만들기 전에 웹 경계에서
끝냅니다(canonical 형태는 소문자 hex). 형식은 `8-4-4-4-12` hex 그룹입니다.
- **Period**: 작업 기간. `end == null` 이면 "진행 중"이라는 뜻입니다. 생성자에서 `end < start`
를 막습니다(기간 역전 불가).
- **WorkLogOwner**: 소유자 식별자. 공백/빈 값을 막고 trim 합니다.
### 열거형 — WorkLogStatus / WorkCategory / WorkLogSortField
- **WorkLogStatus**: OPEN → IN_PROGRESS → CLOSED. 전이 규칙은 WorkLog 가 강제합니다.
- **WorkCategory**: 포트폴리오 보드에 노출하는 작업 분류(INFRASTRUCTURE/DATABASE/BACKEND/PLATFORM).
- **WorkLogSortField**: 목록 정렬에 **허용된 필드만** 담은 닫힌 열거형입니다. 정렬 키를 ORM 에
넘기는 열린 문자열이 아니라 열거형으로 막아둔 이유는, 알 수 없는 필드를 쿼리까지 보내지 않고
웹 경계에서 400 으로 거절하기 위해서입니다(`?sort=field,direction` 계약을 안전하게 만드는 핵심).
### WorkLogInvariantException
- WorkLog 불변식 위반을 알리는 예외입니다. `Reason`(명사형 안전 enum)만 담고, **운영 에러
코드·HTTP 상태·로깅은 모릅니다.** 도메인은 로거를 갖지 않고 운영 코드로 매핑하지도 않는다는
규칙 때문입니다. `reason()` 을 로그 라인과 `error.category` 로 번역하는 일은 application/web 의
책임입니다. 사유가 거친 명사라서 민감 정보가 클라이언트로 새지 않습니다.
### WorkLogReserved (도메인 이벤트)
- "WorkLog 기간이 예약됨"이라는 도메인 사실을 표현하는 이벤트입니다. **전송(transport)과
무관**합니다 — 도메인 데이터(`WorkLogId`/`WorkCategory`/`LocalDate`)만 담고 broker/wire/HTTP
타입을 일절 참조하지 않습니다. 이것을 발행 가능한 integration 이벤트로 바꾸는 일은 application
경계의 몫입니다(`application.event.WorkLogReservedIntegrationEvent` 와 그 매퍼 참고).
### 포트 — WorkLogRepository / WorkLogIdFactory / OutboxEventIdFactory
- **WorkLogRepository**: WorkLog 영속 아웃바운드 포트(구현은 adapter-persistence). `findPage`
한 페이지 + 전체 개수를 함께 돌려주며, `sortField`/`category``null` 이면 정렬/필터 없음을
뜻합니다. 전체 개수는 같은 필터 기준으로 세어 `meta.page.total` 이 필터된 결과를 반영하게 합니다.
- **WorkLogIdFactory / OutboxEventIdFactory**: id 를 만들어 주는 도메인 포트. 둘을 **굳이 따로 둔
이유**는, 애그리거트 식별자 타입(`WorkLogId`)이 이벤트 id 모양을 묶어버리지 않게 하기 위해서입니다.
실제로는 둘 다 UUIDv7 기반이지만(identifier 어댑터가 구현), 도메인은 생성 방식을 모릅니다.
outbox 이벤트 id 는 outbox 봉투의 `eventId` 이자 `idempotencyKey` 로 쓰입니다(같은 값 → 이벤트
단위 중복 제거).
### RepoStats
- 외부 저장소 통계를 정규화한 결과 값입니다. 아웃바운드 ACL(B7)을 통과한 뒤에만 만들어집니다.
## application — 응용 (유스케이스)
### Command / Query — 스스로 검증하는 입력 모델
- `CreateWorkLogCommand`, `UpdateWorkLogCommand`, `DeleteWorkLogCommand`, `GetWorkLogQuery`,
`GetRepoStatsQuery`, `ListWorkLogsQuery`, `BatchCreateWorkLogsCommand` 는 **생성자에서 스스로
필수값을 검증**합니다("self-validating input model"). 이렇게 하면 어떤 인바운드 어댑터가
부르든(오늘은 웹, 내일은 메시징 컨슈머) 웹 경계의 jakarta validation 에 기대지 않고도 같은
계약을 보장받습니다. id 나 patch 가 null 인 것은 프로그래밍 오류이므로 생성 시점에 즉시
실패시킵니다("없음"은 `Patch.absent()` 로 표현하고 null 을 쓰지 않습니다).
- `UpdateWorkLogCommand` 의 필드는 `Patch<T>` 입니다. PATCH 의 3가지 상태 — 값 없음(변경 안 함) /
명시적 null(필드 비움) / 값(교체) — 을 구분하기 위해서입니다.
### CreateWorkLogUseCase — 생성 + outbox 동시 기록 (dual-write 금지)
- WorkLog 를 저장하면서 `WorkLogReserved` outbox 이벤트를 **같은 쓰기 트랜잭션 안에서** 함께
적재하는 예시입니다. 핵심은 `repository.save(...)``outboxAppendPort.append(...)` 가 같은
`tx.inWrite { ... }` 블록 안에 있다는 점입니다. 저장이나 적재 중 하나라도 실패하면 트랜잭션
전체가 롤백됩니다 — "DB 한 번, 브로커 한 번" 식의 분리된 이중 쓰기(dual-write)를 막는 보장입니다.
- 순서: ① 저장된 애그리거트로 도메인 이벤트 생성 → ② 매퍼로 integration 이벤트로 변환(경계는
application) → ③ 직접 만든 JSON 페이로드로 직렬화 → ④ 같은 트랜잭션에서 outbox 에 append.
- `eventId``OutboxEventIdFactory`(UUIDv7) 로 만들고, `idempotencyKey = eventId` 로 둡니다
(이벤트 단위 중복 제거).
- `correlationId` 는 application-core의 `CorrelationIdPort`로 읽습니다. inbound web adapter가
sanitized MDC 값을 포트 뒤에서 제공하므로 sample application 코드는 SLF4J/MDC를 알지 않습니다.
요청 밖에서 실행되거나 값이 blank면 `eventId`를 그대로 `correlationId`로 사용해 이벤트가
최소한 자신을 가리키게(self-correlation) 폴백합니다.
- 쓰기 작업이라 `@RequiresPermission("worklog:write")` 로 권한을 요구합니다(이 권한은 `user`/`admin`
역할 묶음에 부여).
### BatchCreateWorkLogsUseCase — 원자적 일괄 생성
- 동기 일괄 생성은 **전부 성공 아니면 전부 롤백**입니다. 모든 항목을 하나의 `tx.inWrite` 단위로
처리하므로 한 항목이라도(매핑/검증/영속) 실패하면 배치 전체가 되돌려지고, 엔드포인트는 부분
성공 대신 단일 4xx 를 냅니다. (`BATCH_PARTIAL_FAILURE` 같은 부분 실패 형태는 비동기 폴링
응답용으로 예약돼 있고, 동기 배치에는 쓰지 않습니다.)
### UpdateWorkLogUseCase — Patch 3-state 적용
- 각 필드의 `Patch` 상태에 따라 다르게 동작합니다: `hasValue()` 면 교체, `isExplicitNull()` 이면
비움, 둘 다 아니면(없음) 변경하지 않습니다. 상태 전이는 도메인 메서드(`startProgress`/`close`)에
위임하고, OPEN 으로 되돌리는 시도는 잘못된 전이로 막습니다.
### DeleteWorkLogUseCase — 권한 3-tier 가시화
- 삭제는 파괴적인 admin 등급 작업이라 `@RequiresPermission("worklog:close")` 로 막습니다.
`worklog:close`**`admin` 역할 묶음에만** 부여되고 `user` 에는 주지 않습니다(샘플 모델에는 별도
"close" 유스케이스가 없어 delete 가 그 민감 작업 역할을 합니다). 이 한 군데가 3-tier 권한 모델을
눈에 보이게 만듭니다 — 인증된 `user` 는 여기서 거절되고 `admin` 만 통과합니다. (참고로 생성·수정·
일괄 생성은 `worklog:write` 로 막고, 이 권한은 `user`/`admin` 양쪽에 부여됩니다.)
### 읽기 모델(projection) — ListRecentWorkLogSummariesUseCase / WorkLogSummaryQueryPort / WorkLogSummary
- 이 세 타입은 **CQRS-lite "쿼리 우회(query bypass)"** 예시입니다. 보드/목록 화면처럼 가벼운 읽기는
WorkLog 애그리거트를 통째로 복원하지 않고, 필요한 컬럼만 뽑은 projection 으로 읽습니다
(`content`/`summary`/`techStack`/`links` 같은 무거운 상태를 빼서, lazy 컬렉션 조인 없는 컬럼-부분
SELECT 가 됩니다).
- **이것은 강제 기본값이 아니라 선택적 최적화**입니다. 애그리거트를 통해 읽는 길
(`WorkLogRepository`)도 단순한 읽기에서는 똑같이 유효한 기본 경로입니다.
- projection DTO 의 순수성: projection 은 **application 타입**이어야 합니다 — 도메인 애그리거트도,
JPA 엔티티도, 웹 DTO 도 아니어야 합니다. 도메인 ** 타입(예: `WorkCategory` enum)을 참조하는 것은
허용됩니다(application 은 domain 에 의존 가능). 다만 쿼리 포트의 반환 시그니처에 도메인 애그리거트/
JPA 엔티티/웹 타입이 새면 안 됩니다(`List<WorkLogSummary>` 같은 제네릭 인자까지 포함, ArchUnit 규칙
`query_ports_do_not_leak_domain_jpa_or_web_types` 가 강제).
- **명명 규칙**: projection 을 반환하는 읽기 포트 이름은 `…QueryPort` 로 끝냅니다.
- **id 표현**: projection 의 `id` 는 애플리케이션의 표준 id 어휘인 36자 UUID 문자열입니다. 저장소
고유의 `UUID` 는 어댑터 안에서 변환되어, 읽기 모델은 저장 형식이 아니라 애플리케이션과 같은
식별자 형식을 말합니다.
- 격식(ceremony)은 그대로 지킵니다: projection 읽기여도 `QueryUseCase` 빈을 거치고
(`@UseCaseCapability` 계약을 모든 읽기에 기계적으로 적용), 기본은 `tx.inRead` 안에서 읽습니다.
projection 도 저장소 기반이면 여전히 `READ_REPOSITORY` 입니다 — "projection 이냐 애그리거트냐"는
반환 *모양*의 축이고, 저장소 접근 *수준*과는 별개라서 새 capability enum 이 필요 없습니다.
### WorkLogReservedIntegrationEvent / …Mapper — integration(wire) 이벤트
- **IntegrationEvent**: 도메인 이벤트와 달리 아웃바운드 어댑터가 실제로 발행할(예: Kafka)
직렬화 가능한 경계 소유 형태입니다. 도메인 사실을 wire 계약으로 옮기는 일 — 값 객체를 원시
타입으로 평탄화, wire 표현 선택 — 은 application 의 관심사라서 application 계층에 둡니다. 도메인은
wire 를 모릅니다.
- **Mapper**: 도메인 이벤트 → integration 이벤트 변환을 **application 경계에서** 수행하고, 트랜잭션
outbox 용 JSON 페이로드 문자열로 직렬화합니다.
- JSON 직렬화(`toJson`)는 의존성을 늘리지 않으려고 RFC 8259 §7 이스케이프까지 직접 구현했습니다.
실제 프로젝트라면 스키마 직렬화 계약(Avro/Protobuf, 스키마 레지스트리 붙은 Jackson)을 써야
합니다 — 샘플은 새 의존성 0을 유지합니다. 세 필드는 도메인 식별자와 enum 이름뿐이라 PII/토큰/
원문 본문이 없습니다.
- 이벤트 타입 이름 `"worklog.reserved"`: `도메인.동사` 패턴의 안정적인 점-구분 논리명입니다.
outbox relay 규약(`topic = eventType`)에 따라 그대로 브로커 토픽 이름이 됩니다. integration
이벤트의 클래스 단순명을 쓰는 대안도 있지만, 클래스 이름을 바꾸면 토픽이 조용히 바뀌어 컨슈머가
깨질 수 있어, 안정적인 문자열 리터럴을 택했습니다(토픽 마이그레이션 계획 없이는 바꾸지 말 것).
### WorkLogReservedPayload / …ContractContribution — canonical contract fixture
- `WorkLogReservedPayload`는 새 closed contract SPI에 제공하는 exact final record다. v1은
`workLogId` 하나만 소유하며, schema와 같은 canonical bounded identifier 규칙을 생성자에서 지킨다.
- `contracts/messaging/portfolio.worklog.reserved/v1.schema.json`과 golden vector는
**sample fixture contract**다. 공통 envelope는 WorkLog 필드를 알지 못하며 이 모듈을 삭제해도
production module build/runtime은 깨지지 않아야 한다.
- contribution은 contract ID, exact payload type/component order, classpath resource ID, checked-in
digest와 provider-neutral descriptor만 제공한다. JSON mapper, physical topic, Kafka 설정을 소유하지
않으며 매 호출 filesystem I/O도 하지 않는다.
- 기존 3-field `WorkLogReservedIntegrationEvent`/hand-rolled mapper는 현재 legacy outbox
characterization 경로에 남아 있다. 새 1-field canonical payload는 그 타입을 조용히 대체하지 않으며,
closed catalog와 encoder가 조립되기 전까지 양쪽은 서로 호출하거나 직렬화하지 않는다.
- 현재는 classpath scanning이나 runtime discovery가 없다. Task 5의 closed catalog 조립 전에는
자동 등록되지 않고, Task 6 validator qualification 전에는 Draft 2020-12 validator compatibility를
주장하지 않는다.
### RepoStatsPort
- 외부 저장소 통계를 가져오는 아웃바운드 포트(구현은 adapter-outbound).
## adapter/web — 인바운드 HTTP / 보안 / DTO
### WorkLogController
- 포트폴리오 보드 엔드포인트. 리소스 이름은 AIP-122 를 따릅니다: `/worklogs`(복수·소문자),
`/worklogs/repoStats`(lowerCamelCase 하위 세그먼트), 콜론-동사 커스텀 메서드
`/worklogs:batchCreate`. `/v1` 버전 접두사는 `PresentationWebConfig` 가 중앙에서 붙입니다.
- **GET /worklogs**: 페이지네이션 + (선택) 정렬 + (선택) 평면 동등 필터. 정렬 문자열은
`SortParam.parse` 가 파싱해 native 가 아닌 구문은 400 으로 거절하고, 허용 목록에 없는 필드도
거절합니다. offset 이 너무 깊으면 커서 사용을 권하는 `Deprecation` 헤더를 답니다.
- **GET /worklogs/{id}**: `ETag` 를 내보내고, `If-None-Match` 가 맞으면 본문 없이 304 를 답니다.
- **PATCH /worklogs/{id}**: `If-Match` 가 오면 현재 ETag 와 같아야 하고, 다르면 412(낙관적 동시성).
`If-Match` 가 없으면 검사를 건너뜁니다 — 스켈레톤은 권장 패턴을 보여주되 강제하지는 않습니다.
`ETag`/`If-Match` 는 HTTP 전송 관심사(RFC 7232)라, 읽고-비교하는 로직을 일부러 웹 어댑터에만
두고 application/domain 은 ETag 를 전혀 보지 않게 합니다.
- **POST /worklogs**: `Idempotency-Key` 헤더는 POST 요청 표면의 일부로 받아만 둡니다. 키의
모양·범위·재생(replay) 의미는 별도 계약(rate-limit-idempotency)의 몫이라, 그 브랜치가 replay 를
배선하기 전까지는 서버가 헤더를 그냥 허용(무시)합니다.
- **POST /worklogs:batchCreate**: AIP-136 콜론-동사 일괄 생성. **동기 = 원자적**이라 한 항목이라도
실패하면 배치 전체가 롤백되고 단일 4xx 가 납니다. 한 번에 보낼 수 있는 항목 수는
`MAX_BATCH_SIZE`(1000)로 제한하고, 넘으면 400 입니다. 요청 봉투는 `{ "requests": [ ... ] }` 형태입니다.
- **소유자(owner) 스탬프**: 생성되는 WorkLog 에는 인증된 호출자의 IdP subject 를 소유자로 찍습니다.
보안 컨텍스트에서 null 안전하게 읽으며, 이 값은 **가공 전 원시 principal id** 입니다 —
가명화(pseudonymization)는 별도 프라이버시 계약의 몫이라 여기서 적용하지 않고, 소유자 범위
인가(ABAC)는 의도적으로 뒤로 미룬 확장입니다. 유스케이스에 건 `worklog:write` 권한이 생성이 실제
실행될 시점엔 인증된 principal 이 있음을 보장합니다.
- **id 정규화(`toId`)**: 대소문자 무관 canonical UUID 경로 입력을 받아, 도메인 id 를 만들기 전에
표준 36자 소문자 UUID 로 정규화합니다. 여기서 쓰는 `UUID.fromString(...)` 은 (생성기가 아니라) 파서라,
잘못된 id 면 `IllegalArgumentException` 을 던지고 전역 핸들러가 이를 HTTP 400 으로 매핑합니다.
### OperationsController / SampleOperationStore / WorkLogExportResult — 장기 실행 작업(LRO) 예시
- `POST /worklogs:export``202 Accepted` + 폴링 경로를 가리키는 `Location` 헤더 + `operationId`/
`statusUrl` 을 담은 봉투를 돌려주고, `GET /operations/{id}` 로 상태를 폴링합니다. 상태는 5값
`OperationStatus` enum 에서 옵니다.
- **operation id 를 컨트롤러가 아니라 `SampleOperationStore` 에서 만드는 이유**: 컨트롤러/유스케이스는
id 를 직접 생성하면 안 된다는 규칙(`no_uuid_random_in_controller`) 때문입니다. `SampleOperationStore`
는 컨트롤러도 application 서비스도 아닌 웹 인프라 컴포넌트라 여기서 id 를 발급해도 됩니다.
- **단순화**: 이 "export" 는 동기로 끝나므로, 클라이언트가 폴링할 때 저장된 operation 은 이미
`SUCCEEDED` 입니다. 진짜 비동기 잡이라면 PENDING → RUNNING → 종료 로 전이하겠지만, wire 모양
(202 + Location + 폴링 enum)은 동일합니다.
### SamplePolymorphicRequest — 안전한 다형 역직렬화(B5)
- 다형 JSON 을 안전하게 받는 표준 예시입니다. `@JsonTypeInfo(use = NAME, property = "kind")` +
명시적 `@JsonSubTypes` 허용 목록은, RCE 취약점(CVE-2019-14379)의 입구인
`ObjectMapper.enableDefaultTyping()` 대신 Jackson 이 권장하는 방식입니다. 목록에 없는 하위 타입은
`InvalidTypeIdException` 으로 거절되고, 계약 핸들러가 이를 `MAPPING_FAILED` 로 보냅니다.
- `sealed` 키워드를 쓴 이유: 허용 목록과 타입 계층을 기계적으로 일치시키기 위해서입니다. `permits`
만 추가하고 `@JsonSubTypes.Type` 을 안 넣거나(혹은 반대로) 하면 조용히 통과되는 게 아니라
컴파일/테스트에서 잡힙니다.
### WorkLogWebMapper
- 요청 DTO ↔ application command 변환을 **웹 어댑터가 전부 책임**지게 모은 매퍼입니다. PATCH 도
컨트롤러에서 인라인으로 만들지 않고 매퍼를 거치게 해서 일관성을 지킵니다. `WorkLogId` 는 웹
edge 에서 파싱(UUID 경로 정규화)해 넘겨받아, id 구문 관심사를 컨트롤러에 둡니다.
- 응답에는 **소유자(owner)를 일부러 넣지 않습니다** — 원시 principal id 노출은 프라이버시 문제라,
가명화는 별도 프라이버시 계약의 몫입니다.
- 링크 URI 가 잘못되면 `MappingException` 을 던져 전역 핸들러가 `MAPPING_FAILED`(400)로 보냅니다.
### WorkLogIdSerializer
- `WorkLogId` 를 record 기본 모양(`{"value":"..."}`)이 아니라 **맨 36자 소문자 UUID 문자열**로
직렬화합니다. `@JsonComponent` 로 Spring Boot 가 자동 등록합니다.
### DomainExceptionHandler / PortfolioErrorCode
- **DomainExceptionHandler**: 이 샘플의 도메인 예외를 `Envelope` 로 매핑합니다. 운영/전송/보안
예외는 스켈레톤의 기본 `GlobalExceptionHandler` 가 처리하고, Spring 이 두 advice 를 함께 적용합니다.
이 핸들러를 `@Order(HIGHEST_PRECEDENCE)` 로 **기본 핸들러보다 앞**에 두는 이유는, 그렇지 않으면
기본 advice 의 포괄 핸들러(`@ExceptionHandler(Exception.class)`)가 도메인 예외를 먼저 잡아
INTERNAL_ERROR 로 만들어 버리기 때문입니다.
- **PortfolioErrorCode**: 이 샘플 전용 도메인 에러 코드. 운영/전송/보안 코드는 공용
`OperationalError` 에 있습니다.
### WorkLogValidationGroups
- `@GroupSequence` 용 검증 그룹 순서(형식 → 불변식)를 정의합니다 — 형식 검사가 실패하면 불변식
평가를 건너뛰게 합니다.
## adapter/persistence — RDBMS / JPA 영속
### WorkLogEntity
- WorkLog 애그리거트의 JPA 엔티티. `AuditableEntity` 를 상속해 `created_at/updated_at/created_by/
updated_by` 감사 컬럼을 물려받습니다. **감사 필드는 엔티티(영속 계층)에만 있고 도메인 WorkLog 에는
없습니다** — 값은 `WorkLogRepositoryAdapter` 가 Clock + 감사 컨텍스트로 찍어줍니다.
- **id 저장 형식**: UUIDv7 을 varchar(36)가 아니라 PostgreSQL 16 의 native `uuid` 타입(16바이트
바이너리)으로 저장합니다. UUID 문자열↔native uuid 변환은 `WorkLogPersistenceMapper` 가 합니다.
(tenant 범위 컬럼/복합 인덱스는 tenant 정책 계약으로 미뤄둠.)
- `@Version version`: 낙관적 락 버전이자 웹 경계 ETag/If-Match 의 출처. Hibernate 가 증가를
관리하고, `null` 이면 새 행이라 `save()` 가 INSERT 합니다.
### WorkLogPersistenceMapper — UUID 문자열 ↔ native uuid 변환
- 36자 canonical UUID 와 PostgreSQL native `uuid`(128비트)를 서로 변환합니다. `UuidCodec` 같은 공용
코덱이 아니라 JDK `java.util.UUID` 를 **직접** 쓰는 이유: 영속 어댑터는 경계 규칙상
`adapter-outbound`(코덱이 있는 곳)에 의존하면 안 되기 때문입니다(stdlib 이라 의존 문제 자체가 없음).
- 엔티티로 변환할 때 도메인 `version` 을 그대로 실어, JPA 가 새 행(`null` → INSERT)과 추적 중인 행
(non-null → 낙관적 merge)을 구분할 수 있게 합니다.
### WorkLogRepositoryAdapter — 저장 + 감사 스탬프
- `WorkLogRepository` 포트 구현체. 감사 메타데이터(생성/수정 시각·주체)를 **여기 영속 어댑터에서**
찍습니다(생성자 주입 Clock + 감사 컨텍스트). 도메인 WorkLog 는 감사 필드를 들고 있지 않습니다.
- INSERT(버전 null): 새 애그리거트라 created/updated 둘 다 지금 시각·주체로 초기화합니다.
- UPDATE(버전 non-null): 기존 행의 created 값을 읽어 그대로 이어가고(같은 트랜잭션에서 유스케이스가
이미 로드해둔 JPA 1차 캐시에서 served), updated 만 갱신합니다.
### WorkLogJpaRepository
- `findByCategory`: 계약이 허용하는 유일한 필터 구문인 **평면 동등 필터**를 데이터 계층 안에 둔
파생 쿼리입니다.
- `findRecentSummaryRows`: CQRS-lite projection 읽기. JPQL `SELECT new` 생성자 표현식으로 요약
컬럼만 골라(`content/summary/techStack/links` 제외) `WorkLogSummaryRow` 로 담는 컬럼-부분 읽기라,
애그리거트 복원과 lazy `@ElementCollection` 조인을 건너뜁니다. 닫힌 인터페이스 projection 대신
`SELECT new`/`JdbcTemplate` 을 택한 이유: 닫힌-projection 의 컬럼 가지치기는 구현체 의존적이고
Hibernate 6 에서 검증되지 않았기 때문입니다.
### WorkLogSummaryQueryAdapter / WorkLogSummaryRow
- **WorkLogSummaryQueryAdapter**: application `WorkLogSummaryQueryPort` 구현체. JPQL projection 쿼리에
위임하고, 저장소 native `WorkLogSummaryRow`(UUID id)를 application `WorkLogSummary`(UUID 문자열 id)로
매핑합니다. **저장소 native `UUID` 는 이 어댑터를 벗어나지 않습니다** — application 에 노출되는 읽기
모델은 canonical UUID 문자열 형식으로 말합니다.
- **WorkLogSummaryRow**: projection 의 영속-쪽 읽기 행. JPQL `SELECT new` 의 대상이며 저장 모양
(id 가 native `UUID`)을 그대로 반영합니다. 이 행을 persistence 패키지 안에 두는 것 자체가 native
`UUID` 가 어댑터를 벗어나지 못하게 막는 장치입니다.
### JpaConfig
- Spring Boot 메인 클래스가 `dev.caskeleton.bootstrap` 에 있어, 패키지 기준 기본 스캔이
`…sample.portfolio.adapter.persistence.*` 를 놓칩니다. 그래서 엔티티/리포지토리 스캔 위치를 여기서
명시해, 그 배선을 실제로 소유하는 모듈 안에 둡니다.
## adapter/outbound — 아웃바운드 통합 (저장소 통계 ACL)
### RepoStatsPortClient / RepoStatsAclMapper / RawRepoStatsResponse
- **ACL(Anti-Corruption Layer) 패턴(B7) 예시**입니다. 외부 응답이 도메인으로 들어오기 전에 반드시
ACL 매퍼를 거치게 해서, 원시 외부 타입이 도메인을 오염시키지 못하게 합니다.
- **RepoStatsPortClient**: 실제 HTTP 호출(`fetchRaw`)을 추상화해 템플릿을 가볍게 유지합니다 — 실제
프로젝트라면 여기에 WebClient/RestClient 를 끼웁니다. 원시 응답은 ACL 을 통과한 뒤에야 도메인 타입이
됩니다.
- **RepoStatsAclMapper**: ACL 의 세 가지 일 — 정규화(full name 소문자화), 마스킹(echoedToken 은 버려서
도메인에 닿지 않음), 공개 필드 선택(`fullName/stargazers/pushedAt` 만 `RepoStats` 로). 필수 필드가
없으면 `MappingException`.
- **RawRepoStatsResponse**: 외부 제공자 원시 모양. 도메인으로 넘어가지 않으며 package-private 입니다.
## adapter/identifier — ID 생성 (UUIDv7)
### UuidWorkLogIdFactory / UuidOutboxEventIdFactory
- 도메인 id 포트(`WorkLogIdFactory`, `OutboxEventIdFactory`)의 인프라 구현체입니다.
- 둘 다 `UuidCreator.getTimeOrderedEpochPlus1()` 을 씁니다 — RFC 9562 UUIDv7(time-ordered)이며 같은
밀리초 안에서도 단조 증가(monotonic)하고 secure random 으로 뒷받침됩니다.
- **여기가 샘플에서 `UuidCreator` 호출이 허용되는 유일한 곳**입니다. `no_uuid_random_in_controller`
ArchUnit 규칙이 웹/application 계층의 직접 id 생성을 금지하기 때문에, id 생성은 이 인프라 어댑터에만
둡니다.
## bootstrap — 샘플 전용 구성 루트
> **이 패키지가 왜 존재하는가 (공통 배경):** 프로덕션 조립은 `app-bootstrap` 모듈이 합니다. 그런데
> `sample-portfolio` 는 일부러 `app-bootstrap` 에 의존하지 않습니다(샘플이 부트스트랩을 끌어오면
> 안 됨). 그래서 스캔으로 자동 발견되지 않고 오직 `app-bootstrap` 에만 있는 구성 루트 빈들
> (tracing/metrics/idempotency/privacy/management/domain-context 등)을, 여기 `bootstrap.*` 패키지가
> **샘플 전용으로 동등하게 복제**해 제공합니다. 모두 일회용(disposable)입니다 — `sample-portfolio`
> 모듈을 지우면 함께 사라지고 프로덕션에는 영향이 없습니다. 프로덕션(`CaSkeletonApplication`)은 원래
> `app-bootstrap` 빈들을 그대로 씁니다.
### SamplePortfolioApplication — 독립 실행 구성 루트
- `dev.caskeleton` 하위 전부를 스캔해 프로덕션 모듈의 Spring 컴포넌트(adapter-web `SecurityConfig`,
adapter-persistence `PersistenceJpaConfig` 등)를 자동으로 발견·배선합니다. `app-bootstrap` 에만 있어
자동 발견되지 않는 구성 루트만 위 `bootstrap.*` 가 채웁니다.
- **`@SpringBootApplication` 대신 `@Configuration` + `@EnableAutoConfiguration` + `@ComponentScan` 을
쓴 이유**: `@SpringBootApplication` 은 `@SpringBootConfiguration` 을 포함하는데, 그러면 이 클래스가
Spring Boot 의 `AnnotatedClassFinder` 에 보입니다. 그 finder 가 이 클래스와 (같은 패키지·테스트
classpath 의) `SamplePortfolioTestApplication` **둘 다** 찾으면 "multiple @SpringBootConfiguration"
오류를 냅니다. `@Configuration`(= `@SpringBootConfiguration` 아님)을 쓰면 finder 에 숨으면서도,
`@SpringBootTest(classes = SamplePortfolioApplication.class)` 로 부팅하는 데는 문제가 없습니다.
- **`@ComponentScan` 에 exclude 두 개를 둔 이유**:
- `PostgreSqlPersistenceConfig` 제외 — 이 클래스는 `@PersistenceContext EntityManager` 필드와
`FlywayConfigurationCustomizer @Bean` 을 함께 가져, 깨지지 않는 초기화 순환을 만듭니다
(flyway → customizer 수집 → PostgreSqlPersistenceConfig 인스턴스화 → `@PersistenceContext` 가
entityManagerFactory 를 당김 → 그건 flywayInitializer 에 의존 → 순환). 대신
`SamplePostgreSqlPersistenceConfig` 가 static `@Bean` 으로 동일 빈을 제공해 순환을 끊습니다(아래 참고).
- `TestEnclosedConfigurationFilter` 제외 — 테스트 클래스 안에 중첩된 `@Configuration` 들이 넓은
컴포넌트 스캔에 잡혀, 두 테스트 픽스처가 같은 이름의 빈(예: `clock`)을 정의하면
`BeanDefinitionOverrideException` 이 나기 때문입니다.
### SamplePostgreSqlPersistenceConfig — Flyway ↔ EntityManager 초기화 순환 끊기
- `adapter-persistence-postgresql` 의 `PostgreSqlPersistenceConfig` 를 샘플 전용으로 대체한 것입니다.
- **왜 필요한가**: 원본은 `@PersistenceContext EntityManager` 필드와 `FlywayConfigurationCustomizer`
빈을 함께 등록합니다. Spring Boot 의 Flyway 자동 구성은 `entityManagerFactory` 가 준비되기 전에 모든
customizer 빈을 모읍니다(Flyway 가 먼저 마이그레이션을 돌려야 JPA 가 스키마를 검증할 수 있으니까).
그 customizer 를 모으려고 `PostgreSqlPersistenceConfig` 를 인스턴스화하면 `@PersistenceContext` 가
`entityManagerFactory` 를 당기는데, 그 빈은 `flywayInitializer` 완료에 의존합니다 →
`flyway → PostgreSqlPersistenceConfig → entityManagerFactory → flywayInitializer → flyway` 의 끊을 수
없는 순환이 생깁니다.
- **어떻게 끊는가**: customizer 를 **static `@Bean`** 으로 등록합니다. Spring 은 static `@Bean` 을
소유 클래스 인스턴스화 없이 호출하므로, Flyway 초기화 동안 `@PersistenceContext` 가 처리되지
않습니다. `EntityManager` 는 (런타임에 실제로 필요해질 때까지 미루도록) 빈 경로에서 늦게 해소됩니다.
- **마이그레이션 위치 주의**: 이 customizer 가 두 위치(프로덕션 `db/migration/postgresql` + 샘플 전용
`db/sample-migration`)를 모두 설정합니다. `configuration.locations(...)` 는 기존 설정을 **덮어쓰므로**
(append 아님), `application.yml` 의 `spring.flyway.locations` 는 런타임 효과 없는 문서용일 뿐이고
실제 기준은 이 customizer 입니다.
### TestEnclosedConfigurationFilter — 테스트 중첩 @Configuration 제외
- 테스트 클래스 안에 중첩된 `@Configuration` 을 컴포넌트 스캔에서 빼는 필터입니다.
- **문제**: `:sample-portfolio:test` 를 돌리면 테스트 클래스가 classpath 에 올라옵니다. 테스트 안
static 중첩 `@Configuration`(예: `WorkLogAuthorizationContractTest$AuthzTestConfig`)이 평범한
`@Configuration` 이라, 전체 컨텍스트로 부팅할 때 스캔에 발견·등록됩니다. 두 테스트 설정이 같은 이름의
빈(예: `clock`)을 정의하면 `BeanDefinitionOverrideException` 이 납니다(override 기본 false).
- **휴리스틱**: 바이너리 이름에 `$` 가 있고 그 바깥(enclosing) 클래스 이름이 `Test` 로 끝나면(자바
테스트 클래스 관례) 제외합니다. 테스트 프레임워크 의존성을 프로덕션 소스에 들이지 않고도 중첩
테스트 픽스처를 전부 거릅니다.
### 분산 추적 복제본 — SampleTracingConfig / …Settings / …SampleRateResolver / …EnvironmentPostProcessor / SampleMicrometerSpanErrorRecorder
- `app-bootstrap` 의 tracing 구성 루트를 샘플 전용으로 옮긴 것들입니다(위 "공통 배경" 참고). tracing
런타임(OTel/Micrometer 브리지)이 샘플 build.gradle 에 켜져 있지만, 그 빈들을 등록하는 조립 코드는
`app-bootstrap` 에만 있어서 복제했습니다.
- **per-profile 기본 샘플 레이트(SampleRateResolver)**: `prod=0.01`, `staging=0.10`, `dev/local=1.0`
(그 외 프로파일도 1.0). 설정값(`APP_TRACING_SAMPLE_RATE`)이 `[0.0, 1.0]` 범위로 들어오면 그 값이
per-profile 기본을 이깁니다.
- **EnvironmentPostProcessor**: 해소된 effective 레이트를 native 키
`management.tracing.sampling.probability` 로 이어줘, 실제 OTel sampler 가 gauge 값과 일치하게 합니다.
사용자가 native 키를 직접 지정했으면 그 값을 우선합니다. (이 PostProcessor 는 샘플 자신의
`META-INF/spring.factories` 로 등록됩니다.)
- **SampleMicrometerSpanErrorRecorder**: 현재 span 에 예외를 `error.code` 태그와 함께 기록(D12).
Tracer 가 있으면 실제 recorder, 없으면 NOOP 로 동작합니다(adapter-web `GlobalExceptionHandler` 가
`ObjectProvider` 로 집어 씀).
### 그 밖의 부트스트랩 복제본
- **SampleMetricsContractConfig**: Micrometer `MeterFilter` 두 개를 meter 등록 전에 설치합니다 —
고-cardinality 태그 키를 막는 필터(D8)와 SLO 기반 히스토그램 분포 필터(D9, 소유한 timer 이름에만 적용).
- **SampleIdempotencyConfig / …Settings**: 스캔된 `IdempotencyStoreAdapter`/`IdempotencyReaper` 와
`IdempotencyExecutor` 가 모두 `Clock` 을 필요로 하고, `@EnableScheduling` 이 reaper 의 `@Scheduled`
정리를 켭니다. TTL 은 양수이며 ≤ 72h 여야 합니다(아니면 시작 실패), reaper 주기 기본 10분.
- **SamplePseudonymizationConfig / SamplePrivacySettings**: adapter-web `RequestLoggingFilter` 가
`UserPrincipalPseudonymizerPort` 를 주입받으므로 그 포트를 채웁니다. salt 는
`APP_PRIVACY_PSEUDONYMIZATION_SALT` 에서 오며, 비어 있으면 로컬/테스트가 시작은 되도록 dev 센티넬
salt 로 폴백합니다(프로덕션 전엔 실제 시크릿 값을 넣을 것).
- **SampleDomainContextConfig**: `adapter-persistence-rdbms` 의 `DomainContextAuditContextPort` 가
`DomainContextPropagator` 빈을 무조건 주입받으므로, 기본 `THREAD_LOCAL` 전략 구현
(`ThreadLocalDomainContextPropagator`)을 제공합니다 — async 데코레이터를 안 켜는 WorkLog 데모에
적합합니다.
-159
View File
@@ -1,159 +0,0 @@
// Fixture/sample module. Production modules must not depend on this module.
// Lean standalone boot: apply the Spring Boot plugin so bootJar / bootRun are available.
apply plugin: 'org.springframework.boot'
apply from: "${rootProject.projectDir}/gradle/strict-qualification-test.gradle"
def repositoryRootForContractTests = rootProject.projectDir.parentFile
def errorCodeRegistryForContractTests = new File(
repositoryRootForContractTests,
'docs/registries/error-codes.yaml')
tasks.withType(Test).configureEach {
systemProperty 'ca.repository.root', repositoryRootForContractTests.absolutePath
}
tasks.named('test') {
inputs.file(errorCodeRegistryForContractTests)
.withPathSensitivity(PathSensitivity.RELATIVE)
}
sourceSets {
posterImageMigrationTest {
java.srcDir 'src/posterImageMigrationTest/java'
resources.srcDir 'src/posterImageMigrationTest/resources'
compileClasspath += sourceSets.main.output + sourceSets.test.output
runtimeClasspath += output + compileClasspath
}
}
configurations {
posterImageMigrationTestImplementation.extendsFrom testImplementation
posterImageMigrationTestRuntimeOnly.extendsFrom testRuntimeOnly
}
// The objectstorage adapter (runtimeOnly) pulls in software.amazon.awssdk:s3, whose version is
// managed by the AWS SDK v2 BOM (NOT the Spring Boot BOM). Import that BOM at this consumer's scope
// mirroring the objectstorage module so the transitive s3 dependency resolves on this module's
// runtime/test classpath. The AWS-SDK version SSOT stays ext.awsSdkVersion.
dependencyManagement {
imports {
mavenBom "software.amazon.awssdk:bom:${awsSdkVersion}"
}
}
dependencies {
implementation project(':domain-core')
implementation project(':application-core')
implementation project(':adapter:outbound:persistence-jpa')
implementation project(':adapter:outbound:identifier')
// Object storage adapter (filesystem default / S3-MinIO opt-in) provides the ObjectStoragePort
// bean at runtime for the Poster image-upload use case; the port itself lives in application-core.
runtimeOnly project(':adapter:outbound:objectstorage')
implementation project(':adapter:inbound:web')
implementation project(':shared-contract')
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
// UUIDv7 generation (id factory) + UUID/String conversion (persistence mapper, web path).
implementation 'com.github.f4b6a3:uuid-creator:6.1.1'
// PATCH 3-state (absent / explicit-null / value) via JsonNullable. See README.
implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
// Lean standalone boot: actuator + prometheus + tracing runtime + dotenv (mirrors app-bootstrap).
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
implementation 'io.micrometer:micrometer-tracing-bridge-otel'
implementation 'io.opentelemetry:opentelemetry-exporter-otlp'
// Spring dotenv: picks up src/.env when bootRun workingDir = src/ (same as app-bootstrap).
implementation 'me.paulschwarz:spring-dotenv:4.0.0'
// Boot 4 Flyway API/autoconfiguration needed by sample-local persistence configuration.
implementation 'org.springframework.boot:spring-boot-flyway'
// Flyway core API needed by MigrationStartupConfig (scanned from adapter-persistence).
implementation 'org.flywaydb:flyway-core'
// Micrometer core for OutboxMetrics / BackgroundJobMetrics.
implementation 'io.micrometer:micrometer-core'
// (spring-boot-starter-security is already declared above; it also backs ManagementSecurityConfig.)
// Property-based testing for @ValueObject invariants (registers its own JUnit Platform engine).
testImplementation 'net.jqwik:jqwik:1.9.1'
// Real-DB integration test for the persistence adapter (disposable Postgres, real Flyway + JPA).
testImplementation 'org.testcontainers:testcontainers-postgresql'
testImplementation 'org.testcontainers:testcontainers-junit-jupiter'
// @ServiceConnection supplies datasource connection details to Spring context/slice tests.
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
// Boot 4 split JPA/JDBC slice annotations and TestEntityManager into dedicated test modules.
testImplementation 'org.springframework.boot:spring-boot-data-jpa-test'
testImplementation 'org.springframework.boot:spring-boot-jpa-test'
testImplementation 'org.springframework.boot:spring-boot-jdbc-test'
testRuntimeOnly 'org.postgresql:postgresql'
// Spring Security test support (MockMvc + @WithMockUser) for any slice tests.
testImplementation 'org.springframework.security:spring-security-test'
}
// feature-contract-verification-test-suite §5 OpenAPI drift gate.
// OpenApiDriftContractTest runs as a normal test (so `check` is release-blocking on drift) AND
// can be invoked through this task to (re)generate the committed snapshot after an intentional
// API change: ./gradlew :sample-portfolio:openapiCheckSnapshot -PapproveOpenApiChange
tasks.register('openapiCheckSnapshot', Test) {
group = 'verification'
description = 'OpenAPI drift gate: runtime springdoc /v3/api-docs vs the committed snapshot. ' +
'-PapproveOpenApiChange regenerates the committed baseline.'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform()
filter {
includeTestsMatching 'dev.caskeleton.sample.portfolio.adapter.inbound.web.contract.OpenApiDriftContractTest'
}
systemProperty 'openapi.snapshot.write', project.hasProperty('approveOpenApiChange') ? 'true' : 'false'
// Always re-check drift; never serve a stale UP-TO-DATE result.
outputs.upToDateWhen { false }
// Pin UTC like the main test task for host-locale independence.
jvmArgs '-Duser.timezone=UTC'
}
def posterImageMigrationQualification = registerStrictQualificationTest(
name: 'posterImageMigrationTest',
sourceSet: sourceSets.posterImageMigrationTest,
requiredClasses: [
'dev.caskeleton.sample.portfolio.qualification.PosterImageIdempotencyRotationQualificationTest',
'dev.caskeleton.sample.portfolio.qualification.PosterImageRetirementQualificationTest',
'dev.caskeleton.sample.portfolio.qualification.PosterImageV8MigrationQualificationTest'
],
description: 'Runs the non-skipping PostgreSQL Poster image migration/rotation lane.')
posterImageMigrationQualification.configure {
shouldRunAfter tasks.named('test')
}
def messagingSampleContractQualification = registerStrictQualificationTest(
name: 'messagingSampleContractQualificationTest',
sourceSet: sourceSets.test,
requiredClasses: [
'dev.caskeleton.sample.portfolio.application.event.WorkLogReservedContractContributionTest'
],
junitXmlOutput: rootProject.layout.buildDirectory.dir(
'test-results/messaging-evidence/sample'),
binaryResultsOutput: rootProject.layout.buildDirectory.dir(
'test-results/messaging-evidence-binary/sample'),
description: 'Runs exact Messaging sample contract qualification tests.')
messagingSampleContractQualification.configure {
dependsOn ':prepareMessagingContractEvidence'
}
bootJar {
mainClass = 'dev.caskeleton.sample.portfolio.SamplePortfolioApplication'
}
tasks.register('stageDockerJar', Sync) {
dependsOn tasks.named('bootJar')
from(tasks.named('bootJar').flatMap { it.archiveFile })
into(layout.buildDirectory.dir('docker'))
rename { 'application.jar' }
}
// Run from the repo's src/ root so src/.env is picked up (same as app-bootstrap).
bootRun {
workingDir = rootProject.projectDir
}
-338
View File
@@ -1,338 +0,0 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
aopalliance:aopalliance:1.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0=compileClasspath,posterImageMigrationTestCompileClasspath,testCompileClasspath
ch.qos.logback:logback-classic:1.5.21=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
ch.qos.logback:logback-core:1.5.21=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-annotations:2.20=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-core:2.20.1=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.core:jackson-databind:2.20.1=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.20.1=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.20.1=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.fasterxml.jackson:jackson-bom:2.20.1=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.fasterxml:classmate:1.7.1=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.ben-manes.caffeine:caffeine:3.2.3=annotationProcessor,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
com.github.docker-java:docker-java-api:3.7.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.docker-java:docker-java-transport-zerodep:3.7.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.docker-java:docker-java-transport:3.7.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.f4b6a3:uuid-creator:6.1.1=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
com.github.spotbugs:spotbugs-annotations:4.10.2=spotbugs
com.github.spotbugs:spotbugs-annotations:4.8.6=compileClasspath,posterImageMigrationTestCompileClasspath,testCompileClasspath
com.github.spotbugs:spotbugs:4.10.2=spotbugs
com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
com.google.auto:auto-common:1.2.2=annotationProcessor,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
com.google.code.findbugs:jsr305:3.0.2=checkstyle,compileClasspath,posterImageMigrationTestCompileClasspath,spotbugs,testCompileClasspath
com.google.code.gson:gson:2.13.2=spotbugs
com.google.errorprone:error_prone_annotation:2.49.0=annotationProcessor,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.38.0=compileClasspath,posterImageMigrationTestCompileClasspath,testCompileClasspath
com.google.errorprone:error_prone_annotations:2.41.0=spotbugs
com.google.errorprone:error_prone_annotations:2.47.0=checkstyle
com.google.errorprone:error_prone_annotations:2.49.0=annotationProcessor,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_check_api:2.49.0=annotationProcessor,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
com.google.errorprone:error_prone_core:2.49.0=annotationProcessor,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
com.google.googlejavaformat:google-java-format:1.35.0=annotationProcessor,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
com.google.guava:failureaccess:1.0.3=annotationProcessor,checkstyle,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
com.google.guava:guava:33.5.0-jre=annotationProcessor,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
com.google.guava:guava:33.6.0-jre=checkstyle
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,checkstyle,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
com.google.j2objc:j2objc-annotations:3.1=annotationProcessor,checkstyle,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
com.google.protobuf:protobuf-java:4.33.2=annotationProcessor,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
com.h2database:h2:2.4.240=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.h3xstream.findsecbugs:findsecbugs-plugin:1.14.0=spotbugsPlugins
com.jayway.jsonpath:json-path:2.9.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.nimbusds:nimbus-jose-jwt:10.4=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.puppycrawl.tools:checkstyle:13.5.0=checkstyle
com.squareup.okhttp3:okhttp-jvm:5.2.1=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.squareup.okhttp3:okhttp:5.2.1=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.squareup.okio:okio-jvm:3.16.1=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.squareup.okio:okio:3.16.1=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.sun.istack:istack-commons-runtime:4.1.2=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
com.vaadin.external.google:android-json:0.0.20131108.vaadin1=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
com.zaxxer:HikariCP:7.0.2=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
commons-beanutils:commons-beanutils:1.11.0=checkstyle
commons-codec:commons-codec:1.19.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
commons-collections:commons-collections:3.2.2=checkstyle
commons-io:commons-io:2.20.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
commons-io:commons-io:2.21.0=spotbugs
commons-logging:commons-logging:1.3.5=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
info.picocli:picocli:4.7.7=checkstyle
io.github.cdimascio:dotenv-java:3.0.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
io.micrometer:context-propagation:1.2.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-commons:1.16.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-core:1.16.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-jakarta9:1.16.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-observation:1.16.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-registry-prometheus:1.16.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-tracing-bridge-otel:1.6.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.micrometer:micrometer-tracing:1.6.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-buffer:4.2.17.Final=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.netty:netty-codec-base:4.2.17.Final=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.netty:netty-codec-compression:4.2.17.Final=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.netty:netty-codec-http2:4.2.17.Final=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.netty:netty-codec-http:4.2.17.Final=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.netty:netty-codec-marshalling:4.2.17.Final=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.netty:netty-codec-protobuf:4.2.17.Final=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.netty:netty-codec:4.2.17.Final=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.netty:netty-common:4.2.17.Final=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.netty:netty-handler:4.2.17.Final=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.netty:netty-resolver:4.2.17.Final=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.netty:netty-transport-classes-epoll:4.2.17.Final=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.netty:netty-transport-native-unix-common:4.2.17.Final=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.netty:netty-transport:4.2.17.Final=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.opentelemetry.semconv:opentelemetry-semconv:1.37.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-api:1.55.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-common:1.55.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-context:1.55.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-exporter-common:1.55.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-exporter-otlp-common:1.55.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-exporter-otlp:1.55.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.55.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-extension-trace-propagators:1.55.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk-common:1.55.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:1.55.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk-logs:1.55.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk-metrics:1.55.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk-trace:1.55.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opentelemetry:opentelemetry-sdk:1.55.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.projectreactor:reactor-core:3.8.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-config:1.4.3=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-core:1.4.3=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-exposition-formats:1.4.3=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-exposition-textformats:1.4.3=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-model:1.4.3=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.prometheus:prometheus-metrics-tracer-common:1.4.3=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.swagger.core.v3:swagger-annotations-jakarta:2.2.38=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.swagger.core.v3:swagger-core-jakarta:2.2.38=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.swagger.core.v3:swagger-models-jakarta:2.2.38=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
jakarta.activation:jakarta.activation-api:2.1.4=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.annotation:jakarta.annotation-api:3.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.inject:jakarta.inject-api:2.0.1=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
jakarta.persistence:jakarta.persistence-api:3.2.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.transaction:jakarta.transaction-api:2.0.1=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.validation:jakarta.validation-api:3.1.1=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
jakarta.xml.bind:jakarta.xml.bind-api:4.0.4=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
javax.inject:javax.inject:1=annotationProcessor,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
jaxen:jaxen:2.0.0=spotbugs
me.paulschwarz:spring-dotenv:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy-agent:1.17.8=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.bytebuddy:byte-buddy:1.17.8=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
net.java.dev.jna:jna:5.18.1=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.jqwik:jqwik-api:1.9.1=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.jqwik:jqwik-engine:1.9.1=posterImageMigrationTestRuntimeClasspath,testRuntimeClasspath
net.jqwik:jqwik-time:1.9.1=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.jqwik:jqwik-web:1.9.1=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.jqwik:jqwik:1.9.1=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.minidev:accessors-smart:2.6.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.minidev:json-smart:2.6.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
net.sf.saxon:Saxon-HE:12.9=checkstyle,spotbugs
org.antlr:antlr4-runtime:4.13.2=checkstyle,compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.bcel:bcel:6.12.0=spotbugs
org.apache.commons:commons-compress:1.28.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.commons:commons-lang3:3.20.0=checkstyle,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
org.apache.commons:commons-text:1.15.0=spotbugs
org.apache.commons:commons-text:1.3=checkstyle
org.apache.httpcomponents:httpclient:4.5.13=checkstyle,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.apache.httpcomponents:httpcore:4.4.16=checkstyle,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.apache.logging.log4j:log4j-api:2.25.2=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
org.apache.logging.log4j:log4j-core:2.25.2=spotbugs
org.apache.logging.log4j:log4j-to-slf4j:2.25.2=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.maven.doxia:doxia-core:1.12.0=checkstyle
org.apache.maven.doxia:doxia-logging-api:1.12.0=checkstyle
org.apache.maven.doxia:doxia-module-xdoc:1.12.0=checkstyle
org.apache.maven.doxia:doxia-sink-api:1.12.0=checkstyle
org.apache.tomcat.embed:tomcat-embed-core:11.0.14=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-el:11.0.14=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.tomcat.embed:tomcat-embed-websocket:11.0.14=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.apache.xbean:xbean-reflect:3.7=checkstyle
org.apiguardian:apiguardian-api:1.1.2=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.aspectj:aspectjweaver:1.9.25=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.assertj:assertj-core:3.27.6=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.awaitility:awaitility:4.3.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.checkerframework:checker-qual:3.49.5=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.codehaus.plexus:plexus-classworlds:2.6.0=checkstyle
org.codehaus.plexus:plexus-component-annotations:2.1.0=checkstyle
org.codehaus.plexus:plexus-container-default:2.1.0=checkstyle
org.codehaus.plexus:plexus-utils:3.3.0=checkstyle
org.dom4j:dom4j:2.2.0=spotbugs
org.eclipse.angus:angus-activation:2.0.3=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.flywaydb:flyway-core:11.14.1=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.flywaydb:flyway-database-postgresql:11.14.1=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.glassfish.jaxb:jaxb-core:4.0.6=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.glassfish.jaxb:jaxb-runtime:4.0.6=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.glassfish.jaxb:txw2:4.0.6=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.hamcrest:hamcrest:3.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.hdrhistogram:HdrHistogram:2.2.2=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.hibernate.models:hibernate-models:1.0.1=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.hibernate.orm:hibernate-core:7.1.8.Final=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.hibernate.validator:hibernate-validator:9.0.1.Final=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.javassist:javassist:3.28.0-GA=checkstyle
org.jboss.logging:jboss-logging:3.6.1.Final=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.2.21=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.jetbrains:annotations:13.0=productionRuntimeClasspath,runtimeClasspath
org.jetbrains:annotations:17.0.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.jspecify:jspecify:1.0.0=annotationProcessor,checkstyle,compileClasspath,posterImageMigrationTestAnnotationProcessor,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:6.0.1=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:6.0.1=posterImageMigrationTestRuntimeClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-params:6.0.1=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter:6.0.1=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-commons:6.0.1=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-engine:6.0.1=posterImageMigrationTestRuntimeClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-launcher:6.0.1=posterImageMigrationTestRuntimeClasspath,testRuntimeClasspath
org.junit:junit-bom:6.0.1=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.junit:junit-bom:6.1.0=spotbugs
org.latencyutils:LatencyUtils:2.0.3=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.mockito:mockito-core:5.20.0=mockitoAgent,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.mockito:mockito-junit-jupiter:5.20.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.objenesis:objenesis:3.3=posterImageMigrationTestRuntimeClasspath,testRuntimeClasspath
org.openapitools:jackson-databind-nullable:0.2.6=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.osgi:org.osgi.annotation.bundle:2.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,testCompileClasspath
org.osgi:org.osgi.annotation.versioning:1.1.2=compileClasspath,posterImageMigrationTestCompileClasspath,testCompileClasspath
org.osgi:org.osgi.resource:1.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,testCompileClasspath
org.osgi:org.osgi.service.serviceloader:1.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,testCompileClasspath
org.ow2.asm:asm-analysis:9.10.1=spotbugs
org.ow2.asm:asm-commons:9.10.1=spotbugs
org.ow2.asm:asm-tree:9.10.1=spotbugs
org.ow2.asm:asm-util:9.10.1=spotbugs
org.ow2.asm:asm:9.10.1=spotbugs
org.ow2.asm:asm:9.7.1=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.pcollections:pcollections:4.0.1=annotationProcessor,posterImageMigrationTestAnnotationProcessor,testAnnotationProcessor
org.postgresql:postgresql:42.7.8=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.reactivestreams:reactive-streams:1.0.4=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.reflections:reflections:0.10.2=checkstyle
org.rnorth.duct-tape:duct-tape:1.0.8=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.skyscreamer:jsonassert:1.5.3=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.slf4j:jul-to-slf4j:2.0.17=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.slf4j:slf4j-api:2.0.17=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,spotbugs,spotbugsSlf4j,testCompileClasspath,testRuntimeClasspath
org.slf4j:slf4j-simple:2.0.17=checkstyle,spotbugsSlf4j
org.springdoc:springdoc-openapi-starter-common:3.0.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-actuator-autoconfigure:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-actuator:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-autoconfigure:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-configuration-processor:4.0.0=annotationProcessor
org.springframework.boot:spring-boot-data-commons:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-data-jpa-test:4.0.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-data-jpa:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-flyway:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-health:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-hibernate:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-http-client:4.0.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-http-converter:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-jackson:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-jdbc-test:4.0.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-jdbc:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-jpa-test:4.0.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-jpa:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-micrometer-metrics:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-micrometer-observation:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-persistence:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-restclient:4.0.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-resttestclient:4.0.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-security-oauth2-resource-server:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-security:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-servlet:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-sql:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-actuator:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-data-jpa:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-flyway:4.0.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-jackson-test:4.0.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-jackson:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-jdbc:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-logging:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-micrometer-metrics:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-oauth2-resource-server:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-security:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-test:4.0.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-tomcat-runtime:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-tomcat:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-validation:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-web:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-webmvc-test:4.0.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter-webmvc:4.0.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-starter:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-test-autoconfigure:4.0.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-test:4.0.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-testcontainers:4.0.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-tomcat:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-transaction:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-validation:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-web-server:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-webmvc-test:4.0.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot-webmvc:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.boot:spring-boot:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.data:spring-data-commons:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.data:spring-data-jpa:4.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.integration:spring-integration-core:7.0.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.springframework.integration:spring-integration-jdbc:7.0.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.springframework.security:spring-security-config:7.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-core:7.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-crypto:7.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-oauth2-core:7.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-oauth2-jose:7.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-oauth2-resource-server:7.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-test:7.0.0=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.security:spring-security-web:7.0.0=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework.session:spring-session-core:4.0.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.springframework:spring-aop:7.0.1=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-aspects:7.0.1=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-beans:7.0.1=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-context:7.0.1=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-core:7.0.1=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-expression:7.0.1=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-jdbc:7.0.1=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-messaging:7.0.1=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.springframework:spring-orm:7.0.1=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-test:7.0.1=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-tx:7.0.1=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-web:7.0.1=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.springframework:spring-webflux:7.0.1=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
org.springframework:spring-webmvc:7.0.1=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.testcontainers:testcontainers-database-commons:2.0.2=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.testcontainers:testcontainers-jdbc:2.0.2=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.testcontainers:testcontainers-junit-jupiter:2.0.2=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.testcontainers:testcontainers-postgresql:2.0.2=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.testcontainers:testcontainers:2.0.2=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.xmlresolver:xmlresolver:5.3.3=checkstyle,spotbugs
org.xmlunit:xmlunit-core:2.10.4=posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath
org.yaml:snakeyaml:2.5=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
software.amazon.awssdk:annotations:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:apache-client:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:arns:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:auth:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:aws-core:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:aws-query-protocol:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:aws-xml-protocol:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:checksums-spi:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:checksums:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:crt-core:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:endpoints-spi:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:http-auth-aws-eventstream:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:http-auth-aws:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:http-auth-spi:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:http-auth:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:http-client-spi:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:identity-spi:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:json-utils:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:metrics-spi:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:netty-nio-client:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:profiles:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:protocol-core:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:regions:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:retries-spi:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:retries:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:s3:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:sdk-core:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:third-party-jackson-core:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.awssdk:utils:2.30.0=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
software.amazon.eventstream:eventstream:1.0.1=posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
tools.jackson.core:jackson-core:3.0.2=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson.core:jackson-databind:3.0.2=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson:jackson-bom:3.0.2=compileClasspath,posterImageMigrationTestCompileClasspath,posterImageMigrationTestRuntimeClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
empty=developmentOnly,testAndDevelopmentOnly
@@ -1,52 +0,0 @@
package dev.caskeleton.sample.portfolio;
import dev.caskeleton.adapter.inbound.web.auth.SecurityConfig;
import dev.caskeleton.adapter.inbound.web.authz.MethodSecurityConfig;
import dev.caskeleton.adapter.outbound.persistence.postgresql.PostgreSqlPersistenceConfig;
import dev.caskeleton.sample.portfolio.bootstrap.scan.TestEnclosedConfigurationFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
/**
* Standalone composition root for the WorkLog sample/reference application. Scans all {@code
* dev.caskeleton} sub-packages so production-module components are auto-discovered;
* composition-root beans that live only in {@code app-bootstrap} are provided by the sample-local
* {@code bootstrap.*} package. Disposable: deleting the {@code sample-portfolio} module removes it
* without affecting production.
*
* <p>See README for the wiring rationale: why {@code @Configuration} is used instead of
* {@code @SpringBootApplication}, why the two component-scan exclusions exist, and the
* lean-bootstrap omissions a full clone would add.
*/
@Configuration
@EnableAutoConfiguration
@ComponentScan(
basePackages = "dev.caskeleton",
excludeFilters = {
// Replaced by SamplePostgreSqlPersistenceConfig to break a Flyway/EntityManager
// init cycle (see README).
@ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = {
PostgreSqlPersistenceConfig.class,
// The standalone sample is a public demo app: URL access should not require a
// configured IdP or method-security principal. Production modules keep these guards.
SecurityConfig.class,
MethodSecurityConfig.class
}),
// Excludes @Configuration nested in test classes (see README).
@ComponentScan.Filter(
type = FilterType.CUSTOM,
classes = TestEnclosedConfigurationFilter.class)
})
@ConfigurationPropertiesScan(basePackages = "dev.caskeleton")
public class SamplePortfolioApplication {
public static void main(String[] args) {
SpringApplication.run(SamplePortfolioApplication.class, args);
}
}
@@ -1,52 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.controller;
import dev.caskeleton.application.storage.StoredObject;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response.StoredObjectResponse;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.mapper.PosterWebMapper;
import dev.caskeleton.sample.portfolio.application.command.UploadPosterImageCommand;
import dev.caskeleton.sample.portfolio.application.poster.UploadPosterImageUseCase;
import dev.caskeleton.sample.portfolio.domain.poster.PosterId;
import dev.caskeleton.shared.error.MappingException;
import java.io.IOException;
import java.util.Locale;
import java.util.UUID;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/** Explicit compatibility-profile whole-byte endpoint; absent from canonical publication mode. */
@RestController
@ConditionalOnProperty(prefix = "app.poster-image.api", name = "mode", havingValue = "legacy")
// This exact endpoint remains active until data/API migration is complete.
@SuppressWarnings("removal")
public final class LegacyPosterImageController {
private final UploadPosterImageUseCase upload;
public LegacyPosterImageController(UploadPosterImageUseCase upload) {
this.upload = upload;
}
@PostMapping(value = "/posters/{id}/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public StoredObjectResponse uploadImage(
@PathVariable String id, @RequestParam("file") MultipartFile file) {
byte[] bytes;
try {
bytes = file.getBytes();
} catch (IOException exception) {
throw new MappingException("failed to read uploaded file", exception);
}
String contentType =
file.getContentType() == null ? "application/octet-stream" : file.getContentType();
StoredObject stored = upload.handle(new UploadPosterImageCommand(toId(id), bytes, contentType));
return PosterWebMapper.toStoredObjectResponse(stored);
}
private static PosterId toId(String raw) {
return PosterId.of(UUID.fromString(raw.toLowerCase(Locale.ROOT)).toString());
}
}
@@ -1,45 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.controller;
import dev.caskeleton.adapter.inbound.web.settings.PresentationSettings;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.operation.SampleOperationStore;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.operation.WorkLogExportResult;
import dev.caskeleton.shared.operation.Operation;
import java.net.URI;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Long-running-operation fixture. {@code POST /worklogs:export} returns {@code 202 Accepted} + a
* {@code Location} header + a polling envelope; {@code GET /operations/{id}} returns the {@link
* Operation}. The id is minted by {@link SampleOperationStore}, never by this controller. See
* README.
*/
@RestController
public class OperationsController {
private final SampleOperationStore operations;
private final PresentationSettings presentationSettings;
public OperationsController(
SampleOperationStore operations, PresentationSettings presentationSettings) {
this.operations = operations;
this.presentationSettings = presentationSettings;
}
@PostMapping("/worklogs:export")
public ResponseEntity<Operation<WorkLogExportResult>> export() {
Operation<WorkLogExportResult> accepted =
operations.startExport(presentationSettings.apiBasePath(), 0L);
return ResponseEntity.accepted().location(URI.create(accepted.statusUrl())).body(accepted);
}
@GetMapping("/operations/{id}")
public Operation<WorkLogExportResult> poll(@PathVariable String id) {
return operations
.find(id)
.orElseThrow(() -> new IllegalArgumentException("unknown operation id: " + id));
}
}
@@ -1,164 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.controller;
import dev.caskeleton.adapter.inbound.web.observability.ResponseMetaFactory;
import dev.caskeleton.adapter.inbound.web.pagination.PageParams;
import dev.caskeleton.adapter.inbound.web.pagination.PageValidationException;
import dev.caskeleton.adapter.inbound.web.pagination.SortParam;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.request.CreatePosterRequest;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.request.UpdatePosterRequest;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response.PosterResponse;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.mapper.PosterWebMapper;
import dev.caskeleton.sample.portfolio.application.command.ArchivePosterCommand;
import dev.caskeleton.sample.portfolio.application.command.DeletePosterCommand;
import dev.caskeleton.sample.portfolio.application.command.PublishPosterCommand;
import dev.caskeleton.sample.portfolio.application.poster.ArchivePosterUseCase;
import dev.caskeleton.sample.portfolio.application.poster.CreatePosterUseCase;
import dev.caskeleton.sample.portfolio.application.poster.DeletePosterUseCase;
import dev.caskeleton.sample.portfolio.application.poster.GetPosterUseCase;
import dev.caskeleton.sample.portfolio.application.poster.ListPostersUseCase;
import dev.caskeleton.sample.portfolio.application.poster.PublishPosterUseCase;
import dev.caskeleton.sample.portfolio.application.poster.UpdatePosterUseCase;
import dev.caskeleton.sample.portfolio.application.query.GetPosterQuery;
import dev.caskeleton.sample.portfolio.application.query.ListPostersQuery;
import dev.caskeleton.sample.portfolio.domain.poster.PosterId;
import dev.caskeleton.sample.portfolio.domain.poster.PosterPage;
import dev.caskeleton.sample.portfolio.domain.poster.PosterSortField;
import dev.caskeleton.sample.portfolio.domain.poster.PosterStatus;
import dev.caskeleton.shared.response.Envelope;
import dev.caskeleton.shared.response.PageMeta;
import dev.caskeleton.shared.response.ResponseMeta;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* Promotional-poster endpoints. Resource naming follows AIP-122 ({@code /posters}); lifecycle
* actions are modelled as sub-resource paths ({@code /posters/{id}/image}, {@code /publish}, {@code
* /archive}) a resource-level colon-verb ({@code {id}:publish}) is rejected by the AIP-122
* guardrail, which only permits collection-level colon verbs. The {@code /v1} prefix is applied
* centrally. The upload publish flow is what ties object storage to a domain invariant (a poster
* cannot be published without an image). Mirrors {@code WorkLogController} (conditional-request /
* owner / batch machinery deliberately omitted WorkLog demonstrates those).
*/
@RestController
public class PosterController {
private final CreatePosterUseCase create;
private final UpdatePosterUseCase update;
private final PublishPosterUseCase publish;
private final ArchivePosterUseCase archive;
private final DeletePosterUseCase delete;
private final GetPosterUseCase get;
private final ListPostersUseCase list;
public PosterController(
CreatePosterUseCase create,
UpdatePosterUseCase update,
PublishPosterUseCase publish,
ArchivePosterUseCase archive,
DeletePosterUseCase delete,
GetPosterUseCase get,
ListPostersUseCase list) {
this.create = create;
this.update = update;
this.publish = publish;
this.archive = archive;
this.delete = delete;
this.get = get;
this.list = list;
}
/** Paginated, optionally sorted + status-filtered listing with {@code meta.page}. */
@GetMapping("/posters")
public Envelope<List<PosterResponse>> list(
@RequestParam(required = false) Integer page,
@RequestParam(required = false) Integer size,
@RequestParam(required = false) String sort,
@RequestParam(required = false) PosterStatus status) {
PageParams pageParams = PageParams.of(page, size);
PosterSortField sortField = null;
boolean ascending = true;
String appliedSort = null;
if (sort != null && !sort.isBlank()) {
SortParam parsed = SortParam.parse(sort); // rejects non-native syntax 400
sortField = PosterSortField.fromRequest(parsed.field());
if (sortField == null) {
throw new PageValidationException(
"sort",
"SORT_FIELD_NOT_ALLOWED",
"sort field '" + parsed.field() + "' is not sortable");
}
ascending = parsed.ascending();
appliedSort = parsed.canonical();
}
PosterPage result =
list.handle(
new ListPostersQuery(
pageParams.page(), pageParams.size(), sortField, ascending, status));
List<PosterResponse> body = result.items().stream().map(PosterWebMapper::toResponse).toList();
PageMeta pageMeta = pageParams.toPageMeta(result.total(), appliedSort);
ResponseMeta meta = ResponseMetaFactory.fromMdc().withPage(pageMeta);
return Envelope.ok(body, meta);
}
@GetMapping("/posters/{id}")
public PosterResponse getOne(@PathVariable String id) {
return PosterWebMapper.toResponse(get.handle(new GetPosterQuery(toId(id))));
}
@PostMapping("/posters")
public ResponseEntity<PosterResponse> create(@Valid @RequestBody CreatePosterRequest body) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(PosterWebMapper.toResponse(create.handle(PosterWebMapper.toCommand(body))));
}
@PatchMapping("/posters/{id}")
public PosterResponse patch(
@PathVariable String id, @Valid @RequestBody UpdatePosterRequest body) {
return PosterWebMapper.toResponse(
update.handle(PosterWebMapper.toUpdateCommand(toId(id), body)));
}
/**
* {@code DRAFT PUBLISHED}. Fails with 400 {@code POSTER_IMAGE_REQUIRED} if no image is
* attached.
*/
@PostMapping("/posters/{id}/publish")
public PosterResponse publish(@PathVariable String id) {
return PosterWebMapper.toResponse(publish.handle(new PublishPosterCommand(toId(id))));
}
@PostMapping("/posters/{id}/archive")
public PosterResponse archive(@PathVariable String id) {
return PosterWebMapper.toResponse(archive.handle(new ArchivePosterCommand(toId(id))));
}
@DeleteMapping("/posters/{id}")
public ResponseEntity<Void> delete(@PathVariable String id) {
delete.handle(new DeletePosterCommand(toId(id)));
return ResponseEntity.noContent().build();
}
/**
* Accepts case-insensitive canonical UUID path input and normalises it to the canonical 36-char
* lowercase UUID. {@code UUID.fromString(...)} is a parser (throws {@link
* IllegalArgumentException} on a malformed id HTTP 400).
*/
private static PosterId toId(String raw) {
return PosterId.of(UUID.fromString(raw.toLowerCase(Locale.ROOT)).toString());
}
}
@@ -1,113 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.controller;
import dev.caskeleton.adapter.inbound.web.auth.AuthenticatedPrincipal;
import dev.caskeleton.adapter.inbound.web.http.ApiHeaders;
import dev.caskeleton.application.objectstorage.content.CancellationView;
import dev.caskeleton.application.objectstorage.identity.ObjectOperationId;
import dev.caskeleton.application.objectstorage.model.ObjectContentIdentity;
import dev.caskeleton.application.objectstorage.model.ObjectDigest;
import dev.caskeleton.application.objectstorage.model.ObjectDigestAlgorithm;
import dev.caskeleton.application.objectstorage.model.ObjectMediaType;
import dev.caskeleton.application.outbound.CallBudget;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response.PosterImagePublicationResponse;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response.PosterImagePublicationStatusResponse;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.mapper.PosterImagePublicationWebMapper;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.objectstorage.MultipartObjectContentProducer;
import dev.caskeleton.sample.portfolio.application.command.PublishPosterImageCommand;
import dev.caskeleton.sample.portfolio.application.poster.GetPosterImagePublicationStatusUseCase;
import dev.caskeleton.sample.portfolio.application.poster.PublishPosterImageUseCase;
import dev.caskeleton.sample.portfolio.application.query.GetPosterImagePublicationStatusQuery;
import dev.caskeleton.sample.portfolio.domain.poster.PosterId;
import java.time.Duration;
import java.util.Locale;
import java.util.UUID;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/** Additive streaming publication/status API; no raw locator or signing surface. */
@RestController
@ConditionalOnProperty(prefix = "app.poster-image.api", name = "mode", havingValue = "publication")
public final class PosterImagePublicationController {
private static final Duration CALL_TIMEOUT = Duration.ofMinutes(2);
private final PublishPosterImageUseCase publish;
private final GetPosterImagePublicationStatusUseCase status;
public PosterImagePublicationController(
PublishPosterImageUseCase publish, GetPosterImagePublicationStatusUseCase status) {
this.publish = publish;
this.status = status;
}
@PostMapping(
value = "/posters/{id}/imagePublications",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<PosterImagePublicationResponse> publish(
@PathVariable String id,
@RequestHeader(ApiHeaders.IDEMPOTENCY_KEY) String idempotencyKey,
@RequestParam("expectedVersion") long expectedVersion,
@RequestParam("sha256") String sha256,
@RequestParam("file") MultipartFile file,
@AuthenticationPrincipal AuthenticatedPrincipal principal) {
if (idempotencyKey == null
|| idempotencyKey.isBlank()
|| idempotencyKey.length() > 256
|| principal == null
|| principal.idpUserId() == null
|| principal.idpUserId().isBlank()) {
throw new IllegalArgumentException(
"authenticated principal and Idempotency-Key are required");
}
ObjectContentIdentity identity =
new ObjectContentIdentity(
file.getSize(), ObjectDigest.of(ObjectDigestAlgorithm.SHA_256, sha256));
String contentType =
file.getContentType() == null ? "application/octet-stream" : file.getContentType();
var reservation =
publish.handle(
new PublishPosterImageCommand(
toId(id),
expectedVersion,
null,
principal.idpUserId(),
idempotencyKey.trim(),
identity,
ObjectMediaType.of(contentType),
new MultipartObjectContentProducer(file, identity),
CallBudget.fromNow(CALL_TIMEOUT),
CancellationView.never()));
PosterImagePublicationResponse response =
PosterImagePublicationWebMapper.toResponse(reservation);
return ResponseEntity.status(HttpStatus.ACCEPTED)
.header(ApiHeaders.LOCATION, response.statusPath())
.body(response);
}
@GetMapping("/posters/{id}/imagePublications/{operationId}")
public PosterImagePublicationStatusResponse status(
@PathVariable String id,
@PathVariable String operationId,
@AuthenticationPrincipal AuthenticatedPrincipal principal) {
if (principal == null) {
throw new IllegalArgumentException("authenticated principal is required");
}
return PosterImagePublicationWebMapper.toResponse(
status.handle(
new GetPosterImagePublicationStatusQuery(toId(id), ObjectOperationId.of(operationId))));
}
private static PosterId toId(String raw) {
return PosterId.of(UUID.fromString(raw.toLowerCase(Locale.ROOT)).toString());
}
}
@@ -1,254 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.controller;
import dev.caskeleton.adapter.inbound.web.auth.AuthenticatedPrincipal;
import dev.caskeleton.adapter.inbound.web.conditional.ETags;
import dev.caskeleton.adapter.inbound.web.conditional.PreconditionFailedException;
import dev.caskeleton.adapter.inbound.web.http.ApiHeaders;
import dev.caskeleton.adapter.inbound.web.observability.ResponseMetaFactory;
import dev.caskeleton.adapter.inbound.web.pagination.PageParams;
import dev.caskeleton.adapter.inbound.web.pagination.PageValidationException;
import dev.caskeleton.adapter.inbound.web.pagination.SortParam;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.request.CreateWorkLogRequest;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.request.UpdateWorkLogRequest;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response.RepoStatsResponse;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response.WorkLogResponse;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response.WorkLogSummaryResponse;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.mapper.WorkLogWebMapper;
import dev.caskeleton.sample.portfolio.application.command.BatchCreateWorkLogsCommand;
import dev.caskeleton.sample.portfolio.application.command.DeleteWorkLogCommand;
import dev.caskeleton.sample.portfolio.application.query.GetRepoStatsQuery;
import dev.caskeleton.sample.portfolio.application.query.GetWorkLogQuery;
import dev.caskeleton.sample.portfolio.application.query.ListWorkLogsQuery;
import dev.caskeleton.sample.portfolio.application.worklog.BatchCreateWorkLogsUseCase;
import dev.caskeleton.sample.portfolio.application.worklog.CreateWorkLogUseCase;
import dev.caskeleton.sample.portfolio.application.worklog.DeleteWorkLogUseCase;
import dev.caskeleton.sample.portfolio.application.worklog.GetRepoStatsUseCase;
import dev.caskeleton.sample.portfolio.application.worklog.GetWorkLogUseCase;
import dev.caskeleton.sample.portfolio.application.worklog.ListWorkLogsUseCase;
import dev.caskeleton.sample.portfolio.application.worklog.UpdateWorkLogUseCase;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkCategory;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkLog;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkLogId;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkLogPage;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkLogSortField;
import dev.caskeleton.shared.response.Envelope;
import dev.caskeleton.shared.response.PageMeta;
import dev.caskeleton.shared.response.ResponseMeta;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Size;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* Portfolio board endpoints. Resource naming follows AIP-122 ({@code /worklogs}, {@code
* /worklogs/repoStats}, colon-verb {@code /worklogs:batchCreate}); the {@code /v1} prefix is
* applied centrally. Pagination/sort/filter drive the list, and conditional requests ({@code
* ETag}/{@code If-Match}/{@code If-None-Match}) drive GET/PATCH. See README.
*/
@RestController
public class WorkLogController {
private final CreateWorkLogUseCase create;
private final BatchCreateWorkLogsUseCase batchCreate;
private final UpdateWorkLogUseCase update;
private final DeleteWorkLogUseCase delete;
private final GetWorkLogUseCase get;
private final ListWorkLogsUseCase list;
private final GetRepoStatsUseCase repoStats;
public WorkLogController(
CreateWorkLogUseCase create,
BatchCreateWorkLogsUseCase batchCreate,
UpdateWorkLogUseCase update,
DeleteWorkLogUseCase delete,
GetWorkLogUseCase get,
ListWorkLogsUseCase list,
GetRepoStatsUseCase repoStats) {
this.create = create;
this.batchCreate = batchCreate;
this.update = update;
this.delete = delete;
this.get = get;
this.list = list;
this.repoStats = repoStats;
}
/** Paginated, optionally sorted + flat-filtered listing with {@code meta.page}. */
@GetMapping("/worklogs")
public ResponseEntity<Envelope<List<WorkLogSummaryResponse>>> list(
@RequestParam(required = false) Integer page,
@RequestParam(required = false) Integer size,
@RequestParam(required = false) String sort,
@RequestParam(required = false) WorkCategory category) {
PageParams pageParams = PageParams.of(page, size);
WorkLogSortField sortField = null;
boolean ascending = true;
String appliedSort = null;
if (sort != null && !sort.isBlank()) {
SortParam parsed = SortParam.parse(sort); // rejects non-native syntax 400
sortField = WorkLogSortField.fromRequest(parsed.field());
if (sortField == null) {
throw new PageValidationException(
"sort",
"SORT_FIELD_NOT_ALLOWED",
"sort field '" + parsed.field() + "' is not sortable");
}
ascending = parsed.ascending();
appliedSort = parsed.canonical();
}
WorkLogPage result =
list.handle(
new ListWorkLogsQuery(
pageParams.page(), pageParams.size(), sortField, ascending, category));
List<WorkLogSummaryResponse> body =
result.items().stream().map(WorkLogWebMapper::toSummary).toList();
PageMeta pageMeta = pageParams.toPageMeta(result.total(), appliedSort);
ResponseMeta meta = ResponseMetaFactory.fromMdc().withPage(pageMeta);
ResponseEntity.BodyBuilder builder = ResponseEntity.ok();
if (pageParams.isDeepOffset()) { // deep offset recommend cursor
builder.header(ApiHeaders.DEPRECATION, "deep-offset-pagination");
}
return builder.body(Envelope.ok(body, meta));
}
@GetMapping("/worklogs/repoStats")
public RepoStatsResponse repoStats(@RequestParam String repoUrl) {
return WorkLogWebMapper.toRepoStatsResponse(repoStats.handle(new GetRepoStatsQuery(repoUrl)));
}
/** Emits an {@code ETag}; honours {@code If-None-Match} with a 304 (no body). */
@GetMapping("/worklogs/{id}")
public ResponseEntity<WorkLogResponse> getOne(
@PathVariable String id,
@RequestHeader(value = ApiHeaders.IF_NONE_MATCH, required = false) String ifNoneMatch) {
WorkLog workLog = get.handle(new GetWorkLogQuery(toId(id)));
String etag = etagOf(workLog);
if (ETags.matches(ifNoneMatch, etag)) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED).eTag(etag).build();
}
return ResponseEntity.ok().eTag(etag).body(WorkLogWebMapper.toResponse(workLog));
}
/**
* {@code Idempotency-Key} is accepted on the POST surface but only tolerated (ignored) until the
* rate-limit/idempotency contract wires replay. See README.
*/
@PostMapping("/worklogs")
public ResponseEntity<WorkLogResponse> create(
@Valid @RequestBody CreateWorkLogRequest body,
@RequestHeader(value = ApiHeaders.IDEMPOTENCY_KEY, required = false) String idempotencyKey) {
WorkLog created = create.handle(WorkLogWebMapper.toCommand(body, currentOwnerSubject()));
return ResponseEntity.ok().eTag(etagOf(created)).body(WorkLogWebMapper.toResponse(created));
}
/**
* When {@code If-Match} is supplied it must match the current ETag, else 412 (optimistic
* concurrency); an absent {@code If-Match} is allowed. See README.
*/
@PatchMapping("/worklogs/{id}")
public ResponseEntity<WorkLogResponse> patch(
@PathVariable String id,
@RequestHeader(value = ApiHeaders.IF_MATCH, required = false) String ifMatch,
@Valid @RequestBody UpdateWorkLogRequest body) {
WorkLogId workLogId = toId(id);
requireIfMatch(ifMatch, workLogId);
WorkLog updated = update.handle(WorkLogWebMapper.toUpdateCommand(workLogId, body));
return ResponseEntity.ok().eTag(etagOf(updated)).body(WorkLogWebMapper.toResponse(updated));
}
@DeleteMapping("/worklogs/{id}")
public ResponseEntity<Void> delete(@PathVariable String id) {
delete.handle(new DeleteWorkLogCommand(toId(id)));
return ResponseEntity.noContent().build();
}
/**
* AIP-136 colon-verb batch create. Sync = atomic: {@link BatchCreateWorkLogsUseCase} runs every
* item in one transaction, so any failure rolls back the whole batch (single 4xx, never partial
* success). See README.
*/
@PostMapping("/worklogs:batchCreate")
public List<WorkLogResponse> batchCreate(
@Valid @RequestBody BatchCreateRequest request,
@RequestHeader(value = ApiHeaders.IDEMPOTENCY_KEY, required = false) String idempotencyKey) {
String owner = currentOwnerSubject();
List<WorkLog> created =
batchCreate.handle(
new BatchCreateWorkLogsCommand(
request.requests().stream()
.map(r -> WorkLogWebMapper.toCommand(r, owner))
.toList()));
return created.stream().map(WorkLogWebMapper::toResponse).toList();
}
/** Batch size cap: a single sync batch may not exceed this many items (else 400). */
public static final int MAX_BATCH_SIZE = 1000;
/** AIP-233 batch request envelope: {@code { "requests": [ ... ] }}. */
public record BatchCreateRequest(
@Valid
@Size(max = MAX_BATCH_SIZE, message = "batch may not exceed " + MAX_BATCH_SIZE + " items")
List<CreateWorkLogRequest> requests) {}
private static String etagOf(WorkLog workLog) {
long version = workLog.version() == null ? 0L : workLog.version();
return ETags.weakFromVersion(version);
}
/**
* Optimistic-concurrency precondition. {@code ETag}/{@code If-Match} is an HTTP transport
* concern, so the read-then-compare stays in the web adapter; an absent/blank {@code If-Match}
* skips the check. See README.
*/
private void requireIfMatch(String ifMatch, WorkLogId id) {
if (ifMatch == null || ifMatch.isBlank()) {
return;
}
WorkLog current = get.handle(new GetWorkLogQuery(id));
if (!ETags.matches(ifMatch, etagOf(current))) {
throw new PreconditionFailedException("If-Match does not match the current resource version");
}
}
/**
* The owner stamped on a created work log is the authenticated caller's IdP subject, read
* null-safely from the security context. This is the raw principal id (no pseudonymization here).
* See README.
*/
private static String currentOwnerSubject() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null
&& auth.isAuthenticated()
&& auth.getPrincipal() instanceof AuthenticatedPrincipal user) {
return user.idpUserId();
}
return null;
}
/**
* Accepts case-insensitive canonical UUID path input and normalises it to the canonical 36-char
* lowercase UUID. {@code UUID.fromString(...)} is a parser (not a generator) and throws {@link
* IllegalArgumentException} on a malformed id HTTP 400. See README.
*/
private static WorkLogId toId(String raw) {
return WorkLogId.of(UUID.fromString(raw.toLowerCase(Locale.ROOT)).toString());
}
}
@@ -1,11 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
/** Create a new poster (starts in DRAFT with no image). Bean Validation at the edge. */
public record CreatePosterRequest(
@NotBlank(message = "title is required")
@Size(max = 200, message = "title must be at most 200 characters")
String title,
@Size(max = 2000, message = "caption must be at most 2000 characters") String caption) {}
@@ -1,32 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.request;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.request.WorkLogValidationGroups.Invariant;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.request.WorkLogValidationGroups.Syntax;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkCategory;
import jakarta.validation.GroupSequence;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.LocalDate;
import java.util.List;
@GroupSequence({Syntax.class, Invariant.class, CreateWorkLogRequest.class})
public record CreateWorkLogRequest(
@NotBlank(groups = Syntax.class) @Size(max = 200, groups = Syntax.class) String title,
@NotNull(groups = Syntax.class) WorkCategory category,
String summary,
String content,
List<String> techStack,
List<String> links,
@NotNull(groups = Syntax.class) LocalDate periodStart,
LocalDate periodEnd) {
@AssertTrue(message = "periodEnd must not be before periodStart", groups = Invariant.class)
public boolean isPeriodOrdered() {
if (periodStart == null || periodEnd == null) {
return true;
}
return !periodEnd.isBefore(periodStart);
}
}
@@ -1,23 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.request;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
/**
* Reference for safe polymorphic deserialization. {@code @JsonTypeInfo(use = NAME)} plus an
* explicit {@code @JsonSubTypes} allowlist replaces Jackson's unsafe default-typing entry point
* (CVE-2019-14379); the {@code sealed} hierarchy keeps the allowlist mechanically aligned. See
* README.
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind")
@JsonSubTypes({
@JsonSubTypes.Type(value = SamplePolymorphicRequest.Text.class, name = "text"),
@JsonSubTypes.Type(value = SamplePolymorphicRequest.Image.class, name = "image")
})
public sealed interface SamplePolymorphicRequest
permits SamplePolymorphicRequest.Text, SamplePolymorphicRequest.Image {
record Text(String title, String body) implements SamplePolymorphicRequest {}
record Image(String title, String imageUrl) implements SamplePolymorphicRequest {}
}
@@ -1,27 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.request;
import dev.caskeleton.shared.request.Patch;
import org.openapitools.jackson.nullable.JsonNullable;
/**
* Partial update of a poster. {@code JsonNullable} captures the JSON merge-patch 3-state (field
* absent / explicit null / value) at the web edge, converted to {@link Patch} for the command.
* Mirrors {@code UpdateWorkLogRequest}.
*/
public record UpdatePosterRequest(JsonNullable<String> title, JsonNullable<String> caption) {
public Patch<String> titlePatch() {
return toPatch(title);
}
public Patch<String> captionPatch() {
return toPatch(caption);
}
private static <T> Patch<T> toPatch(JsonNullable<T> field) {
if (field == null || !field.isPresent()) {
return Patch.absent();
}
return field.get() == null ? Patch.ofNull() : Patch.of(field.get());
}
}
@@ -1,40 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.request;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkCategory;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkLogStatus;
import dev.caskeleton.shared.request.Patch;
import org.openapitools.jackson.nullable.JsonNullable;
public record UpdateWorkLogRequest(
JsonNullable<String> title,
JsonNullable<String> summary,
JsonNullable<String> content,
JsonNullable<WorkCategory> category,
JsonNullable<WorkLogStatus> status) {
public Patch<String> titlePatch() {
return toPatch(title);
}
public Patch<String> summaryPatch() {
return toPatch(summary);
}
public Patch<String> contentPatch() {
return toPatch(content);
}
public Patch<WorkCategory> categoryPatch() {
return toPatch(category);
}
public Patch<WorkLogStatus> statusPatch() {
return toPatch(status);
}
private static <T> Patch<T> toPatch(JsonNullable<T> field) {
if (field == null || !field.isPresent()) {
return Patch.absent();
}
return field.get() == null ? Patch.ofNull() : Patch.of(field.get());
}
}
@@ -1,10 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.request;
/** Validation group ordering for @GroupSequence (syntax → invariant). */
public final class WorkLogValidationGroups {
private WorkLogValidationGroups() {}
public interface Syntax {}
public interface Invariant {}
}
@@ -1,4 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response;
/** Stable accepted response containing only an opaque operation and relative status path. */
public record PosterImagePublicationResponse(String operationId, String statusPath) {}
@@ -1,10 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response;
/** Locator-free publication progress/result. Reference appears only for READY. */
public record PosterImagePublicationStatusResponse(
String operationId,
String state,
String reference,
long exactLength,
String mediaType,
String sha256) {}
@@ -1,8 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response;
import dev.caskeleton.sample.portfolio.domain.poster.PosterId;
import dev.caskeleton.sample.portfolio.domain.poster.PosterStatus;
/** A poster with its current state. {@code imageKey} is null until an image is uploaded. */
public record PosterResponse(
PosterId id, String title, String caption, String imageKey, PosterStatus status) {}
@@ -1,3 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response;
public record RepoStatsResponse(String fullName, int stars, String lastPushedIso) {}
@@ -1,4 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response;
/** Receipt for an uploaded poster image stored in object storage. */
public record StoredObjectResponse(String key, long size, String contentType, String location) {}
@@ -1,19 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkCategory;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkLogId;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkLogStatus;
import java.time.LocalDate;
import java.util.List;
public record WorkLogResponse(
WorkLogId id,
String title,
WorkCategory category,
WorkLogStatus status,
String summary,
String content,
List<String> techStack,
List<String> links,
LocalDate periodStart,
LocalDate periodEnd) {}
@@ -1,14 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkCategory;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkLogId;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkLogStatus;
import java.time.LocalDate;
public record WorkLogSummaryResponse(
WorkLogId id,
String title,
WorkCategory category,
WorkLogStatus status,
LocalDate periodStart,
LocalDate periodEnd) {}
@@ -1,70 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.error;
import dev.caskeleton.adapter.inbound.web.error.ErrorResponseFactory;
import dev.caskeleton.sample.portfolio.application.exception.PosterNotFoundException;
import dev.caskeleton.sample.portfolio.application.exception.PosterTitleAlreadyExistsException;
import dev.caskeleton.sample.portfolio.application.exception.WorkLogNotFoundException;
import dev.caskeleton.sample.portfolio.domain.poster.PosterInvariantException;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkLogInvariantException;
import dev.caskeleton.shared.response.Envelope;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* Domain-specific exception {@link Envelope} mapping for the portfolio sample. Operational/
* transport/security exceptions are handled by the base {@code GlobalExceptionHandler}. This advice
* is ordered ahead of the base so domain exceptions resolve to their specific code instead of the
* base catch-all's INTERNAL_ERROR. See README.
*/
@Order(Ordered.HIGHEST_PRECEDENCE)
@RestControllerAdvice
public class DomainExceptionHandler {
@ExceptionHandler(WorkLogNotFoundException.class)
public ResponseEntity<Envelope<Void>> handleWorkLogNotFound(WorkLogNotFoundException ex) {
return ErrorResponseFactory.envelope(
PortfolioErrorCode.WORKLOG_NOT_FOUND,
PortfolioClientSafeErrorMessages.forCode(PortfolioErrorCode.WORKLOG_NOT_FOUND),
null);
}
@ExceptionHandler(WorkLogInvariantException.class)
public ResponseEntity<Envelope<Void>> handleWorkLogInvariant(WorkLogInvariantException ex) {
return ErrorResponseFactory.envelope(
PortfolioErrorCode.WORKLOG_CONFLICT,
PortfolioClientSafeErrorMessages.forCode(PortfolioErrorCode.WORKLOG_CONFLICT),
null);
}
@ExceptionHandler(PosterNotFoundException.class)
public ResponseEntity<Envelope<Void>> handlePosterNotFound(PosterNotFoundException ex) {
return ErrorResponseFactory.envelope(
PortfolioErrorCode.POSTER_NOT_FOUND,
PortfolioClientSafeErrorMessages.forCode(PortfolioErrorCode.POSTER_NOT_FOUND),
null);
}
@ExceptionHandler(PosterTitleAlreadyExistsException.class)
public ResponseEntity<Envelope<Void>> handlePosterTitleConflict(
PosterTitleAlreadyExistsException ex) {
return ErrorResponseFactory.envelope(
PortfolioErrorCode.POSTER_ALREADY_EXISTS,
PortfolioClientSafeErrorMessages.forCode(PortfolioErrorCode.POSTER_ALREADY_EXISTS),
null);
}
/** Maps the invariant's {@code reason()} to a specific code (finer-grained than WorkLog's). */
@ExceptionHandler(PosterInvariantException.class)
public ResponseEntity<Envelope<Void>> handlePosterInvariant(PosterInvariantException ex) {
PortfolioErrorCode code =
switch (ex.reason()) {
case IMAGE_REQUIRED -> PortfolioErrorCode.POSTER_IMAGE_REQUIRED;
case INVALID_STATUS_TRANSITION, TITLE_BLANK -> PortfolioErrorCode.POSTER_INVALID_STATE;
};
return ErrorResponseFactory.envelope(
code, PortfolioClientSafeErrorMessages.forCode(code), null);
}
}
@@ -1,17 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.error;
final class PortfolioClientSafeErrorMessages {
private PortfolioClientSafeErrorMessages() {}
static String forCode(PortfolioErrorCode code) {
return switch (code) {
case WORKLOG_NOT_FOUND -> "Work log was not found";
case WORKLOG_CONFLICT -> "Work log conflicts with the current state";
case POSTER_NOT_FOUND -> "Poster was not found";
case POSTER_ALREADY_EXISTS -> "Poster title is already in use";
case POSTER_INVALID_STATE -> "Poster is not valid in the current state";
case POSTER_IMAGE_REQUIRED -> "Poster image is required";
};
}
}
@@ -1,47 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.error;
import dev.caskeleton.shared.error.ApiErrorCode;
import dev.caskeleton.shared.error.Category;
/**
* Domain-specific error codes for the portfolio sample. Operational/transport/ security codes live
* in {@code dev.caskeleton.shared.error.OperationalError}.
*/
public enum PortfolioErrorCode implements ApiErrorCode {
WORKLOG_NOT_FOUND(Category.NOT_FOUND, 404, false),
WORKLOG_CONFLICT(Category.CONFLICT, 409, false),
POSTER_NOT_FOUND(Category.NOT_FOUND, 404, false),
POSTER_ALREADY_EXISTS(Category.CONFLICT, 409, false),
POSTER_INVALID_STATE(Category.VALIDATION, 400, false),
POSTER_IMAGE_REQUIRED(Category.VALIDATION, 400, false);
private final Category category;
private final int httpStatus;
private final boolean retryable;
PortfolioErrorCode(Category category, int httpStatus, boolean retryable) {
this.category = category;
this.httpStatus = httpStatus;
this.retryable = retryable;
}
@Override
public String code() {
return name();
}
@Override
public Category category() {
return category;
}
@Override
public int httpStatus() {
return httpStatus;
}
@Override
public boolean retryable() {
return retryable;
}
}
@@ -1,23 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.json;
import dev.caskeleton.sample.portfolio.domain.poster.PosterId;
import org.springframework.boot.jackson.JacksonComponent;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.ValueSerializer;
/**
* Serialises a {@link PosterId} as its bare 36-character lowercase UUID string instead of the
* record's default {@code {"value":"..."}} shape. Registered via {@link JacksonComponent}. Mirrors
* {@code WorkLogIdSerializer}.
*/
@JacksonComponent
public class PosterIdSerializer extends ValueSerializer<PosterId> {
@Override
public void serialize(PosterId id, JsonGenerator gen, SerializationContext serializers)
throws JacksonException {
gen.writeString(id.value());
}
}
@@ -1,23 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.json;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkLogId;
import org.springframework.boot.jackson.JacksonComponent;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.ValueSerializer;
/**
* Serialises a {@link WorkLogId} as its bare 36-character lowercase UUID string instead of the
* record's default {@code {"value":"..."}} shape. Registered via {@link JacksonComponent}. See
* README.
*/
@JacksonComponent
public class WorkLogIdSerializer extends ValueSerializer<WorkLogId> {
@Override
public void serialize(WorkLogId id, JsonGenerator gen, SerializationContext serializers)
throws JacksonException {
gen.writeString(id.value());
}
}
@@ -1,33 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.mapper;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response.PosterImagePublicationResponse;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response.PosterImagePublicationStatusResponse;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImagePublicationReservation;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImagePublicationStatus;
public final class PosterImagePublicationWebMapper {
private PosterImagePublicationWebMapper() {}
public static PosterImagePublicationResponse toResponse(
PosterImagePublicationReservation reservation) {
return new PosterImagePublicationResponse(
reservation.operationId().value(),
statusPath(reservation.posterId().value(), reservation.operationId().value()));
}
public static PosterImagePublicationStatusResponse toResponse(
PosterImagePublicationStatus status) {
return new PosterImagePublicationStatusResponse(
status.operationId().value(),
status.state().name(),
status.reference() == null ? null : status.reference().value(),
status.contentIdentity().exactLength(),
status.mediaType().canonicalText(),
status.contentIdentity().fullDigest().base64Value());
}
public static String statusPath(String posterId, String operationId) {
return "/posters/" + posterId + "/imagePublications/" + operationId;
}
}
@@ -1,37 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.mapper;
import dev.caskeleton.application.storage.StoredObject;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.request.CreatePosterRequest;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.request.UpdatePosterRequest;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response.PosterResponse;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response.StoredObjectResponse;
import dev.caskeleton.sample.portfolio.application.command.CreatePosterCommand;
import dev.caskeleton.sample.portfolio.application.command.UpdatePosterCommand;
import dev.caskeleton.sample.portfolio.domain.poster.Poster;
import dev.caskeleton.sample.portfolio.domain.poster.PosterId;
/** Translates between poster REST DTOs and application/domain types. */
public final class PosterWebMapper {
private PosterWebMapper() {}
public static CreatePosterCommand toCommand(CreatePosterRequest r) {
return new CreatePosterCommand(r.title(), r.caption());
}
// PATCH goes through the mapper too (not built inline in the controller). The PosterId is parsed
// at the web edge and passed in. Mirrors WorkLogWebMapper.
public static UpdatePosterCommand toUpdateCommand(PosterId id, UpdatePosterRequest body) {
return new UpdatePosterCommand(id, body.titlePatch(), body.captionPatch());
}
public static PosterResponse toResponse(Poster p) {
return new PosterResponse(p.id(), p.title(), p.caption(), p.imageKey(), p.status());
}
// This exact response mapper remains active until data/API migration is complete.
@SuppressWarnings("removal")
public static StoredObjectResponse toStoredObjectResponse(StoredObject s) {
return new StoredObjectResponse(s.key(), s.size(), s.contentType(), s.location().toString());
}
}
@@ -1,79 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.mapper;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.request.CreateWorkLogRequest;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.request.UpdateWorkLogRequest;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response.RepoStatsResponse;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response.WorkLogResponse;
import dev.caskeleton.sample.portfolio.adapter.inbound.web.dto.response.WorkLogSummaryResponse;
import dev.caskeleton.sample.portfolio.application.command.CreateWorkLogCommand;
import dev.caskeleton.sample.portfolio.application.command.UpdateWorkLogCommand;
import dev.caskeleton.sample.portfolio.domain.worklog.RepoStats;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkLog;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkLogId;
import dev.caskeleton.shared.error.MappingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
public final class WorkLogWebMapper {
private WorkLogWebMapper() {}
public static CreateWorkLogCommand toCommand(CreateWorkLogRequest r, String owner) {
List<String> links = r.links() == null ? List.of() : r.links();
for (String link : links) {
try {
new URI(link);
} catch (URISyntaxException e) {
throw new MappingException("invalid link URI: " + link, e);
}
}
return new CreateWorkLogCommand(
owner,
r.title(),
r.category(),
r.summary(),
r.content(),
r.techStack() == null ? List.of() : r.techStack(),
links,
r.periodStart(),
r.periodEnd());
}
// PATCH goes through the mapper too (not built inline in the controller) so the web
// adapter owns all request DTO -> command translation. The WorkLogId is parsed at the
// web edge and passed in. See README.
public static UpdateWorkLogCommand toUpdateCommand(WorkLogId id, UpdateWorkLogRequest body) {
return new UpdateWorkLogCommand(
id,
body.titlePatch(),
body.summaryPatch(),
body.contentPatch(),
body.categoryPatch(),
body.statusPatch());
}
public static WorkLogResponse toResponse(WorkLog w) {
// owner (raw principal id) is intentionally NOT exposed in the response (privacy). See README.
return new WorkLogResponse(
w.id(),
w.title(),
w.category(),
w.status(),
w.summary(),
w.content(),
w.techStack(),
w.links(),
w.period().start(),
w.period().end());
}
public static WorkLogSummaryResponse toSummary(WorkLog w) {
return new WorkLogSummaryResponse(
w.id(), w.title(), w.category(), w.status(), w.period().start(), w.period().end());
}
public static RepoStatsResponse toRepoStatsResponse(RepoStats s) {
return new RepoStatsResponse(s.fullName(), s.stars(), s.lastPushedIso());
}
}
@@ -1,65 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.objectstorage;
import dev.caskeleton.application.objectstorage.content.ObjectChunkSink;
import dev.caskeleton.application.objectstorage.content.ObjectContentProducer;
import dev.caskeleton.application.objectstorage.content.ObjectContentProductionContext;
import dev.caskeleton.application.objectstorage.content.ObjectContentProductionException;
import dev.caskeleton.application.objectstorage.model.ObjectContentIdentity;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.web.multipart.MultipartFile;
/** Single-use request-bound stream bridge; it never calls {@code MultipartFile.getBytes()}. */
public final class MultipartObjectContentProducer implements ObjectContentProducer {
private final MultipartFile file;
private final ObjectContentIdentity expected;
private final AtomicBoolean consumed = new AtomicBoolean();
public MultipartObjectContentProducer(MultipartFile file, ObjectContentIdentity expected) {
if (file == null || expected == null) {
throw new IllegalArgumentException("multipart producer inputs are required");
}
this.file = file;
this.expected = expected;
}
@Override
public void produce(ObjectContentProductionContext context, ObjectChunkSink sink)
throws ObjectContentProductionException {
if (!consumed.compareAndSet(false, true)) {
throw new ObjectContentProductionException("multipart request content is already consumed");
}
try (InputStream input = file.getInputStream()) {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] buffer = new byte[Math.min(context.maximumChunkBytes(), 64 * 1024)];
long delivered = 0;
int read;
while ((read = input.read(buffer)) >= 0) {
if (read == 0) {
continue;
}
delivered += read;
if (delivered > expected.exactLength()) {
throw new ObjectContentProductionException(
"multipart content exceeds its declared length");
}
digest.update(buffer, 0, read);
sink.write(buffer, 0, read);
}
if (delivered != expected.exactLength()
|| !MessageDigest.isEqual(digest.digest(), expected.fullDigest().decodedValue())) {
throw new ObjectContentProductionException(
"multipart content identity does not match its declaration");
}
} catch (IOException exception) {
throw new ObjectContentProductionException(
"multipart request stream could not be read", exception);
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 must be available", exception);
}
}
}
@@ -1,36 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.operation;
import com.github.f4b6a3.uuid.UuidCreator;
import dev.caskeleton.shared.operation.Operation;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Component;
/**
* In-memory long-running-operation store backing the sample {@code :export} fixture. It mints the
* operation id here (web infrastructure), not in the controller, to respect the {@code
* no_uuid_random_in_controller} rule. The export completes synchronously, so the stored operation
* is already {@code SUCCEEDED} when polled. See README.
*/
@Component
public class SampleOperationStore {
private final Map<String, Operation<WorkLogExportResult>> store = new ConcurrentHashMap<>();
/**
* Accepts an export, persisting a terminal operation, and returns the accept-time (PENDING) view
* used for the 202 body. {@code apiBasePath} is the configured version prefix so {@code
* statusUrl} matches the live {@code /operations/{id}} route.
*/
public Operation<WorkLogExportResult> startExport(String apiBasePath, long exportedCount) {
String id = UuidCreator.getTimeOrderedEpochPlus1().toString();
String statusUrl = apiBasePath + "/operations/" + id;
store.put(id, Operation.succeeded(id, statusUrl, new WorkLogExportResult(exportedCount)));
return Operation.pending(id, statusUrl);
}
public Optional<Operation<WorkLogExportResult>> find(String operationId) {
return Optional.ofNullable(store.get(operationId));
}
}
@@ -1,7 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.inbound.web.operation;
/**
* Result payload of the sample long-running {@code :export} operation, surfaced as {@code
* data.result} on the polling response once the operation reaches {@code SUCCEEDED}.
*/
public record WorkLogExportResult(long exportedCount) {}
@@ -1,128 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.identifier;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageIdempotencyScopeDigest;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageIdempotencyScopeDigesterPort;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
/** HMAC-SHA-256 length-prefixed scope digester with a versioned retained key ring. */
public final class HmacPosterImageIdempotencyScopeDigester
implements PosterImageIdempotencyScopeDigesterPort {
private static final byte[] DOMAIN =
"poster-image-idempotency-scope-v1".getBytes(StandardCharsets.UTF_8);
private final Supplier<String> activeVersion;
private final Map<String, byte[]> keys;
public HmacPosterImageIdempotencyScopeDigester(String activeVersion, Map<String, byte[]> keys) {
this(() -> activeVersion, keys);
}
public HmacPosterImageIdempotencyScopeDigester(
Supplier<String> activeVersion, Map<String, byte[]> keys) {
this.activeVersion = Objects.requireNonNull(activeVersion, "activeVersion must be non-null");
Objects.requireNonNull(keys, "keys must be non-null");
Map<String, byte[]> copied = new LinkedHashMap<>();
for (Map.Entry<String, byte[]> entry : keys.entrySet()) {
String version = requireVersion(entry.getKey());
byte[] key =
Objects.requireNonNull(entry.getValue(), "key material must be non-null").clone();
if (key.length < 32 || key.length > 128) {
throw new IllegalArgumentException("idempotency HMAC key length is invalid");
}
copied.put(version, key);
}
if (!copied.containsKey(requireVersion(this.activeVersion.get()))) {
throw new IllegalArgumentException("active idempotency HMAC key is absent");
}
this.keys = Map.copyOf(copied);
}
@Override
public PosterImageIdempotencyScopeDigest digest(
String tenant, String principal, String useCase, String idempotencyKey) {
byte[] canonical =
canonical(
tenant,
bounded(principal, "principal", 256),
bounded(useCase, "useCase", 128),
bounded(idempotencyKey, "idempotencyKey", 256));
String selected = requireVersion(activeVersion.get());
return digest(selected, canonical);
}
@Override
public List<PosterImageIdempotencyScopeDigest> retainedDigests(
String tenant, String principal, String useCase, String idempotencyKey) {
byte[] canonical =
canonical(
tenant,
bounded(principal, "principal", 256),
bounded(useCase, "useCase", 128),
bounded(idempotencyKey, "idempotencyKey", 256));
return keys.keySet().stream().sorted().map(version -> digest(version, canonical)).toList();
}
private static byte[] canonical(
String tenant, String principal, String useCase, String idempotencyKey) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
field(output, DOMAIN);
field(output, new byte[] {(byte) (tenant == null ? 0 : 1)});
field(
output,
tenant == null
? new byte[0]
: bounded(tenant, "tenant", 256).getBytes(StandardCharsets.UTF_8));
field(output, principal.getBytes(StandardCharsets.UTF_8));
field(output, useCase.getBytes(StandardCharsets.UTF_8));
field(output, idempotencyKey.getBytes(StandardCharsets.UTF_8));
return output.toByteArray();
}
private static void field(ByteArrayOutputStream output, byte[] value) {
output.writeBytes(ByteBuffer.allocate(Integer.BYTES).putInt(value.length).array());
output.writeBytes(value);
}
private static String bounded(String value, String label, int maximum) {
if (value == null || value.isBlank() || value.length() > maximum) {
throw new IllegalArgumentException(label + " is invalid");
}
return value;
}
private static String requireVersion(String value) {
if (value == null || !value.matches("[a-z0-9][a-z0-9._-]{0,63}")) {
throw new IllegalArgumentException("idempotency key version is invalid");
}
return value;
}
private PosterImageIdempotencyScopeDigest digest(String version, byte[] canonical) {
byte[] key = keys.get(version);
if (key == null) {
throw new IllegalStateException(
"DB-active idempotency HMAC key is not retained by this process");
}
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
String encoded =
Base64.getUrlEncoder().withoutPadding().encodeToString(mac.doFinal(canonical));
return new PosterImageIdempotencyScopeDigest(version, encoded);
} catch (GeneralSecurityException exception) {
throw new IllegalStateException("HmacSHA256 must be available", exception);
}
}
}
@@ -1,19 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.identifier;
import com.github.f4b6a3.uuid.UuidCreator;
import dev.caskeleton.sample.portfolio.domain.worklog.OutboxEventIdFactory;
import org.springframework.stereotype.Component;
/**
* Infrastructure adapter for the {@link OutboxEventIdFactory} domain port. Uses the same {@code
* UuidCreator.getTimeOrderedEpochPlus1()} mechanism as {@link UuidWorkLogIdFactory}; id generation
* lives here, never in the application layer. See README.
*/
@Component
public class UuidOutboxEventIdFactory implements OutboxEventIdFactory {
@Override
public String newEventId() {
return UuidCreator.getTimeOrderedEpochPlus1().toString();
}
}
@@ -1,21 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.identifier;
import com.github.f4b6a3.uuid.UuidCreator;
import dev.caskeleton.sample.portfolio.domain.poster.PosterId;
import dev.caskeleton.sample.portfolio.domain.poster.PosterIdFactory;
import org.springframework.stereotype.Component;
/**
* Infrastructure adapter for the {@link PosterIdFactory} domain port. {@code
* UuidCreator.getTimeOrderedEpochPlus1()} mints a monotonic UUIDv7 (RFC 9562, time-ordered): it
* increments strictly within a millisecond and is backed by a secure random source. Id generation
* is only allowed in this identifier adapter. Mirrors {@code UuidWorkLogIdFactory}.
*/
@Component
public class UuidPosterIdFactory implements PosterIdFactory {
@Override
public PosterId newId() {
return PosterId.of(UuidCreator.getTimeOrderedEpochPlus1().toString());
}
}
@@ -1,18 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.identifier;
import com.github.f4b6a3.uuid.UuidCreator;
import dev.caskeleton.application.objectstorage.identity.ObjectOperationId;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageOperationIdFactory;
import java.util.UUID;
import org.springframework.stereotype.Component;
/** UUIDv7 allocator for the DB-committed Poster image operation identity. */
@Component
public final class UuidPosterImageOperationIdFactory implements PosterImageOperationIdFactory {
@Override
public AllocatedOperation allocate() {
UUID value = UuidCreator.getTimeOrderedEpochPlus1();
return new AllocatedOperation(value, ObjectOperationId.of(value.toString()));
}
}
@@ -1,21 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.identifier;
import com.github.f4b6a3.uuid.UuidCreator;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkLogId;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkLogIdFactory;
import org.springframework.stereotype.Component;
/**
* Infrastructure adapter for the {@link WorkLogIdFactory} domain port. {@code
* UuidCreator.getTimeOrderedEpochPlus1()} mints a monotonic UUIDv7 (RFC 9562, time-ordered): it
* increments strictly within a millisecond and is backed by a secure random source. This is the
* only place in the sample allowed to generate ids. See README.
*/
@Component
public class UuidWorkLogIdFactory implements WorkLogIdFactory {
@Override
public WorkLogId newId() {
return WorkLogId.of(UuidCreator.getTimeOrderedEpochPlus1().toString());
}
}
@@ -1,17 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.persistence.config;
import org.springframework.boot.persistence.autoconfigure.EntityScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
/**
* The Spring Boot main class lives in {@code dev.caskeleton.bootstrap}, so the default
* package-relative scan misses {@code
* dev.caskeleton.sample.portfolio.adapter.outbound.persistence.*}. Pointing entity and repository
* scans here keeps that wiring in the module that actually owns them.
*/
@Configuration
@EntityScan(basePackages = "dev.caskeleton.sample.portfolio.adapter.outbound.persistence.entity")
@EnableJpaRepositories(
basePackages = "dev.caskeleton.sample.portfolio.adapter.outbound.persistence.repository")
public class JpaConfig {}
@@ -1,103 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.persistence.entity;
import dev.caskeleton.adapter.outbound.persistence.audit.AuditableEntity;
import dev.caskeleton.sample.portfolio.domain.poster.PosterStatus;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.util.UUID;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
// Extends AuditableEntity to inherit the created/updated audit columns; the domain Poster carries
// none of them they are stamped by PosterRepositoryAdapter. Mirrors WorkLogEntity.
@Entity
@Table(name = "poster")
public class PosterEntity extends AuditableEntity {
// UUIDv7 stored as the PostgreSQL native `uuid` type (16-byte binary), not varchar.
// UUID string<->uuid conversion happens in PosterPersistenceMapper.
@Id
@Column(name = "id", columnDefinition = "uuid", nullable = false, updatable = false)
@JdbcTypeCode(SqlTypes.UUID)
private UUID id;
private String title;
@Column(length = 2000)
private String caption;
// Object-storage key of the uploaded image; null until an image is attached.
@Column(name = "image_key", length = 512)
private String imageKey;
@Column(name = "image_reference", length = 96)
private String imageReference;
@Enumerated(EnumType.STRING)
private PosterStatus status;
// Optimistic-lock version (ETag source). Hibernate manages increments; `null` marks a new row so
// save() inserts.
@Version private Long version;
protected PosterEntity() {}
public PosterEntity(
UUID id, String title, String caption, String imageKey, PosterStatus status, Long version) {
this(id, title, caption, imageKey, null, status, version);
}
public PosterEntity(
UUID id,
String title,
String caption,
String imageKey,
String imageReference,
PosterStatus status,
Long version) {
if (imageKey != null && imageReference != null) {
throw new IllegalArgumentException(
"poster entity cannot contain both image key and image reference");
}
this.id = id;
this.title = title;
this.caption = caption;
this.imageKey = imageKey;
this.imageReference = imageReference;
this.status = status;
this.version = version;
}
public UUID getId() {
return id;
}
public String getTitle() {
return title;
}
public String getCaption() {
return caption;
}
public String getImageKey() {
return imageKey;
}
public String getImageReference() {
return imageReference;
}
public PosterStatus getStatus() {
return status;
}
public Long getVersion() {
return version;
}
}
@@ -1,58 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.persistence.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.time.Instant;
/** Single-row DB coordination fence for publication idempotency HMAC rotation. */
@Entity
@Table(name = "poster_image_idempotency_key_epoch")
public class PosterImageIdempotencyKeyEpochEntity {
@Id
@Column(name = "epoch_id", nullable = false, length = 64, updatable = false)
private String epochId;
@Column(name = "active_key_version", nullable = false, length = 64)
private String activeKeyVersion;
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@Version
@Column(nullable = false)
private long revision;
protected PosterImageIdempotencyKeyEpochEntity() {}
public PosterImageIdempotencyKeyEpochEntity(
String epochId, String activeKeyVersion, Instant updatedAt) {
this.epochId = epochId;
this.activeKeyVersion = activeKeyVersion;
this.updatedAt = updatedAt;
}
public void activate(String activeKeyVersion, Instant now) {
this.activeKeyVersion = activeKeyVersion;
this.updatedAt = now;
}
public String getEpochId() {
return epochId;
}
public String getActiveKeyVersion() {
return activeKeyVersion;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public long getRevision() {
return revision;
}
}
@@ -1,198 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.persistence.entity;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageRetirementIntentState;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.persistence.Version;
import java.time.Instant;
import java.util.UUID;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
/** JPA row for exact-reference logical retirement work. */
@Entity
@Table(
name = "poster_image_retirement_intent",
uniqueConstraints = {
@UniqueConstraint(
name = "uq_poster_image_retirement_operation",
columnNames = "operation_id"),
@UniqueConstraint(
name = "uq_poster_image_retirement_exact",
columnNames = {"image_reference", "exact_version"})
})
public class PosterImageRetirementIntentEntity {
@Id
@JdbcTypeCode(SqlTypes.UUID)
@Column(nullable = false, updatable = false)
private UUID id;
@JdbcTypeCode(SqlTypes.UUID)
@Column(name = "poster_id", updatable = false)
private UUID posterId;
@Column(name = "operation_id", nullable = false, length = 64, updatable = false)
private String operationId;
@Column(name = "image_reference", nullable = false, length = 96, updatable = false)
private String imageReference;
@Column(name = "exact_version", nullable = false, length = 96, updatable = false)
private String exactVersion;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 32)
private PosterImageRetirementIntentState state;
@Column(name = "claimant_digest", length = 64)
private String claimantDigest;
@Column(name = "claim_fence")
private Long claimFence;
@Column(name = "claim_expires_at")
private Instant claimExpiresAt;
@Column(name = "retain_until", nullable = false, updatable = false)
private Instant retainUntil;
@Column(name = "retry_at")
private Instant retryAt;
@Column(name = "attempt_count", nullable = false)
private int attemptCount;
@Column(name = "last_error_code", length = 64)
private String lastErrorCode;
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@Version
@Column(nullable = false)
private long revision;
protected PosterImageRetirementIntentEntity() {}
public PosterImageRetirementIntentEntity(
UUID id,
UUID posterId,
String operationId,
String imageReference,
String exactVersion,
PosterImageRetirementIntentState state,
String claimantDigest,
Long claimFence,
Instant claimExpiresAt,
Instant retainUntil,
Instant retryAt,
int attemptCount,
String lastErrorCode,
Instant createdAt,
Instant updatedAt,
long revision) {
this.id = id;
this.posterId = posterId;
this.operationId = operationId;
this.imageReference = imageReference;
this.exactVersion = exactVersion;
this.state = state;
this.claimantDigest = claimantDigest;
this.claimFence = claimFence;
this.claimExpiresAt = claimExpiresAt;
this.retainUntil = retainUntil;
this.retryAt = retryAt;
this.attemptCount = attemptCount;
this.lastErrorCode = lastErrorCode;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
this.revision = revision;
}
public void applyMutableState(PosterImageRetirementIntentEntity source) {
if (!id.equals(source.id) || revision + 1 != source.revision) {
throw new IllegalArgumentException("retirement intent replacement revision conflicts");
}
state = source.state;
claimantDigest = source.claimantDigest;
claimFence = source.claimFence;
claimExpiresAt = source.claimExpiresAt;
retryAt = source.retryAt;
attemptCount = source.attemptCount;
lastErrorCode = source.lastErrorCode;
updatedAt = source.updatedAt;
}
public UUID getId() {
return id;
}
public UUID getPosterId() {
return posterId;
}
public String getOperationId() {
return operationId;
}
public String getImageReference() {
return imageReference;
}
public String getExactVersion() {
return exactVersion;
}
public PosterImageRetirementIntentState getState() {
return state;
}
public String getClaimantDigest() {
return claimantDigest;
}
public Long getClaimFence() {
return claimFence;
}
public Instant getClaimExpiresAt() {
return claimExpiresAt;
}
public Instant getRetainUntil() {
return retainUntil;
}
public Instant getRetryAt() {
return retryAt;
}
public int getAttemptCount() {
return attemptCount;
}
public String getLastErrorCode() {
return lastErrorCode;
}
public Instant getCreatedAt() {
return createdAt;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public long getRevision() {
return revision;
}
}
@@ -1,311 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.persistence.entity;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageUploadIntentState;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.persistence.Version;
import java.time.Instant;
import java.util.UUID;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
/** JPA representation of the V8 durable publication queue; no payload or provider locator. */
@Entity
@Table(
name = "poster_image_upload_intent",
uniqueConstraints = {
@UniqueConstraint(
name = "uq_poster_image_upload_operation",
columnNames = {"operation_destination", "operation_epoch", "operation_id"}),
@UniqueConstraint(
name = "uq_poster_image_upload_scope",
columnNames = {"hmac_key_version", "idempotency_scope_digest"})
})
public class PosterImageUploadIntentEntity {
@Id
@JdbcTypeCode(SqlTypes.UUID)
@Column(name = "id", nullable = false, updatable = false)
private UUID id;
@JdbcTypeCode(SqlTypes.UUID)
@Column(name = "poster_id", nullable = false, updatable = false)
private UUID posterId;
@Column(name = "operation_destination", nullable = false, length = 64, updatable = false)
private String operationDestination;
@Column(name = "operation_epoch", nullable = false, length = 64, updatable = false)
private String operationEpoch;
@Column(name = "operation_id", nullable = false, length = 64, updatable = false)
private String operationId;
@Column(name = "hmac_key_version", nullable = false, length = 64, updatable = false)
private String hmacKeyVersion;
@Column(name = "idempotency_scope_digest", nullable = false, length = 43, updatable = false)
private String idempotencyScopeDigest;
@Column(name = "request_fingerprint", nullable = false, length = 64, updatable = false)
private String requestFingerprint;
@Column(name = "expected_poster_version", nullable = false, updatable = false)
private long expectedPosterVersion;
@Column(name = "content_length", nullable = false, updatable = false)
private long contentLength;
@Column(name = "content_sha256", nullable = false, length = 44, updatable = false)
private String contentSha256;
@Column(name = "media_type", nullable = false, length = 127, updatable = false)
private String mediaType;
@Enumerated(EnumType.STRING)
@Column(name = "state", nullable = false, length = 32)
private PosterImageUploadIntentState state;
@Column(name = "stage_handle", length = 96)
private String stageHandle;
@Column(name = "exact_version", length = 96)
private String exactVersion;
@Column(name = "handoff_claimant_digest", length = 64)
private String handoffClaimantDigest;
@Column(name = "handoff_fence")
private Long handoffFence;
@Column(name = "handoff_expires_at")
private Instant handoffExpiresAt;
@Column(name = "published_reference", length = 96)
private String publishedReference;
@Column(name = "published_version", length = 96)
private String publishedVersion;
@Column(name = "replaced_reference", length = 96)
private String replacedReference;
@Column(name = "retry_at")
private Instant retryAt;
@Column(name = "expires_at", nullable = false, updatable = false)
private Instant expiresAt;
@Column(name = "attempt_count", nullable = false)
private int attemptCount;
@Column(name = "last_error_code", length = 64)
private String lastErrorCode;
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@Version
@Column(name = "revision", nullable = false)
private long revision;
protected PosterImageUploadIntentEntity() {}
public PosterImageUploadIntentEntity(
UUID id,
UUID posterId,
String operationDestination,
String operationEpoch,
String operationId,
String hmacKeyVersion,
String idempotencyScopeDigest,
String requestFingerprint,
long expectedPosterVersion,
long contentLength,
String contentSha256,
String mediaType,
PosterImageUploadIntentState state,
String stageHandle,
String exactVersion,
String handoffClaimantDigest,
Long handoffFence,
Instant handoffExpiresAt,
String publishedReference,
String publishedVersion,
String replacedReference,
Instant retryAt,
Instant expiresAt,
int attemptCount,
String lastErrorCode,
Instant createdAt,
Instant updatedAt,
long revision) {
this.id = id;
this.posterId = posterId;
this.operationDestination = operationDestination;
this.operationEpoch = operationEpoch;
this.operationId = operationId;
this.hmacKeyVersion = hmacKeyVersion;
this.idempotencyScopeDigest = idempotencyScopeDigest;
this.requestFingerprint = requestFingerprint;
this.expectedPosterVersion = expectedPosterVersion;
this.contentLength = contentLength;
this.contentSha256 = contentSha256;
this.mediaType = mediaType;
this.state = state;
this.stageHandle = stageHandle;
this.exactVersion = exactVersion;
this.handoffClaimantDigest = handoffClaimantDigest;
this.handoffFence = handoffFence;
this.handoffExpiresAt = handoffExpiresAt;
this.publishedReference = publishedReference;
this.publishedVersion = publishedVersion;
this.replacedReference = replacedReference;
this.retryAt = retryAt;
this.expiresAt = expiresAt;
this.attemptCount = attemptCount;
this.lastErrorCode = lastErrorCode;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
this.revision = revision;
}
public void applyMutableState(PosterImageUploadIntentEntity source) {
if (!id.equals(source.id) || revision + 1 != source.revision) {
throw new IllegalArgumentException("upload intent replacement revision conflicts");
}
state = source.state;
stageHandle = source.stageHandle;
exactVersion = source.exactVersion;
handoffClaimantDigest = source.handoffClaimantDigest;
handoffFence = source.handoffFence;
handoffExpiresAt = source.handoffExpiresAt;
publishedReference = source.publishedReference;
publishedVersion = source.publishedVersion;
replacedReference = source.replacedReference;
retryAt = source.retryAt;
attemptCount = source.attemptCount;
lastErrorCode = source.lastErrorCode;
updatedAt = source.updatedAt;
}
public UUID getId() {
return id;
}
public UUID getPosterId() {
return posterId;
}
public String getOperationDestination() {
return operationDestination;
}
public String getOperationEpoch() {
return operationEpoch;
}
public String getOperationId() {
return operationId;
}
public String getHmacKeyVersion() {
return hmacKeyVersion;
}
public String getIdempotencyScopeDigest() {
return idempotencyScopeDigest;
}
public String getRequestFingerprint() {
return requestFingerprint;
}
public long getExpectedPosterVersion() {
return expectedPosterVersion;
}
public long getContentLength() {
return contentLength;
}
public String getContentSha256() {
return contentSha256;
}
public String getMediaType() {
return mediaType;
}
public PosterImageUploadIntentState getState() {
return state;
}
public String getStageHandle() {
return stageHandle;
}
public String getExactVersion() {
return exactVersion;
}
public String getHandoffClaimantDigest() {
return handoffClaimantDigest;
}
public Long getHandoffFence() {
return handoffFence;
}
public Instant getHandoffExpiresAt() {
return handoffExpiresAt;
}
public String getPublishedReference() {
return publishedReference;
}
public String getPublishedVersion() {
return publishedVersion;
}
public String getReplacedReference() {
return replacedReference;
}
public Instant getRetryAt() {
return retryAt;
}
public Instant getExpiresAt() {
return expiresAt;
}
public int getAttemptCount() {
return attemptCount;
}
public String getLastErrorCode() {
return lastErrorCode;
}
public Instant getCreatedAt() {
return createdAt;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public long getRevision() {
return revision;
}
}
@@ -1,170 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.persistence.entity;
import dev.caskeleton.adapter.outbound.persistence.audit.AuditableEntity;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkCategory;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkLogStatus;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
// Extends AuditableEntity to inherit the created/updated audit columns; the domain WorkLog
// carries none of them they are stamped by WorkLogRepositoryAdapter. See README.
@Entity
@Table(name = "work_log")
public class WorkLogEntity extends AuditableEntity {
// UUIDv7 stored as the PostgreSQL native `uuid` type (16-byte binary), not varchar.
// UUID string<->uuid conversion happens in WorkLogPersistenceMapper. See README.
@Id
@Column(name = "id", columnDefinition = "uuid", nullable = false, updatable = false)
@JdbcTypeCode(SqlTypes.UUID)
private UUID id;
private String owner;
private String title;
@Enumerated(EnumType.STRING)
private WorkCategory category;
@Enumerated(EnumType.STRING)
private WorkLogStatus status;
@Column(length = 500)
private String summary;
@Column(length = 10000)
private String content;
@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "work_log_tech_stack", joinColumns = @JoinColumn(name = "work_log_id"))
@Column(name = "tech")
private List<String> techStack = new ArrayList<>();
@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "work_log_link", joinColumns = @JoinColumn(name = "work_log_id"))
@Column(name = "url")
private List<String> links = new ArrayList<>();
private LocalDate periodStart;
private LocalDate periodEnd;
// Optimistic-lock version (ETag/If-Match source). Hibernate manages increments; `null`
// marks a new row so save() inserts. See README.
@Version private Long version;
protected WorkLogEntity() {}
public WorkLogEntity(
UUID id,
String title,
WorkCategory category,
String summary,
String content,
List<String> techStack,
List<String> links,
LocalDate periodStart,
LocalDate periodEnd,
Long version) {
this(
id,
"sample-owner",
title,
category,
WorkLogStatus.OPEN,
summary,
content,
techStack,
links,
periodStart,
periodEnd,
version);
}
public WorkLogEntity(
UUID id,
String owner,
String title,
WorkCategory category,
WorkLogStatus status,
String summary,
String content,
List<String> techStack,
List<String> links,
LocalDate periodStart,
LocalDate periodEnd,
Long version) {
this.id = id;
this.owner = owner;
this.title = title;
this.category = category;
this.status = status;
this.summary = summary;
this.content = content;
this.techStack = new ArrayList<>(techStack);
this.links = new ArrayList<>(links);
this.periodStart = periodStart;
this.periodEnd = periodEnd;
this.version = version;
}
public UUID getId() {
return id;
}
public String getOwner() {
return owner;
}
public String getTitle() {
return title;
}
public WorkCategory getCategory() {
return category;
}
public WorkLogStatus getStatus() {
return status;
}
public String getSummary() {
return summary;
}
public String getContent() {
return content;
}
public List<String> getTechStack() {
return techStack;
}
public List<String> getLinks() {
return links;
}
public LocalDate getPeriodStart() {
return periodStart;
}
public LocalDate getPeriodEnd() {
return periodEnd;
}
public Long getVersion() {
return version;
}
}
@@ -1,54 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.persistence.mapper;
import dev.caskeleton.application.objectstorage.identity.ObjectOperationId;
import dev.caskeleton.application.objectstorage.identity.ObjectVersionToken;
import dev.caskeleton.sample.portfolio.adapter.outbound.persistence.entity.PosterImageRetirementIntentEntity;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageRetirementIntent;
import dev.caskeleton.sample.portfolio.domain.poster.PosterId;
import dev.caskeleton.sample.portfolio.domain.poster.PosterImageReference;
/** Pure V8 retirement-intent domain/JPA translation. */
public final class PosterImageRetirementIntentPersistenceMapper {
private PosterImageRetirementIntentPersistenceMapper() {}
public static PosterImageRetirementIntentEntity toEntity(PosterImageRetirementIntent intent) {
return new PosterImageRetirementIntentEntity(
intent.id(),
intent.posterId() == null ? null : PosterPersistenceMapper.toUuid(intent.posterId()),
intent.operationId().value(),
intent.reference().value(),
intent.exactVersion().canonicalText(),
intent.state(),
intent.claimantDigest(),
intent.claimFence(),
intent.claimExpiresAt(),
intent.retainUntil(),
intent.retryAt(),
intent.attemptCount(),
intent.lastErrorCode(),
intent.createdAt(),
intent.updatedAt(),
intent.revision());
}
public static PosterImageRetirementIntent toDomain(PosterImageRetirementIntentEntity entity) {
return new PosterImageRetirementIntent(
entity.getId(),
entity.getPosterId() == null ? null : PosterId.of(entity.getPosterId().toString()),
ObjectOperationId.of(entity.getOperationId()),
PosterImageReference.of(entity.getImageReference()),
ObjectVersionToken.parse(entity.getExactVersion()),
entity.getState(),
entity.getClaimantDigest(),
entity.getClaimFence(),
entity.getClaimExpiresAt(),
entity.getRetainUntil(),
entity.getRetryAt(),
entity.getAttemptCount(),
entity.getLastErrorCode(),
entity.getCreatedAt(),
entity.getUpdatedAt(),
entity.getRevision());
}
}
@@ -1,110 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.persistence.mapper;
import dev.caskeleton.application.idempotency.RequestFingerprint;
import dev.caskeleton.application.objectstorage.identity.ObjectDestinationId;
import dev.caskeleton.application.objectstorage.identity.ObjectOperationEpoch;
import dev.caskeleton.application.objectstorage.identity.ObjectOperationId;
import dev.caskeleton.application.objectstorage.identity.ObjectOperationKey;
import dev.caskeleton.application.objectstorage.identity.ObjectStageHandle;
import dev.caskeleton.application.objectstorage.identity.ObjectVersionToken;
import dev.caskeleton.application.objectstorage.model.ObjectContentIdentity;
import dev.caskeleton.application.objectstorage.model.ObjectDigest;
import dev.caskeleton.application.objectstorage.model.ObjectDigestAlgorithm;
import dev.caskeleton.application.objectstorage.model.ObjectMediaType;
import dev.caskeleton.sample.portfolio.adapter.outbound.persistence.entity.PosterImageUploadIntentEntity;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageIdempotencyScopeDigest;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageUploadIntent;
import dev.caskeleton.sample.portfolio.domain.poster.PosterId;
import dev.caskeleton.sample.portfolio.domain.poster.PosterImageReference;
/** Pure V8 upload-intent domain/JPA translation. */
public final class PosterImageUploadIntentPersistenceMapper {
private PosterImageUploadIntentPersistenceMapper() {}
public static PosterImageUploadIntentEntity toEntity(PosterImageUploadIntent intent) {
return new PosterImageUploadIntentEntity(
intent.id(),
PosterPersistenceMapper.toUuid(intent.posterId()),
intent.operationKey().destination().value(),
intent.operationKey().epoch().value(),
intent.operationKey().operationId().value(),
intent.idempotencyScope().keyVersion(),
intent.idempotencyScope().base64UrlDigest(),
intent.requestFingerprint().hex(),
intent.expectedPosterVersion(),
intent.contentIdentity().exactLength(),
intent.contentIdentity().fullDigest().base64Value(),
intent.mediaType().canonicalText(),
intent.state(),
text(intent.stageHandle()),
text(intent.exactVersion()),
intent.handoffClaimantDigest(),
intent.handoffFence(),
intent.handoffExpiresAt(),
text(intent.publishedReference()),
text(intent.publishedVersion()),
text(intent.replacedReference()),
intent.retryAt(),
intent.expiresAt(),
intent.attemptCount(),
intent.lastErrorCode(),
intent.createdAt(),
intent.updatedAt(),
intent.revision());
}
public static PosterImageUploadIntent toDomain(PosterImageUploadIntentEntity entity) {
return new PosterImageUploadIntent(
entity.getId(),
PosterId.of(entity.getPosterId().toString()),
new ObjectOperationKey(
ObjectDestinationId.of(entity.getOperationDestination()),
ObjectOperationEpoch.of(entity.getOperationEpoch()),
ObjectOperationId.of(entity.getOperationId())),
new PosterImageIdempotencyScopeDigest(
entity.getHmacKeyVersion(), entity.getIdempotencyScopeDigest()),
new RequestFingerprint(entity.getRequestFingerprint()),
entity.getExpectedPosterVersion(),
new ObjectContentIdentity(
entity.getContentLength(),
ObjectDigest.of(ObjectDigestAlgorithm.SHA_256, entity.getContentSha256())),
ObjectMediaType.of(entity.getMediaType()),
entity.getState(),
entity.getStageHandle() == null ? null : ObjectStageHandle.parse(entity.getStageHandle()),
entity.getExactVersion() == null
? null
: ObjectVersionToken.parse(entity.getExactVersion()),
entity.getHandoffClaimantDigest(),
entity.getHandoffFence(),
entity.getHandoffExpiresAt(),
entity.getPublishedReference() == null
? null
: PosterImageReference.of(entity.getPublishedReference()),
entity.getPublishedVersion() == null
? null
: ObjectVersionToken.parse(entity.getPublishedVersion()),
entity.getReplacedReference() == null
? null
: PosterImageReference.of(entity.getReplacedReference()),
entity.getRetryAt(),
entity.getExpiresAt(),
entity.getAttemptCount(),
entity.getLastErrorCode(),
entity.getCreatedAt(),
entity.getUpdatedAt(),
entity.getRevision());
}
private static String text(ObjectStageHandle handle) {
return handle == null ? null : handle.canonicalText();
}
private static String text(ObjectVersionToken version) {
return version == null ? null : version.canonicalText();
}
private static String text(PosterImageReference reference) {
return reference == null ? null : reference.value();
}
}
@@ -1,50 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.persistence.mapper;
import dev.caskeleton.sample.portfolio.adapter.outbound.persistence.entity.PosterEntity;
import dev.caskeleton.sample.portfolio.domain.poster.Poster;
import dev.caskeleton.sample.portfolio.domain.poster.PosterId;
import java.util.UUID;
/**
* Pure translation between the {@link Poster} aggregate and its JPA entity. Mirrors {@code
* WorkLogPersistenceMapper}.
*/
public final class PosterPersistenceMapper {
private PosterPersistenceMapper() {}
/**
* Canonical UUID string -> PostgreSQL native {@code uuid} (128-bit). Uses {@code java.util.UUID}
* (JDK stdlib) directly, so the persistence adapter carries no id-library dependency.
*/
public static UUID toUuid(PosterId id) {
return UUID.fromString(id.value());
}
public static PosterEntity toEntity(Poster p) {
// Carry the domain version so JPA tells a new row (null insert) from a tracked one.
return new PosterEntity(
toUuid(p.id()),
p.title(),
p.caption(),
p.imageAttachment()
instanceof dev.caskeleton.sample.portfolio.domain.poster.LegacyPosterImageAttachment
? p.imageKey()
: null,
p.imageReference(),
p.status(),
p.version());
}
public static Poster toDomain(PosterEntity e) {
PosterId id = PosterId.of(e.getId().toString());
return Poster.rehydrate(
id,
e.getTitle(),
e.getCaption(),
e.getImageKey(),
e.getImageReference(),
e.getStatus(),
e.getVersion());
}
}
@@ -1,54 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.persistence.mapper;
import dev.caskeleton.sample.portfolio.adapter.outbound.persistence.entity.WorkLogEntity;
import dev.caskeleton.sample.portfolio.domain.worklog.Period;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkLog;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkLogId;
import dev.caskeleton.sample.portfolio.domain.worklog.WorkLogOwner;
import java.util.UUID;
public final class WorkLogPersistenceMapper {
private WorkLogPersistenceMapper() {}
/**
* Canonical UUID string -> PostgreSQL native {@code uuid} (128-bit). Uses {@code java.util.UUID}
* (JDK stdlib) directly, so the persistence adapter carries no id-library dependency. See README.
*/
public static UUID toUuid(WorkLogId id) {
return UUID.fromString(id.value());
}
public static WorkLogEntity toEntity(WorkLog w) {
// Carry the domain version so JPA tells a new row (null insert) from a tracked one.
return new WorkLogEntity(
toUuid(w.id()),
w.owner().value(),
w.title(),
w.category(),
w.status(),
w.summary(),
w.content(),
w.techStack(),
w.links(),
w.period().start(),
w.period().end(),
w.version());
}
public static WorkLog toDomain(WorkLogEntity e) {
WorkLogId id = WorkLogId.of(e.getId().toString());
return WorkLog.rehydrate(
id,
WorkLogOwner.of(e.getOwner()),
e.getTitle(),
e.getCategory(),
e.getStatus(),
e.getSummary(),
e.getContent(),
e.getTechStack(),
e.getLinks(),
new Period(e.getPeriodStart(), e.getPeriodEnd()),
e.getVersion());
}
}
@@ -1,72 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.persistence.repository;
import dev.caskeleton.adapter.outbound.persistence.audit.AuditContextPort;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageAttachmentCompareAndSetPort;
import dev.caskeleton.sample.portfolio.domain.poster.PosterId;
import dev.caskeleton.sample.portfolio.domain.poster.PosterImageReference;
import jakarta.persistence.EntityManager;
import java.time.Instant;
import java.util.List;
import org.springframework.stereotype.Repository;
/** PostgreSQL conditional V8 attachment update that returns the exact replaced reference. */
@Repository
public class PosterImageAttachmentCasRepository implements PosterImageAttachmentCompareAndSetPort {
private static final String ATTACH_SQL =
"""
WITH candidate AS (
SELECT id, image_reference AS replaced_reference
FROM poster
WHERE id = :posterId
AND version = :expectedVersion
AND image_key IS NULL
FOR UPDATE
),
updated AS (
UPDATE poster AS poster
SET image_reference = :reference,
updated_at = :updatedAt,
updated_by = :updatedBy,
version = poster.version + 1
FROM candidate
WHERE poster.id = candidate.id
RETURNING candidate.replaced_reference
)
SELECT TRUE AS attached, replaced_reference
FROM updated
""";
private final EntityManager entityManager;
private final AuditContextPort auditContext;
public PosterImageAttachmentCasRepository(
EntityManager entityManager, AuditContextPort auditContext) {
this.entityManager = entityManager;
this.auditContext = auditContext;
}
@Override
public Result attach(
PosterId posterId, long expectedPosterVersion, PosterImageReference reference, Instant now) {
java.util.Objects.requireNonNull(now, "now must be non-null");
@SuppressWarnings("unchecked")
List<Object[]> rows =
entityManager
.createNativeQuery(ATTACH_SQL)
.setParameter(
"posterId",
dev.caskeleton.sample.portfolio.adapter.outbound.persistence.mapper
.PosterPersistenceMapper.toUuid(posterId))
.setParameter("expectedVersion", expectedPosterVersion)
.setParameter("reference", reference.value())
.setParameter("updatedAt", now)
.setParameter("updatedBy", auditContext.currentActor())
.getResultList();
if (rows.isEmpty()) {
return new Result(false, null);
}
String replaced = (String) rows.getFirst()[1];
return new Result(true, replaced == null ? null : PosterImageReference.of(replaced));
}
}
@@ -1,20 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.persistence.repository;
import dev.caskeleton.sample.portfolio.adapter.outbound.persistence.entity.PosterImageIdempotencyKeyEpochEntity;
import jakarta.persistence.LockModeType;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface PosterImageIdempotencyKeyEpochJpaRepository
extends JpaRepository<PosterImageIdempotencyKeyEpochEntity, String> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query(
"select epoch from PosterImageIdempotencyKeyEpochEntity epoch"
+ " where epoch.epochId = :epochId")
Optional<PosterImageIdempotencyKeyEpochEntity> findByIdForUpdate(
@Param("epochId") String epochId);
}
@@ -1,48 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.persistence.repository;
import dev.caskeleton.sample.portfolio.adapter.outbound.persistence.entity.PosterImageIdempotencyKeyEpochEntity;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageIdempotencyKeyEpochPort;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageUploadIntentConflictException;
import java.time.Clock;
import org.springframework.stereotype.Repository;
@Repository
public class PosterImageIdempotencyKeyEpochRepositoryAdapter
implements PosterImageIdempotencyKeyEpochPort {
static final String EPOCH_ID = "poster-image-publication";
private final PosterImageIdempotencyKeyEpochJpaRepository jpa;
private final Clock clock;
public PosterImageIdempotencyKeyEpochRepositoryAdapter(
PosterImageIdempotencyKeyEpochJpaRepository jpa, Clock clock) {
this.jpa = jpa;
this.clock = clock;
}
@Override
public String activeVersionForReservation() {
return jpa.findByIdForUpdate(EPOCH_ID)
.orElseThrow(
() ->
new IllegalStateException("poster image idempotency key epoch is not initialized"))
.getActiveKeyVersion();
}
@Override
public void activate(String expectedVersion, String newVersion) {
PosterImageIdempotencyKeyEpochEntity epoch =
jpa.findByIdForUpdate(EPOCH_ID)
.orElseGet(
() ->
new PosterImageIdempotencyKeyEpochEntity(
EPOCH_ID, expectedVersion, clock.instant()));
if (!epoch.getActiveKeyVersion().equals(expectedVersion)) {
throw new PosterImageUploadIntentConflictException(
"poster image idempotency key epoch conflicts");
}
epoch.activate(newVersion, clock.instant());
jpa.saveAndFlush(epoch);
}
}
@@ -1,40 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.persistence.repository;
import dev.caskeleton.sample.portfolio.adapter.outbound.persistence.entity.PosterImageRetirementIntentEntity;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageRetirementIntentState;
import jakarta.persistence.LockModeType;
import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface PosterImageRetirementIntentJpaRepository
extends JpaRepository<PosterImageRetirementIntentEntity, UUID> {
Optional<PosterImageRetirementIntentEntity> findByOperationId(String operationId);
Optional<PosterImageRetirementIntentEntity> findByImageReferenceAndExactVersion(
String reference, String exactVersion);
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query(
"""
select intent
from PosterImageRetirementIntentEntity intent
where intent.state in :states
and intent.retainUntil <= :now
and (intent.retryAt is null or intent.retryAt <= :now)
and (intent.claimExpiresAt is null or intent.claimExpiresAt <= :now)
order by intent.updatedAt, intent.id
""")
List<PosterImageRetirementIntentEntity> findClaimable(
@Param("states") Collection<PosterImageRetirementIntentState> states,
@Param("now") Instant now,
Pageable pageable);
}
@@ -1,95 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.persistence.repository;
import dev.caskeleton.application.objectstorage.identity.ObjectOperationId;
import dev.caskeleton.sample.portfolio.adapter.outbound.persistence.entity.PosterImageRetirementIntentEntity;
import dev.caskeleton.sample.portfolio.adapter.outbound.persistence.mapper.PosterImageRetirementIntentPersistenceMapper;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageRetirementIntent;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageRetirementIntentState;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageRetirementIntentStorePort;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageUploadIntentConflictException;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Repository;
@Repository
public class PosterImageRetirementIntentRepositoryAdapter
implements PosterImageRetirementIntentStorePort {
private final PosterImageRetirementIntentJpaRepository jpa;
public PosterImageRetirementIntentRepositoryAdapter(
PosterImageRetirementIntentJpaRepository jpa) {
this.jpa = jpa;
}
@Override
public PosterImageRetirementIntent createOrRead(PosterImageRetirementIntent proposed) {
Optional<PosterImageRetirementIntentEntity> existing =
jpa.findByOperationId(proposed.operationId().value());
if (existing.isEmpty()) {
existing =
jpa.findByImageReferenceAndExactVersion(
proposed.reference().value(), proposed.exactVersion().canonicalText());
}
if (existing.isPresent()) {
PosterImageRetirementIntent found =
PosterImageRetirementIntentPersistenceMapper.toDomain(existing.orElseThrow());
if (!found.operationId().equals(proposed.operationId())
|| !found.reference().equals(proposed.reference())
|| !found.exactVersion().equals(proposed.exactVersion())) {
throw new PosterImageUploadIntentConflictException(
"poster image retirement operation conflicts");
}
return found;
}
return PosterImageRetirementIntentPersistenceMapper.toDomain(
jpa.saveAndFlush(PosterImageRetirementIntentPersistenceMapper.toEntity(proposed)));
}
@Override
public Optional<PosterImageRetirementIntent> findByOperation(ObjectOperationId operation) {
return jpa.findByOperationId(operation.value())
.map(PosterImageRetirementIntentPersistenceMapper::toDomain);
}
@Override
public PosterImageRetirementIntent compareAndSet(
long expectedRevision, PosterImageRetirementIntent replacement) {
PosterImageRetirementIntentEntity current =
jpa.findById(replacement.id())
.orElseThrow(
() ->
new PosterImageUploadIntentConflictException(
"poster image retirement intent is absent"));
if (current.getRevision() != expectedRevision
|| replacement.revision() != expectedRevision + 1) {
throw new PosterImageUploadIntentConflictException(
"poster image retirement revision conflicts");
}
current.applyMutableState(PosterImageRetirementIntentPersistenceMapper.toEntity(replacement));
return PosterImageRetirementIntentPersistenceMapper.toDomain(jpa.saveAndFlush(current));
}
@Override
public Optional<PosterImageRetirementIntent> claimReady(
String claimantDigest, Instant now, Duration leaseDuration) {
return jpa
.findClaimable(
List.of(
PosterImageRetirementIntentState.PENDING,
PosterImageRetirementIntentState.IN_FLIGHT),
now,
PageRequest.of(0, 1))
.stream()
.findFirst()
.map(PosterImageRetirementIntentPersistenceMapper::toDomain)
.map(
current ->
compareAndSet(
current.revision(),
current.claim(current.revision(), claimantDigest, now, leaseDuration)));
}
}
@@ -1,41 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.persistence.repository;
import dev.caskeleton.sample.portfolio.adapter.outbound.persistence.entity.PosterImageUploadIntentEntity;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageUploadIntentState;
import jakarta.persistence.LockModeType;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface PosterImageUploadIntentJpaRepository
extends JpaRepository<PosterImageUploadIntentEntity, UUID> {
Optional<PosterImageUploadIntentEntity> findByOperationDestinationAndOperationEpochAndOperationId(
String destination, String epoch, String operationId);
Optional<PosterImageUploadIntentEntity> findByHmacKeyVersionAndIdempotencyScopeDigest(
String version, String digest);
Optional<PosterImageUploadIntentEntity> findByPublishedReference(String reference);
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query(
"""
select intent
from PosterImageUploadIntentEntity intent
where intent.state = :state
and (intent.retryAt is null or intent.retryAt <= :now)
and (intent.handoffExpiresAt is null or intent.handoffExpiresAt <= :now)
order by intent.updatedAt, intent.id
""")
List<PosterImageUploadIntentEntity> findClaimable(
@Param("state") PosterImageUploadIntentState state,
@Param("now") Instant now,
Pageable pageable);
}
@@ -1,119 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.persistence.repository;
import dev.caskeleton.application.objectstorage.identity.ObjectOperationKey;
import dev.caskeleton.sample.portfolio.adapter.outbound.persistence.entity.PosterImageUploadIntentEntity;
import dev.caskeleton.sample.portfolio.adapter.outbound.persistence.mapper.PosterImageUploadIntentPersistenceMapper;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageIdempotencyScopeDigest;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageUploadIntent;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageUploadIntentClaim;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageUploadIntentConflictException;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageUploadIntentState;
import dev.caskeleton.sample.portfolio.application.posterimage.PosterImageUploadIntentStorePort;
import dev.caskeleton.sample.portfolio.domain.poster.PosterImageReference;
import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.Optional;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Repository;
@Repository
public class PosterImageUploadIntentRepositoryAdapter implements PosterImageUploadIntentStorePort {
private final PosterImageUploadIntentJpaRepository jpa;
public PosterImageUploadIntentRepositoryAdapter(PosterImageUploadIntentJpaRepository jpa) {
this.jpa = jpa;
}
@Override
public PosterImageUploadIntent createOrRead(PosterImageUploadIntent proposed) {
Optional<PosterImageUploadIntent> existing =
findByAnyScopeDigest(java.util.List.of(proposed.idempotencyScope()));
if (existing.isEmpty()) {
existing = findByOperation(proposed.operationKey());
}
if (existing.isPresent()) {
PosterImageUploadIntent found = existing.orElseThrow();
if (!found.operationKey().equals(proposed.operationKey())
|| !found.requestFingerprint().equals(proposed.requestFingerprint())
|| !found.posterId().equals(proposed.posterId())) {
throw new PosterImageUploadIntentConflictException(
"poster image idempotency scope is already bound to different work");
}
return found;
}
if (proposed.state() != PosterImageUploadIntentState.RESERVED || proposed.revision() != 0) {
throw new IllegalArgumentException("only a fresh RESERVED intent can be created");
}
return PosterImageUploadIntentPersistenceMapper.toDomain(
jpa.saveAndFlush(PosterImageUploadIntentPersistenceMapper.toEntity(proposed)));
}
@Override
public Optional<PosterImageUploadIntent> findByOperation(ObjectOperationKey operation) {
return jpa.findByOperationDestinationAndOperationEpochAndOperationId(
operation.destination().value(),
operation.epoch().value(),
operation.operationId().value())
.map(PosterImageUploadIntentPersistenceMapper::toDomain);
}
@Override
public Optional<PosterImageUploadIntent> findByAnyScopeDigest(
Collection<PosterImageIdempotencyScopeDigest> retainedDigests) {
for (PosterImageIdempotencyScopeDigest digest : retainedDigests) {
Optional<PosterImageUploadIntentEntity> found =
jpa.findByHmacKeyVersionAndIdempotencyScopeDigest(
digest.keyVersion(), digest.base64UrlDigest());
if (found.isPresent()) {
return found.map(PosterImageUploadIntentPersistenceMapper::toDomain);
}
}
return Optional.empty();
}
@Override
public Optional<PosterImageUploadIntent> findReadyByPublishedReference(
PosterImageReference reference) {
return jpa.findByPublishedReference(reference.value())
.map(PosterImageUploadIntentPersistenceMapper::toDomain)
.filter(intent -> intent.state() == PosterImageUploadIntentState.READY);
}
@Override
public PosterImageUploadIntent compareAndSet(
long expectedRevision, PosterImageUploadIntent replacement) {
PosterImageUploadIntentEntity current =
jpa.findById(replacement.id())
.orElseThrow(
() ->
new PosterImageUploadIntentConflictException(
"poster image upload intent is absent"));
if (current.getRevision() != expectedRevision
|| replacement.revision() != expectedRevision + 1) {
throw new PosterImageUploadIntentConflictException(
"poster image upload intent revision conflicts");
}
current.applyMutableState(PosterImageUploadIntentPersistenceMapper.toEntity(replacement));
return PosterImageUploadIntentPersistenceMapper.toDomain(jpa.saveAndFlush(current));
}
@Override
public Optional<PosterImageUploadIntentClaim> claimPending(
String claimantDigest, Instant now, Duration leaseDuration) {
return jpa
.findClaimable(PosterImageUploadIntentState.PENDING, now, PageRequest.of(0, 1))
.stream()
.findFirst()
.map(PosterImageUploadIntentPersistenceMapper::toDomain)
.map(
current -> {
PosterImageUploadIntent replacement =
current.claim(current.revision(), claimantDigest, now, leaseDuration);
PosterImageUploadIntent saved = compareAndSet(current.revision(), replacement);
return new PosterImageUploadIntentClaim(
saved, saved.handoffFence(), saved.handoffExpiresAt());
});
}
}
@@ -1,16 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.persistence.repository;
import dev.caskeleton.sample.portfolio.adapter.outbound.persistence.entity.PosterEntity;
import dev.caskeleton.sample.portfolio.domain.poster.PosterStatus;
import java.util.UUID;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PosterJpaRepository extends JpaRepository<PosterEntity, UUID> {
// Flat equality filter (the only filter syntax the contract allows), kept in the data layer.
Page<PosterEntity> findByStatus(PosterStatus status, Pageable pageable);
boolean existsByTitleIgnoreCase(String title);
}
@@ -1,95 +0,0 @@
package dev.caskeleton.sample.portfolio.adapter.outbound.persistence.repository;
import dev.caskeleton.adapter.outbound.persistence.audit.AuditContextPort;
import dev.caskeleton.sample.portfolio.adapter.outbound.persistence.entity.PosterEntity;
import dev.caskeleton.sample.portfolio.adapter.outbound.persistence.mapper.PosterPersistenceMapper;
import dev.caskeleton.sample.portfolio.domain.poster.Poster;
import dev.caskeleton.sample.portfolio.domain.poster.PosterId;
import dev.caskeleton.sample.portfolio.domain.poster.PosterPage;
import dev.caskeleton.sample.portfolio.domain.poster.PosterRepository;
import dev.caskeleton.sample.portfolio.domain.poster.PosterSortField;
import dev.caskeleton.sample.portfolio.domain.poster.PosterStatus;
import java.time.Clock;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Repository;
@Repository
public class PosterRepositoryAdapter implements PosterRepository {
private final PosterJpaRepository jpa;
// Audit metadata (time/actor) is stamped here in the persistence adapter; the domain Poster
// carries no audit fields. Mirrors WorkLogRepositoryAdapter.
private final Clock clock;
private final AuditContextPort auditContext;
public PosterRepositoryAdapter(
PosterJpaRepository jpa, Clock clock, AuditContextPort auditContext) {
this.jpa = jpa;
this.clock = clock;
this.auditContext = auditContext;
}
@Override
public Poster save(Poster poster) {
PosterEntity entity = PosterPersistenceMapper.toEntity(poster);
Instant now = clock.instant();
String actor = auditContext.currentActor();
if (poster.version() == null) {
// INSERT: created_* and updated_* both take now/actor.
entity.initializeAudit(now, actor);
} else {
// UPDATE: carry the persisted created_* forward and bump only updated_*.
PosterEntity existing =
jpa.findById(entity.getId())
.orElseThrow(
() ->
new IllegalStateException(
"no persisted poster row to carry audit creation from: "
+ entity.getId()));
entity.carryCreation(existing.getCreatedAt(), existing.getCreatedBy());
entity.applyModification(now, actor);
}
return PosterPersistenceMapper.toDomain(jpa.save(entity));
}
@Override
public Optional<Poster> findById(PosterId id) {
return jpa.findById(PosterPersistenceMapper.toUuid(id)).map(PosterPersistenceMapper::toDomain);
}
@Override
public PosterPage findPage(
int page, int size, PosterSortField sortField, boolean ascending, PosterStatus status) {
int safeSize = size <= 0 ? 20 : size;
int safePage = Math.max(0, page);
Sort sort =
sortField == null
? Sort.unsorted()
: Sort.by(ascending ? Sort.Direction.ASC : Sort.Direction.DESC, sortField.property());
PageRequest pageRequest = PageRequest.of(safePage, safeSize, sort);
Page<PosterEntity> result =
status == null ? jpa.findAll(pageRequest) : jpa.findByStatus(status, pageRequest);
List<Poster> items = result.map(PosterPersistenceMapper::toDomain).getContent();
return new PosterPage(items, result.getTotalElements());
}
@Override
public boolean existsById(PosterId id) {
return jpa.existsById(PosterPersistenceMapper.toUuid(id));
}
@Override
public boolean existsByTitle(String title) {
return jpa.existsByTitleIgnoreCase(title);
}
@Override
public void deleteById(PosterId id) {
jpa.deleteById(PosterPersistenceMapper.toUuid(id));
}
}

Some files were not shown because too many files have changed in this diff Show More