docs(keycloak-session-store): import the session-storage lab as a new project

The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-04 22:51:59 +09:00
co-authored by Claude Opus 5
parent 43bccd08a8
commit b2963105a8
5017 changed files with 372751 additions and 4943 deletions
@@ -0,0 +1,7 @@
command: git rev-parse HEAD && git status --short --branch
cwd: /shared/codebase/clean-architecture-backend-template
timestamp: 2026-08-29T00:04:07+00:00
sourceRevision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
--- stdout ---
a24ece9cf797f7ea647e33bf846b115208ed1ba5
## main...origin/main
@@ -0,0 +1,50 @@
command: python3 module inventory from src/config/architecture/modules.json
cwd: /shared/codebase/clean-architecture-backend-template
timestamp: 2026-08-29T00:04:07+00:00
sourceRevision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
--- stdout ---
registry_modules= 44
domain-core|:domain-core|prod=8|prod_loc=107|test=0|test_loc=0|deps=|runtime=app-bootstrap,sample-portfolio
shared-contract|:shared-contract|prod=56|prod_loc=2839|test=23|test_loc=2706|deps=|runtime=app-bootstrap,sample-portfolio
application-core|:application-core|prod=886|prod_loc=35798|test=136|test_loc=19751|deps=domain-core,shared-contract|runtime=app-bootstrap,sample-portfolio
adapter-outbound-support|:adapter:outbound:support|prod=5|prod_loc=107|test=1|test_loc=89|deps=domain-core,application-core,shared-contract|runtime=app-bootstrap
adapter-outbound-persistence-jpa|:adapter:outbound:persistence-jpa|prod=460|prod_loc=44594|test=141|test_loc=12581|deps=domain-core,application-core,shared-contract|runtime=app-bootstrap,sample-portfolio
adapter-outbound-persistence-mongo|:adapter:outbound:persistence-mongo|prod=353|prod_loc=23401|test=139|test_loc=15416|deps=|runtime=app-bootstrap
adapter-outbound-identifier|:adapter:outbound:identifier|prod=5|prod_loc=134|test=2|test_loc=151|deps=domain-core,application-core|runtime=app-bootstrap,sample-portfolio
adapter-outbound-fileserver|:adapter:outbound:fileserver|prod=79|prod_loc=12723|test=37|test_loc=12043|deps=application-core,shared-contract|runtime=app-bootstrap
adapter-outbound-objectstorage|:adapter:outbound:objectstorage|prod=154|prod_loc=14966|test=49|test_loc=6778|deps=application-core,shared-contract|runtime=sample-portfolio
adapter-outbound-cache-redis|:adapter:outbound:cache-redis|prod=316|prod_loc=33690|test=70|test_loc=15245|deps=domain-core,application-core,shared-contract,adapter-outbound-support|runtime=app-bootstrap
adapter-outbound-httpclient|:adapter:outbound:httpclient|prod=270|prod_loc=15887|test=97|test_loc=8803|deps=domain-core,application-core,shared-contract,adapter-outbound-support|runtime=app-bootstrap
adapter-outbound-messaging|:adapter:outbound:messaging|prod=35|prod_loc=4350|test=19|test_loc=3670|deps=domain-core,application-core,shared-contract,adapter-outbound-support|runtime=app-bootstrap
adapter-outbound-notification|:adapter:outbound:notification|prod=172|prod_loc=14749|test=61|test_loc=10037|deps=domain-core,application-core,shared-contract,adapter-outbound-support|runtime=app-bootstrap
adapter-inbound-web|:adapter:inbound:web|prod=428|prod_loc=29488|test=202|test_loc=23500|deps=domain-core,application-core,shared-contract|runtime=app-bootstrap,sample-portfolio
adapter-inbound-grpc|:adapter:inbound:grpc|prod=9|prod_loc=650|test=6|test_loc=782|deps=domain-core,application-core,shared-contract|runtime=
adapter-inbound-graphql|:adapter:inbound:graphql|prod=409|prod_loc=26477|test=117|test_loc=14745|deps=domain-core,application-core,shared-contract|runtime=app-bootstrap
adapter-inbound-websocket|:adapter:inbound:websocket|prod=174|prod_loc=13426|test=74|test_loc=10336|deps=domain-core,application-core,shared-contract|runtime=
app-bootstrap|:app-bootstrap|prod=160|prod_loc=15096|test=282|test_loc=32724|deps=domain-core,application-core,adapter-outbound-persistence-jpa,adapter-outbound-support,adapter-outbound-messaging,adapter-outbound-cache-redis,adapter-outbound-notification,adapter-outbound-fileserver,adapter-outbound-httpclient,adapter-outbound-identifier,adapter-inbound-web,shared-contract,adapter-outbound-persistence-mongo,adapter-inbound-graphql,messaging-spring-boot-starter|runtime=app-bootstrap
sample-portfolio|:sample-portfolio|prod=210|prod_loc=10429|test=63|test_loc=7020|deps=domain-core,application-core,adapter-outbound-persistence-jpa,adapter-outbound-identifier,adapter-outbound-objectstorage,adapter-inbound-web,shared-contract|runtime=sample-portfolio
messaging-core-api|:messaging:messaging-core-api|prod=86|prod_loc=3952|test=8|test_loc=934|deps=|runtime=app-bootstrap
messaging-schema-api|:messaging:messaging-schema-api|prod=11|prod_loc=635|test=3|test_loc=263|deps=messaging-core-api|runtime=app-bootstrap
messaging-schema-json|:messaging:messaging-schema-json|prod=2|prod_loc=234|test=3|test_loc=373|deps=messaging-core-api,messaging-schema-api|runtime=app-bootstrap
messaging-schema-avro|:messaging:messaging-schema-avro|prod=3|prod_loc=356|test=4|test_loc=463|deps=messaging-core-api,messaging-schema-api|runtime=
messaging-schema-protobuf|:messaging:messaging-schema-protobuf|prod=3|prod_loc=209|test=2|test_loc=278|deps=messaging-core-api,messaging-schema-api|runtime=
messaging-cloudevents|:messaging:messaging-cloudevents|prod=4|prod_loc=243|test=1|test_loc=162|deps=messaging-core-api,messaging-schema-api|runtime=app-bootstrap
messaging-policy|:messaging:messaging-policy|prod=27|prod_loc=1744|test=4|test_loc=1236|deps=messaging-core-api,messaging-schema-api|runtime=app-bootstrap
messaging-transport-spi|:messaging:messaging-transport-spi|prod=14|prod_loc=783|test=4|test_loc=586|deps=messaging-core-api,messaging-schema-api,messaging-policy|runtime=app-bootstrap
messaging-runtime-core|:messaging:messaging-runtime-core|prod=7|prod_loc=804|test=4|test_loc=866|deps=messaging-core-api,messaging-schema-api,messaging-policy,messaging-transport-spi,messaging-security,messaging-observability|runtime=app-bootstrap
messaging-observability|:messaging:messaging-observability|prod=10|prod_loc=847|test=6|test_loc=827|deps=messaging-core-api|runtime=app-bootstrap
messaging-security|:messaging:messaging-security|prod=13|prod_loc=959|test=3|test_loc=475|deps=messaging-core-api|runtime=app-bootstrap
messaging-kafka|:messaging:messaging-kafka|prod=36|prod_loc=3711|test=24|test_loc=4087|deps=messaging-core-api,messaging-schema-api,messaging-policy,messaging-transport-spi,messaging-observability,messaging-security,messaging-admin-api|runtime=app-bootstrap
messaging-kafka-share-experimental|:messaging:messaging-kafka-share-experimental|prod=5|prod_loc=200|test=1|test_loc=112|deps=messaging-core-api,messaging-policy,messaging-transport-spi,messaging-kafka|runtime=
messaging-rabbit|:messaging:messaging-rabbit|prod=22|prod_loc=2624|test=10|test_loc=1727|deps=messaging-core-api,messaging-schema-api,messaging-policy,messaging-transport-spi,messaging-observability,messaging-security,messaging-admin-api|runtime=app-bootstrap
messaging-reliability-api|:messaging:messaging-reliability-api|prod=14|prod_loc=822|test=0|test_loc=0|deps=messaging-core-api|runtime=app-bootstrap
messaging-outbox-jdbc-postgresql|:messaging:messaging-outbox-jdbc-postgresql|prod=19|prod_loc=2525|test=8|test_loc=2140|deps=messaging-core-api,messaging-reliability-api,messaging-policy,messaging-observability,messaging-admin-api|runtime=app-bootstrap
messaging-inbox-jdbc-postgresql|:messaging:messaging-inbox-jdbc-postgresql|prod=8|prod_loc=576|test=4|test_loc=607|deps=messaging-core-api,messaging-reliability-api|runtime=app-bootstrap
messaging-claim-check|:messaging:messaging-claim-check|prod=7|prod_loc=424|test=3|test_loc=333|deps=messaging-core-api,messaging-reliability-api|runtime=app-bootstrap
messaging-admin-api|:messaging:messaging-admin-api|prod=26|prod_loc=1619|test=1|test_loc=147|deps=messaging-core-api,messaging-policy|runtime=app-bootstrap
messaging-admin-runtime|:messaging:messaging-admin-runtime|prod=13|prod_loc=1263|test=6|test_loc=1051|deps=messaging-core-api,messaging-policy,messaging-admin-api,messaging-transport-spi,messaging-security,messaging-observability|runtime=app-bootstrap
messaging-pulsar-experimental|:messaging:messaging-pulsar-experimental|prod=9|prod_loc=676|test=2|test_loc=414|deps=messaging-core-api,messaging-schema-api,messaging-policy,messaging-transport-spi,messaging-observability,messaging-security,messaging-admin-api|runtime=
messaging-nats-experimental|:messaging:messaging-nats-experimental|prod=8|prod_loc=768|test=2|test_loc=460|deps=messaging-core-api,messaging-schema-api,messaging-policy,messaging-transport-spi,messaging-observability,messaging-security,messaging-admin-api|runtime=
messaging-spring-cloud-stream-bridge|:messaging:messaging-spring-cloud-stream-bridge|prod=7|prod_loc=516|test=2|test_loc=295|deps=messaging-core-api,messaging-policy,messaging-transport-spi|runtime=
messaging-spring-boot-starter|:messaging:messaging-spring-boot-starter|prod=29|prod_loc=3590|test=12|test_loc=2384|deps=messaging-admin-api,messaging-admin-runtime,messaging-claim-check,messaging-cloudevents,messaging-core-api,messaging-inbox-jdbc-postgresql,messaging-kafka,messaging-observability,messaging-outbox-jdbc-postgresql,messaging-policy,messaging-rabbit,messaging-reliability-api,messaging-runtime-core,messaging-schema-api,messaging-schema-json,messaging-security,messaging-transport-spi|runtime=app-bootstrap
messaging-testkit|:messaging:messaging-testkit|prod=15|prod_loc=1246|test=6|test_loc=828|deps=messaging-core-api,messaging-schema-api,messaging-policy,messaging-transport-spi|runtime=
@@ -0,0 +1,29 @@
command: ./gradlew :domain-core:test --console=plain --no-daemon
cwd: /shared/codebase/clean-architecture-backend-template/src
timestamp: 2026-08-29T00:04:07+00:00
sourceRevision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
--- output ---
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
Daemon will be stopped at the end of the build
> Task :build-logic:extractPluginRequests UP-TO-DATE
> Task :build-logic:generatePluginAdapters UP-TO-DATE
> Task :build-logic:compileJava UP-TO-DATE
> Task :build-logic:compileGroovy UP-TO-DATE
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
> Task :build-logic:pluginDescriptors UP-TO-DATE
> Task :build-logic:processResources UP-TO-DATE
> Task :build-logic:classes UP-TO-DATE
> Task :build-logic:jar UP-TO-DATE
> Task :domain-core:compileJava UP-TO-DATE
> Task :domain-core:processResources NO-SOURCE
> Task :domain-core:classes UP-TO-DATE
> Task :domain-core:compileTestJava NO-SOURCE
> Task :domain-core:processTestResources NO-SOURCE
> Task :domain-core:testClasses UP-TO-DATE
> Task :domain-core:test NO-SOURCE
BUILD SUCCESSFUL in 7s
9 actionable tasks: 9 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
exitCode: 0
@@ -0,0 +1,175 @@
command: ./gradlew :app-bootstrap:test --tests dev.caskeleton.bootstrap.architecture.CleanArchitectureTest --console=plain --no-daemon
cwd: /shared/codebase/clean-architecture-backend-template/src
timestamp: 2026-08-29T00:04:25+00:00
sourceRevision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
--- output ---
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
Daemon will be stopped at the end of the build
> Task :build-logic:extractPluginRequests UP-TO-DATE
> Task :build-logic:generatePluginAdapters UP-TO-DATE
> Task :build-logic:compileJava UP-TO-DATE
> Task :build-logic:compileGroovy UP-TO-DATE
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
> Task :build-logic:pluginDescriptors UP-TO-DATE
> Task :build-logic:processResources UP-TO-DATE
> Task :build-logic:classes UP-TO-DATE
> Task :build-logic:jar UP-TO-DATE
> Task :shared-contract:compileJava UP-TO-DATE
> Task :shared-contract:processResources UP-TO-DATE
> Task :shared-contract:classes UP-TO-DATE
> Task :shared-contract:jar UP-TO-DATE
> Task :application-core:compileJava UP-TO-DATE
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes UP-TO-DATE
> Task :application-core:jar UP-TO-DATE
> Task :domain-core:compileJava UP-TO-DATE
> Task :domain-core:processResources NO-SOURCE
> Task :domain-core:classes UP-TO-DATE
> Task :domain-core:jar UP-TO-DATE
> Task :messaging:messaging-core-api:compileJava UP-TO-DATE
> Task :messaging:messaging-observability:compileJava UP-TO-DATE
> Task :messaging:messaging-schema-api:compileJava UP-TO-DATE
> Task :messaging:messaging-policy:compileJava UP-TO-DATE
> Task :messaging:messaging-reliability-api:compileJava UP-TO-DATE
> Task :messaging:messaging-security:compileJava UP-TO-DATE
> Task :messaging:messaging-admin-api:compileJava UP-TO-DATE
> Task :messaging:messaging-transport-spi:compileJava UP-TO-DATE
> Task :messaging:messaging-admin-runtime:compileJava UP-TO-DATE
> Task :messaging:messaging-claim-check:compileJava UP-TO-DATE
> Task :messaging:messaging-cloudevents:compileJava UP-TO-DATE
> Task :messaging:messaging-inbox-jdbc-postgresql:compileJava UP-TO-DATE
> Task :messaging:messaging-kafka:compileJava UP-TO-DATE
> Task :messaging:messaging-outbox-jdbc-postgresql:compileJava UP-TO-DATE
> Task :messaging:messaging-rabbit:compileJava UP-TO-DATE
> Task :messaging:messaging-runtime-core:compileJava UP-TO-DATE
> Task :messaging:messaging-schema-json:compileJava UP-TO-DATE
> Task :messaging:messaging-spring-boot-starter:compileJava UP-TO-DATE
> Task :adapter:inbound:graphql:compileJava UP-TO-DATE
> Task :adapter:inbound:graphql:processResources UP-TO-DATE
> Task :adapter:inbound:graphql:classes UP-TO-DATE
> Task :adapter:inbound:graphql:jar UP-TO-DATE
> Task :adapter:inbound:web:compileJava UP-TO-DATE
> Task :adapter:inbound:web:processResources UP-TO-DATE
> Task :adapter:inbound:web:classes UP-TO-DATE
> Task :adapter:inbound:web:jar UP-TO-DATE
> Task :adapter:outbound:support:compileJava UP-TO-DATE
> Task :adapter:outbound:support:processResources NO-SOURCE
> Task :adapter:outbound:support:classes UP-TO-DATE
> Task :adapter:outbound:support:jar UP-TO-DATE
> Task :adapter:outbound:cache-redis:compileJava UP-TO-DATE
> Task :adapter:outbound:cache-redis:processResources UP-TO-DATE
> Task :adapter:outbound:cache-redis:classes UP-TO-DATE
> Task :adapter:outbound:cache-redis:jar UP-TO-DATE
> Task :adapter:outbound:fileserver:compileJava UP-TO-DATE
> Task :adapter:outbound:fileserver:processResources NO-SOURCE
> Task :adapter:outbound:fileserver:classes UP-TO-DATE
> Task :adapter:outbound:fileserver:jar UP-TO-DATE
> Task :adapter:outbound:httpclient:compileJava UP-TO-DATE
> Task :adapter:outbound:httpclient:processResources NO-SOURCE
> Task :adapter:outbound:httpclient:classes UP-TO-DATE
> Task :adapter:outbound:httpclient:jar UP-TO-DATE
> Task :adapter:outbound:identifier:compileJava UP-TO-DATE
> Task :adapter:outbound:identifier:compileGroovy NO-SOURCE
> Task :adapter:outbound:identifier:processResources NO-SOURCE
> Task :adapter:outbound:identifier:classes UP-TO-DATE
> Task :adapter:outbound:identifier:jar UP-TO-DATE
> Task :adapter:outbound:messaging:compileJava UP-TO-DATE
> Task :adapter:outbound:messaging:processResources UP-TO-DATE
> Task :adapter:outbound:messaging:classes UP-TO-DATE
> Task :adapter:outbound:messaging:jar UP-TO-DATE
> Task :adapter:outbound:notification:compileJava UP-TO-DATE
> Task :adapter:outbound:notification:processResources NO-SOURCE
> Task :adapter:outbound:notification:classes UP-TO-DATE
> Task :adapter:outbound:notification:jar UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:jar UP-TO-DATE
> Task :adapter:outbound:persistence-mongo:compileJava UP-TO-DATE
> Task :adapter:outbound:persistence-mongo:processResources UP-TO-DATE
> Task :adapter:outbound:persistence-mongo:classes UP-TO-DATE
> Task :adapter:outbound:persistence-mongo:jar UP-TO-DATE
> Task :app-bootstrap:compileJava
> Task :app-bootstrap:processResources
> Task :app-bootstrap:classes
> Task :sample-portfolio:compileJava
> Task :sample-portfolio:processResources
> Task :sample-portfolio:classes
> Task :sample-portfolio:jar
> Task :adapter:inbound:web:compileTestkitJava
> Task :adapter:inbound:web:processTestkitResources
> Task :adapter:inbound:web:testkitClasses
> Task :adapter:inbound:web:testkitJar
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses
> Task :adapter:outbound:persistence-jpa:testkitJar
> Task :app-bootstrap:compileTestJava
> Task :app-bootstrap:runtimeClasspathManifest
> Task :app-bootstrap:processTestResources
> Task :app-bootstrap:testClasses
> Task :messaging:messaging-admin-api:processResources NO-SOURCE
> Task :messaging:messaging-admin-api:classes UP-TO-DATE
> Task :messaging:messaging-admin-api:jar
> Task :messaging:messaging-admin-runtime:processResources NO-SOURCE
> Task :messaging:messaging-admin-runtime:classes UP-TO-DATE
> Task :messaging:messaging-admin-runtime:jar
> Task :messaging:messaging-claim-check:processResources NO-SOURCE
> Task :messaging:messaging-claim-check:classes UP-TO-DATE
> Task :messaging:messaging-claim-check:jar
> Task :messaging:messaging-cloudevents:processResources NO-SOURCE
> Task :messaging:messaging-cloudevents:classes UP-TO-DATE
> Task :messaging:messaging-cloudevents:jar
> Task :messaging:messaging-core-api:processResources NO-SOURCE
> Task :messaging:messaging-core-api:classes UP-TO-DATE
> Task :messaging:messaging-core-api:jar
> Task :messaging:messaging-inbox-jdbc-postgresql:processResources
> Task :messaging:messaging-inbox-jdbc-postgresql:classes
> Task :messaging:messaging-inbox-jdbc-postgresql:jar
> Task :messaging:messaging-kafka:processResources NO-SOURCE
> Task :messaging:messaging-kafka:classes UP-TO-DATE
> Task :messaging:messaging-kafka:jar
> Task :messaging:messaging-observability:processResources NO-SOURCE
> Task :messaging:messaging-observability:classes UP-TO-DATE
> Task :messaging:messaging-observability:jar
> Task :messaging:messaging-outbox-jdbc-postgresql:processResources
> Task :messaging:messaging-outbox-jdbc-postgresql:classes
> Task :messaging:messaging-outbox-jdbc-postgresql:jar
> Task :messaging:messaging-policy:processResources NO-SOURCE
> Task :messaging:messaging-policy:classes UP-TO-DATE
> Task :messaging:messaging-policy:jar
> Task :messaging:messaging-rabbit:processResources NO-SOURCE
> Task :messaging:messaging-rabbit:classes UP-TO-DATE
> Task :messaging:messaging-rabbit:jar
> Task :messaging:messaging-reliability-api:processResources NO-SOURCE
> Task :messaging:messaging-reliability-api:classes UP-TO-DATE
> Task :messaging:messaging-reliability-api:jar
> Task :messaging:messaging-runtime-core:processResources NO-SOURCE
> Task :messaging:messaging-runtime-core:classes UP-TO-DATE
> Task :messaging:messaging-runtime-core:jar
> Task :messaging:messaging-schema-api:processResources NO-SOURCE
> Task :messaging:messaging-schema-api:classes UP-TO-DATE
> Task :messaging:messaging-schema-api:jar
> Task :messaging:messaging-schema-json:processResources NO-SOURCE
> Task :messaging:messaging-schema-json:classes UP-TO-DATE
> Task :messaging:messaging-schema-json:jar
> Task :messaging:messaging-security:processResources NO-SOURCE
> Task :messaging:messaging-security:classes UP-TO-DATE
> Task :messaging:messaging-security:jar
> Task :messaging:messaging-spring-boot-starter:processResources
> Task :messaging:messaging-spring-boot-starter:classes
> Task :messaging:messaging-spring-boot-starter:jar
> Task :messaging:messaging-transport-spi:processResources NO-SOURCE
> Task :messaging:messaging-transport-spi:classes UP-TO-DATE
> Task :messaging:messaging-transport-spi:jar
> Task :adapter:outbound:objectstorage:compileJava
> Task :adapter:outbound:objectstorage:processResources NO-SOURCE
> Task :adapter:outbound:objectstorage:classes
> Task :adapter:outbound:objectstorage:jar
> Task :app-bootstrap:test
BUILD SUCCESSFUL in 1m 9s
98 actionable tasks: 37 executed, 61 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
exitCode: 0
@@ -0,0 +1,8 @@
$ ./gradlew :shared-contract:test --console=plain
> Task :shared-contract:compileTestJava
> Task :shared-contract:processTestResources NO-SOURCE
> Task :shared-contract:testClasses
> Task :shared-contract:test
BUILD SUCCESSFUL in 12s
12 actionable tasks: 2 executed, 10 up-to-date
@@ -0,0 +1,8 @@
$ ./gradlew :shared-contract:edgeRateLimitContractTest --console=plain
> Task :shared-contract:compileEdgeRateLimitContractTestJava
> Task :shared-contract:processEdgeRateLimitContractTestResources NO-SOURCE
> Task :shared-contract:edgeRateLimitContractTestClasses
> Task :shared-contract:edgeRateLimitContractTest
BUILD SUCCESSFUL in 2s
12 actionable tasks: 2 executed, 10 up-to-date
@@ -0,0 +1,24 @@
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :shared-contract:compileJava
> Task :shared-contract:processResources
> Task :shared-contract:classes
> Task :shared-contract:jar
> Task :application-core:compileJava
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes
> Task :application-core:compileTestJava
> Task :application-core:processTestResources NO-SOURCE
> Task :application-core:testClasses
> Task :application-core:test
BUILD SUCCESSFUL in 25s
14 actionable tasks: 14 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
@@ -0,0 +1,29 @@
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :prepareMessagingContractEvidence
> Task :shared-contract:compileJava
> Task :shared-contract:processResources
> Task :shared-contract:classes
> Task :shared-contract:jar
> Task :application-core:compileJava
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes
> Task :application-core:compileTestJava
> Task :application-core:processTestResources NO-SOURCE
> Task :application-core:testClasses
> Task :application-core:messagingApplicationContractQualificationTestRequiredClasses
> Task :application-core:messagingApplicationContractQualificationTest
> Task :application-core:messagingApplicationContractQualificationTestEvidence
messagingApplicationContractQualificationTest: 15 tests, 0 skipped
BUILD SUCCESSFUL in 18s
17 actionable tasks: 17 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
@@ -0,0 +1,11 @@
Command: ./gradlew :application-core:messagingApplicationContractQualificationTest --rerun-tasks
Working directory: /shared/codebase/clean-architecture-backend-template/src
Executed at: 2026-08-29T06:39:57Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Exit code: 1
Stdout/stderr excerpt:
> Task :prepareMessagingContractEvidence FAILED
Execution failed for task ':prepareMessagingContractEvidence'.
> -PmessagingSourceDigest=sha256:<64-lowercase-hex> is required for Messaging evidence.
Observation boundary: this capture records the Gradle evidence-preparation failure before the messaging qualification test lane could execute. It does not establish whether the qualification tests themselves pass or fail. The later exact-digest execution is captured separately in 010-application-core-messaging-qualification.txt.
@@ -0,0 +1,166 @@
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :shared-contract:compileJava
> Task :shared-contract:processResources
> Task :shared-contract:classes
> Task :shared-contract:jar
> Task :application-core:compileJava
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes
> Task :application-core:jar
> Task :domain-core:compileJava
> Task :domain-core:processResources NO-SOURCE
> Task :domain-core:classes
> Task :domain-core:jar
> Task :messaging:messaging-core-api:compileJava
> Task :messaging:messaging-observability:compileJava
> Task :messaging:messaging-schema-api:compileJava
> Task :messaging:messaging-policy:compileJava
> Task :messaging:messaging-reliability-api:compileJava
> Task :messaging:messaging-security:compileJava
> Task :messaging:messaging-admin-api:compileJava
> Task :messaging:messaging-transport-spi:compileJava
> Task :messaging:messaging-admin-runtime:compileJava
> Task :messaging:messaging-claim-check:compileJava
> Task :messaging:messaging-cloudevents:compileJava
> Task :messaging:messaging-inbox-jdbc-postgresql:compileJava
> Task :messaging:messaging-kafka:compileJava
> Task :messaging:messaging-outbox-jdbc-postgresql:compileJava
> Task :messaging:messaging-rabbit:compileJava
> Task :messaging:messaging-runtime-core:compileJava
> Task :messaging:messaging-schema-json:compileJava
> Task :messaging:messaging-spring-boot-starter:compileJava
> Task :adapter:inbound:graphql:compileJava
> Task :adapter:inbound:graphql:processResources
> Task :adapter:inbound:graphql:classes
> Task :adapter:inbound:graphql:jar
> Task :adapter:inbound:web:compileJava
> Task :adapter:inbound:web:processResources
> Task :adapter:inbound:web:classes
> Task :adapter:inbound:web:jar
> Task :adapter:outbound:support:compileJava
> Task :adapter:outbound:support:processResources NO-SOURCE
> Task :adapter:outbound:support:classes
> Task :adapter:outbound:support:jar
> Task :adapter:outbound:cache-redis:compileJava
> Task :adapter:outbound:cache-redis:processResources
> Task :adapter:outbound:cache-redis:classes
> Task :adapter:outbound:cache-redis:jar
> Task :adapter:outbound:fileserver:compileJava
> Task :adapter:outbound:fileserver:processResources NO-SOURCE
> Task :adapter:outbound:fileserver:classes
> Task :adapter:outbound:fileserver:jar
> Task :adapter:outbound:httpclient:compileJava
> Task :adapter:outbound:httpclient:processResources NO-SOURCE
> Task :adapter:outbound:httpclient:classes
> Task :adapter:outbound:httpclient:jar
> Task :adapter:outbound:identifier:compileJava
> Task :adapter:outbound:identifier:compileGroovy NO-SOURCE
> Task :adapter:outbound:identifier:processResources NO-SOURCE
> Task :adapter:outbound:identifier:classes
> Task :adapter:outbound:identifier:jar
> Task :adapter:outbound:messaging:compileJava
> Task :adapter:outbound:messaging:processResources
> Task :adapter:outbound:messaging:classes
> Task :adapter:outbound:messaging:jar
> Task :adapter:outbound:notification:compileJava
> Task :adapter:outbound:notification:processResources NO-SOURCE
> Task :adapter:outbound:notification:classes
> Task :adapter:outbound:notification:jar
> Task :adapter:outbound:persistence-jpa:compileJava
> Task :adapter:outbound:persistence-jpa:processResources
> Task :adapter:outbound:persistence-jpa:classes
> Task :adapter:outbound:persistence-jpa:jar
> Task :adapter:outbound:persistence-mongo:compileJava
> Task :adapter:outbound:persistence-mongo:processResources
> Task :adapter:outbound:persistence-mongo:classes
> Task :adapter:outbound:persistence-mongo:jar
> Task :app-bootstrap:compileJava
> Task :app-bootstrap:processResources
> Task :app-bootstrap:classes
> Task :sample-portfolio:compileJava
> Task :sample-portfolio:processResources
> Task :sample-portfolio:classes
> Task :sample-portfolio:jar
> Task :adapter:inbound:web:compileTestkitJava
> Task :adapter:inbound:web:processTestkitResources
> Task :adapter:inbound:web:testkitClasses
> Task :adapter:inbound:web:testkitJar
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses
> Task :adapter:outbound:persistence-jpa:testkitJar
> Task :app-bootstrap:compileTestJava
> Task :app-bootstrap:runtimeClasspathManifest
> Task :app-bootstrap:processTestResources
> Task :app-bootstrap:testClasses
> Task :messaging:messaging-admin-api:processResources NO-SOURCE
> Task :messaging:messaging-admin-api:classes
> Task :messaging:messaging-admin-api:jar
> Task :messaging:messaging-admin-runtime:processResources NO-SOURCE
> Task :messaging:messaging-admin-runtime:classes
> Task :messaging:messaging-admin-runtime:jar
> Task :messaging:messaging-claim-check:processResources NO-SOURCE
> Task :messaging:messaging-claim-check:classes
> Task :messaging:messaging-claim-check:jar
> Task :messaging:messaging-cloudevents:processResources NO-SOURCE
> Task :messaging:messaging-cloudevents:classes
> Task :messaging:messaging-cloudevents:jar
> Task :messaging:messaging-core-api:processResources NO-SOURCE
> Task :messaging:messaging-core-api:classes
> Task :messaging:messaging-core-api:jar
> Task :messaging:messaging-inbox-jdbc-postgresql:processResources
> Task :messaging:messaging-inbox-jdbc-postgresql:classes
> Task :messaging:messaging-inbox-jdbc-postgresql:jar
> Task :messaging:messaging-kafka:processResources NO-SOURCE
> Task :messaging:messaging-kafka:classes
> Task :messaging:messaging-kafka:jar
> Task :messaging:messaging-observability:processResources NO-SOURCE
> Task :messaging:messaging-observability:classes
> Task :messaging:messaging-observability:jar
> Task :messaging:messaging-outbox-jdbc-postgresql:processResources
> Task :messaging:messaging-outbox-jdbc-postgresql:classes
> Task :messaging:messaging-outbox-jdbc-postgresql:jar
> Task :messaging:messaging-policy:processResources NO-SOURCE
> Task :messaging:messaging-policy:classes
> Task :messaging:messaging-policy:jar
> Task :messaging:messaging-rabbit:processResources NO-SOURCE
> Task :messaging:messaging-rabbit:classes
> Task :messaging:messaging-rabbit:jar
> Task :messaging:messaging-reliability-api:processResources NO-SOURCE
> Task :messaging:messaging-reliability-api:classes
> Task :messaging:messaging-reliability-api:jar
> Task :messaging:messaging-runtime-core:processResources NO-SOURCE
> Task :messaging:messaging-runtime-core:classes
> Task :messaging:messaging-runtime-core:jar
> Task :messaging:messaging-schema-api:processResources NO-SOURCE
> Task :messaging:messaging-schema-api:classes
> Task :messaging:messaging-schema-api:jar
> Task :messaging:messaging-schema-json:processResources NO-SOURCE
> Task :messaging:messaging-schema-json:classes
> Task :messaging:messaging-schema-json:jar
> Task :messaging:messaging-security:processResources NO-SOURCE
> Task :messaging:messaging-security:classes
> Task :messaging:messaging-security:jar
> Task :messaging:messaging-spring-boot-starter:processResources
> Task :messaging:messaging-spring-boot-starter:classes
> Task :messaging:messaging-spring-boot-starter:jar
> Task :messaging:messaging-transport-spi:processResources NO-SOURCE
> Task :messaging:messaging-transport-spi:classes
> Task :messaging:messaging-transport-spi:jar
> Task :adapter:outbound:objectstorage:compileJava
> Task :adapter:outbound:objectstorage:processResources NO-SOURCE
> Task :adapter:outbound:objectstorage:classes
> Task :adapter:outbound:objectstorage:jar
> Task :app-bootstrap:test
BUILD SUCCESSFUL in 1m 45s
98 actionable tasks: 98 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
@@ -0,0 +1,14 @@
> Task :build-logic:extractPluginRequests UP-TO-DATE
> Task :build-logic:generatePluginAdapters UP-TO-DATE
> Task :build-logic:compileJava UP-TO-DATE
> Task :build-logic:compileGroovy UP-TO-DATE
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
> Task :build-logic:pluginDescriptors UP-TO-DATE
> Task :build-logic:processResources UP-TO-DATE
> Task :build-logic:classes UP-TO-DATE
> Task :build-logic:jar UP-TO-DATE
> Task :verifyCleanArchitectureDependencies
BUILD SUCCESSFUL in 1s
9 actionable tasks: 1 executed, 8 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Working directory: /shared/codebase/clean-architecture-backend-template
# Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
# Captured execution time: 2026-08-29T06:43:52Z
python3 - <<'PY' > /shared/document-detail/clean-architecture-backend-template/evidence/raw/013-application-core-reachability.txt
from pathlib import Path
root=Path('src')
app=Path('src/application-core/src/main/java')
packages=['cache','fileexport','filepublication','fileserver','idempotency','inbox','lease','lock','messaging','notification','objectstorage','observability','operation','outbound','outbox','realtime','security','storage','transaction','usecase']
files=[p for p in root.rglob('src/main/java/**/*.java') if app not in p.parents]
for pkg in packages:
needle=f'dev.caskeleton.application.{pkg}'
refs=[]
for f in files:
text=f.read_text(errors='ignore')
if needle in text:
refs.append(str(f))
print(f'{pkg}\texternalProductionFiles={len(refs)}')
for r in refs:
print(f' {r}')
PY
@@ -0,0 +1,582 @@
cache externalProductionFiles=2
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/redis/RedisCapabilityConfig.java
src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/cache/RedisCacheRegionAdapter.java
fileexport externalProductionFiles=2
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FilesystemCsvExportAdapter.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileExportConfig.java
filepublication externalProductionFiles=16
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/PrivateFileManifest.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/R2PublishedReferenceCodec.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalPersistentRecoveryVerifier.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FilePublishRequestFingerprint.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FilePublicationProvider.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileserverControlRecordCodec.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileserverBindingCompiler.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileserverR2Config.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalFilePublicationAdapter.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalFilePublicationPolicy.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/StreamingCsvEncoder.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileExportConfig.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FilePublicationCanonicalDigests.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/LocalPersistentPublicationProvider.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/CompiledFileDestination.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/RoutingFilePublicationAdapter.java
fileserver externalProductionFiles=91
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverStartupConfiguration.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverStartupValidator.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverStartupCheck.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverStorageConfiguration.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/MicrometerFileserverMetrics.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverServiceConfiguration.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverCleanupWorker.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverSecurityConfiguration.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverCleanupConfiguration.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverPlatformAutoConfiguration.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/http/MvcConditionalRequestFactory.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/http/MvcDownloadResponseWriter.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/reactive/FileserverRouterFactory.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/reactive/PartEventUploadReader.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/reactive/ReactiveDownloadResponseWriter.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/reactive/ReactiveUploadApplicationService.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/reactive/FileserverReactiveConfiguration.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/reactive/FileUploadHandler.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/reactive/FileDownloadHandler.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/config/BlockingTransferExecutor.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/config/FileserverWebProperties.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/draft12/Draft12UploadController.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/security/FileserverRequestContextFactory.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/problem/FileserverProblemFactory.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/problem/FileserverProblemTitles.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/problem/FileserverProblemHeaders.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/problem/FileserverExceptionHandler.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/tus/TusChecksumVerifier.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/tus/TusController.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/tus/TusRequestParser.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/lifecycle/FileLifecycleController.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/nginx/NginxDownloadStrategy.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/nginx/DefaultNginxInternalUriMapper.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/nginx/NginxInternalUriMapper.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/admin/FileserverAdminController.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/controller/FileDownloadController.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/controller/FileUploadController.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/mapper/RawUploadRequestMapper.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/dto/BatchUploadItemResult.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/dto/UploadedFileResponse.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/fileserver/dto/BatchUploadItemProblem.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/FilesystemFailureClassifier.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalUploadStorageGateway.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/MetadataPointerContentPublisher.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalStorageFailures.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalUploadContentGateway.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/PublishResult.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalUploadHandle.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalZeroCopyDownloadGateway.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalAppendEngine.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalCleanupContentGateway.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/AtomicMoveContentPublisher.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/PhysicalPathResolver.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalOrphanScanAdapter.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/SafeFileChannelFactory.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/DefaultPhysicalPathResolver.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalStorageUsageProbe.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalStorageProbeResult.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalDownloadContentGateway.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalCopyContentGateway.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalBlockingContentStore.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalReconciliationContentProbe.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalStorageProperties.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalStorageHealthAdapter.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/ContentPublisher.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/ContentPublishVerification.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/security/UnenforcedFileAccessPolicy.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/security/RoleBasedFileAccessPolicy.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/FilenamePolicyVerifier.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/LengthVerifier.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/MediaTypeVerifier.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/VerificationCoordinator.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/Sha256Verifier.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/VerificationVerdictPrecedence.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/ScriptableContentPolicy.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/VerificationContentReader.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/LocalVerificationContentReader.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/VerificationPolicyCombiner.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/audit/StructuredFileserverAuditAdapter.java
src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/audit/StructuredAdminAuditAdapter.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaRecoveryQueue.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaContentReferenceLedger.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaStagingUploadLocator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaQuotaReclaimGateway.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaQuotaCommitGateway.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaFileQuotaService.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaFileMetadataStore.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaCleanupQueue.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaUploadSessionStore.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/FileEntityMapper.java
src/adapter/outbound/identifier/src/main/java/dev/caskeleton/adapter/outbound/identifier/RandomUploadIdentifierFactory.java
idempotency externalProductionFiles=30
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/redis/RedisCapabilityConfig.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/idempotency/PostgreSqlIdempotencyProviderConfig.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/idempotency/IdempotencyProviderSelectionConfig.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/idempotency/IdempotencyConfig.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/objectstorage/PosterImageObjectStorageConfig.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/idempotency/SampleIdempotencyConfig.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/PublishPosterImageUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImagePublicationFingerprint.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImagePublicationReservationCodec.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImageSanitizedIdempotencyContextFactory.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImageUploadIntent.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/outbound/persistence/mapper/PosterImageUploadIntentPersistenceMapper.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/error/GlobalExceptionHandler.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/idempotency/JsonIdempotentResponseCodec.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/idempotency/SemanticRequestFingerprintFactory.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/idempotency/IdempotencyKeySupport.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/idempotency/WebIdempotencyGate.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/idempotency/IdempotencyResponsePlan.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/idempotency/IdempotencyAdmission.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/RoutingObjectMutationAdapter.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/StagedObjectPublicationKernel.java
src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/idempotency/RedisIdempotencyStoreAdapter.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/idempotency/IdempotencyStoreAdapter.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/operation/DurableOperationStoreAdapter.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/idempotency/mapper/IdempotencyRecordEntityMapper.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyClaimGateway.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyRecordRow.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyRowMapper.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyTransitionGateway.java
inbox externalProductionFiles=1
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/inbox/PostgreSqlSameStoreInboxAdapter.java
lease externalProductionFiles=2
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/redis/RedisCapabilityConfig.java
src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/lease/RedisDistributedLeaseAdapter.java
lock externalProductionFiles=4
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/lock/DistributedLockConfig.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/lock/MeteredDistributedLockPort.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/lock/DistributedLockPersistenceConfig.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/lock/LockRegistryDistributedLockAdapter.java
messaging externalProductionFiles=14
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/event/WorkLogReservedPayload.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/event/WorkLogReservedContractContribution.java
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/config/CompiledMessagingDescriptor.java
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/destination/DestinationBindingSettings.java
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/destination/CompiledPublicationBinding.java
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/destination/PartitionKeyV1.java
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/destination/DestinationBindingCompiler.java
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/envelope/EnvelopeHashV1.java
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/envelope/DeterministicEnvelopeWriter.java
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/envelope/JsonSchemaIntegrationEventEncoder.java
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/envelope/LocalJsonSchemaRegistry.java
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/contract/CompiledIntegrationEventContract.java
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/contract/ContractCatalogDigest.java
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/contract/ContractCatalogCompiler.java
notification externalProductionFiles=156
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformRegistriesConfig.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformSecretsConfig.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformProviderConfig.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformDispatchConfig.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformWorkerConfig.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformPolicyConfig.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformObservabilityConfig.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformCodecConfig.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/observation/NotificationObservationConvention.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/observation/MicrometerNotificationMetrics.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/notification/platform/submission/NotificationSubmissionController.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/notification/platform/submission/PublishTemplateHttpResponse.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/notification/platform/submission/SubmitNotificationHttpResponse.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/notification/platform/submission/NotificationVariableCoercion.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/notification/platform/submission/NotificationTemplateController.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/notification/platform/callback/CallbackRequestFactory.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/notification/platform/callback/NotificationCallbackMvcController.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/notification/platform/callback/reactive/NotificationCallbackWebFluxHandler.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/notification/platform/callback/reactive/CallbackWebFluxConfiguration.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/NotificationConfig.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/NotificationRoutesSettings.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifier.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/NotificationProvider.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/FailOpenNotificationProvider.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationReconciliationAdapter.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationAttemptContext.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderAttemptClient.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderRateAdmission.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderReadinessSnapshot.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationProviderAttemptAdapter.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/provider/NotificationAdmissionReadinessAdapter.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/provider/InlineNotificationAttemptAdapter.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationTemplateDescriptor.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationPlanAdapter.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationCutoverRouteCatalog.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationBindingCompiler.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationProviderCapabilityDescriptorSource.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationProviderDescriptor.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationRouteDescriptor.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationCanonicalRouteCatalog.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationProviderCapabilityCard.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/template/NotificationTemplateManifest.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/template/NotificationTemplateRenderer.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/template/LocalEmailRenderer.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/template/SlackBlockKitRenderer.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/template/NotificationTemplateCatalog.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderAttemptLimiter.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/NotificationSchedulerWorker.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/RegistryProviderDispatchGateway.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/CredentialValidationException.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderEventReplayWorker.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderRuntimeRegistry.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/RegistryProviderRuntimeControl.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/CapabilityReconciliationGateway.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/LeaseRecoveryService.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/JacksonRoutingPlanCodec.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ReconciliationJobWorker.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderRuntimeRotator.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/LoggingInboxSignalPublisher.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/SingleTenantContext.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ConfiguredProfileCatalog.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderRuntime.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/UuidV7Generator.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/MapTemplateRendererRegistry.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationPlatformAutoConfiguration.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSmtpProviderConfig.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/ProviderType.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/SmtpProviderRuntimeAssembler.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/AssembledProvider.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationProviderAssembly.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmNotificationPayloadProtection.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtector.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/SettingsSecretMaterialProvider.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmCallbackPayloadProtection.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/HmacProviderRequestIdHasher.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/CredentialGeneration.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/ProviderCredentialManager.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ProviderResults.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/UnconfiguredAttachmentResolver.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/EmailAttachments.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/ThymeleafStringTemplateEngine.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/JacksonTemplateContentCodec.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/JsonSchemaVariableValidator.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/TemplateSlotPolicy.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/Sha256MessageDigestAdapter.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/JacksonNotificationVariablesCodec.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/JacksonInboxContentCodec.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/PlaceholderTemplateEngine.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/ThymeleafNotificationRenderer.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/NotificationTemplateEngine.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/CanonicalNotificationRenderer.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/LoggingNotificationAudit.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/LoggingNotificationMetrics.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/NotificationHealthReporter.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/NotificationServingThresholds.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/reactor/ReactiveNotificationOrchestrator.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/reactor/ReactorNotificationOrchestrator.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ses/SesEventNormalizer.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ses/SesFailureClassifier.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ses/SesSuppressionUpdater.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ses/SesNotificationProviderAdapter.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ses/SesRequestMapper.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ses/SesCallbackAdapter.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ses/SesDeliveryProjector.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/Rfc8291Aes128GcmEncryptor.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/WebPushRequestMapper.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/VapidJwtSigner.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/VapidAuthorizationProvider.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/WebPushNotificationProviderAdapter.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/VapidKeyRegistry.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/WebPushFailureClassifier.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/smtp/SmtpNotificationProviderAdapter.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/smtp/SmtpMimeMessageFactory.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/smtp/SmtpFailureClassifier.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioFailureClassifier.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioRequestMapper.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioDeliveryProjector.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioCallbackAdapter.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioStatusNormalizer.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioSmsProviderAdapter.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioReconciliationCapability.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/fcm/FcmTargetMapper.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/fcm/FcmNotificationProviderAdapter.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/fcm/FcmMessageMapper.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/fcm/FcmFailureClassifier.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/fcm/FcmBatchCoordinator.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/fcm/FcmContactPointUpdater.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/apns/ApnsRequestMapper.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/apns/ApnsProviderProperties.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/apns/ApnsNotificationProviderAdapter.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/apns/ApnsFailureClassifier.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webhook/WebhookNotificationProviderAdapter.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/slack/webhook/SlackClient.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/slack/webhook/SlackWebhookProvider.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/email/google/GoogleEmailProvider.java
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/email/google/GoogleEmailClient.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaDeliveryAttemptStore.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaAdminOperationStore.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaSuppressionStore.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaNotificationRequestStore.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaTemplateRegistry.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaPolicyStores.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaRecipientLeaseStore.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaNotificationSideEffectStore.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/NotificationRecordMapper.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JdbcNotificationServingState.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JdbcReconciliationJobStore.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaRecipientDeliveryStore.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaProviderEventLedger.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/TenantBoundRepositoryGuard.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaContactPointStore.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/configuration/NotificationJpaPersistenceFacade.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/inbox/InboxCommitEventPublisher.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/inbox/InboxOutboxRecordFactory.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/inbox/JpaNotificationInbox.java
objectstorage externalProductionFiles=96
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/objectstorage/PosterImageRetirementJob.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/objectstorage/PosterImageObjectStorageConfig.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/event/PosterImageAttachmentPrepared.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/event/PosterImageAttachmentReadyV2.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/AbortPosterImageUploadUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ReconcilePosterImageUploadUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/GetPosterImagePublicationStatusUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/PublishPosterImageUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ReconcilePosterImageRetirementUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImagePublicationPolicy.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImagePublicationFingerprint.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImageOperationIdFactory.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImagePublicationStatus.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImagePublicationReservation.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImageRetirementResult.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImagePublicationReservationCodec.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImageRetirementIntent.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImageUploadIntent.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImageUploadIntentStorePort.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/PosterImageRetirementIntentStorePort.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/command/ReconcilePosterImageUploadCommand.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/command/PublishPosterImageCommand.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/command/AbortPosterImageUploadCommand.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/command/RetirePosterImageCommand.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/command/ReconcilePosterImageRetirementCommand.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/query/GetPosterImagePublicationStatusQuery.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/objectstorage/MultipartObjectContentProducer.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/controller/PosterImagePublicationController.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/outbound/identifier/UuidPosterImageOperationIdFactory.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/outbound/persistence/mapper/PosterImageUploadIntentPersistenceMapper.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/outbound/persistence/mapper/PosterImageRetirementIntentPersistenceMapper.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/outbound/persistence/repository/PosterImageRetirementIntentRepositoryAdapter.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/outbound/persistence/repository/PosterImageUploadIntentRepositoryAdapter.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageDestinationSettings.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/RoutingObjectMutationAdapter.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageBindingCompiler.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/CompiledObjectStorageDestination.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/RoutingObjectReadAdapter.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/RoutingObjectScanMaintenanceAdapter.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageLegacyMigrationConfig.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageCapabilityAssembler.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectDirectMultipartGrantRecord.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectReferencePointerRecord.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectMultipartPartRecord.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/CanonicalJsonObjectControlRecordCodec.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectDirectMultipartSessionRecord.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectStagedObjectRecord.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectReferenceRecord.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectPublicationHandoffRecord.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectOperationRecord.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectMultipartSessionRecord.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectManifestRecord.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/provider/ObjectStorageProvider.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/provider/ObjectStorageProviderOperation.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/codec/ObjectHandleCodec.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/codec/ObjectDataKeyCodec.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/codec/ObjectControlKeyCodec.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/codec/ObjectReferenceCodec.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/codec/ObjectRequestFingerprintCodec.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/Ed25519LegacyAdoptionApprovalVerifier.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/LegacyAdoptionApprovalReplayRecord.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/LegacyObjectAdoptionService.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/LegacyObjectInspection.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectTransferSessionRecord.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectMultipartCoordinator.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectMultipartGrantLedger.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectPartAcknowledgementVerifier.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectMultipartProvider.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectGrantProvider.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectPublishedObjectResolver.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectTransferCoordinator.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectMultipartCompletionVerifier.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/ObjectPublicationHandoffService.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/ObjectOperationEpochRecord.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/ObjectOperationKernel.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/ObjectScanVerdictPolicy.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/StagedObjectPublicationKernel.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/ObjectReferencePublicationService.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/ObjectPolicySnapshot.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3DirectMultipartProvider.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3AsyncRequestBodyBridge.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ObjectEvidenceMapper.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ChecksumPolicy.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ConditionalRequestMapper.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3AsyncResponseBodyBridge.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3DirectTransferProvider.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ObjectOperationResolver.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3DirectCompletionVerifier.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ManagedObjectProvider.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ManagedMultipartProvider.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/multipart/MultipartOperationResolver.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/multipart/MultipartPartLedger.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/multipart/MultipartCompletionEvidence.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/filesystem/LocalObjectStreamTransfer.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/filesystem/LocalDevObjectDataStore.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/filesystem/LocalDevObjectStorageProvider.java
observability externalProductionFiles=10
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/logging/PseudonymizationConfig.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/logging/SamplePseudonymizationConfig.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/CreateWorkLogUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/event/PosterImagePublicationEventPublisher.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/event/PosterEventPublisher.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/filter/RequestLoggingFilter.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/ratelimit/RateLimitWebConfig.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/ratelimit/VersionedEdgeSubjectPseudonymizer.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/observability/MdcCorrelationIdPortAdapter.java
src/adapter/outbound/identifier/src/main/java/dev/caskeleton/adapter/outbound/identifier/HmacUserPrincipalPseudonymizer.java
operation externalProductionFiles=4
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/operationasync/OperationAccessPolicy.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/operationasync/OperationQueryService.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/operationasync/OperationResourceFactory.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/operation/DurableOperationStoreAdapter.java
outbound externalProductionFiles=11
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/objectstorage/PosterImageRetirementJob.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/command/ReconcilePosterImageUploadCommand.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/command/PublishPosterImageCommand.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/command/AbortPosterImageUploadCommand.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/command/ReconcilePosterImageRetirementCommand.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/controller/PosterImagePublicationController.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/provider/ObjectStorageProvider.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/provider/ObjectStorageProviderOperation.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/filesystem/LocalObjectStreamTransfer.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDeadlineCalculator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionRetryBackoff.java
outbox externalProductionFiles=15
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxMetrics.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxRelayScheduler.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/messaging/DisabledMessagingSentinelAutoConfiguration.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/CreateWorkLogUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/event/PosterImagePublicationEventPublisher.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/event/PosterEventPublisher.java
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfig.java
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/outbox/DisabledOutboxMessagePublisher.java
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/outbox/Slf4jOutboxRelayFailureReportAdapter.java
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/outbox/OutboxEnvelopeJson.java
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/outbox/OutboxMessagePublishAdapter.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlImmutableOutboxAppendAdapter.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java
realtime externalProductionFiles=12
src/adapter/inbound/websocket/src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/resume/PortBackedReplayAvailability.java
src/adapter/inbound/websocket/src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/cluster/MessagingFanoutAdapter.java
src/adapter/inbound/websocket/src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/cluster/PortBackedExternalSessionIndex.java
src/adapter/inbound/websocket/src/main/java/dev/caskeleton/adapter/inbound/websocket/advanced/presence/PortBackedPresenceStore.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/advanced/stream/replay/MessagingReplayBridge.java
src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/realtime/RedisEphemeralFanoutAdapter.java
src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/realtime/RedisConnectionRegistryAdapter.java
src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/realtime/RegistrationCodec.java
src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/realtime/RealtimeKeys.java
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/realtime/MessagingDurableFanoutAdapter.java
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/realtime/RealtimeFanoutEnvelopeJson.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/liveevent/JpaLiveEventReplayAdapter.java
security externalProductionFiles=22
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/CreateWorkLogUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/UpdateWorkLogUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/DeleteWorkLogUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/BatchCreateWorkLogsUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ArchivePosterUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/RetirePosterImageUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/PublishPosterUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/DeletePosterUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/AbortPosterImageUploadUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/UploadPosterImageUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ReconcilePosterImageUploadUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/GetPosterImagePublicationStatusUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/UpdatePosterUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/PublishPosterImageUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ReconcilePosterImageRetirementUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/CreatePosterUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/migration/AdoptLegacyPosterImageUseCase.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/authz/MethodSecurityConfig.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/authz/AuthorizationAdapter.java
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/authz/RequiresPermissionAuthorizationManager.java
src/adapter/inbound/graphql/src/main/java/dev/caskeleton/adapter/inbound/graphql/security/ApplicationObjectAuthorization.java
src/adapter/inbound/graphql/src/main/java/dev/caskeleton/adapter/inbound/graphql/security/GraphQlObjectAuthorizationPort.java
storage externalProductionFiles=16
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/objectstorage/PosterImageApiConfig.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/UploadPosterImageUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/posterimage/LegacyPosterImageAdoptionResult.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/command/AdoptLegacyPosterImageCommand.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/migration/AdoptLegacyPosterImageUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/controller/LegacyPosterImageController.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/inbound/web/mapper/PosterWebMapper.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/S3ObjectStorageAdapter.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/ObjectStorageConfig.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/FilesystemObjectStorageAdapter.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/LegacyObjectAdoptionSettings.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageCapabilityConfig.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageLegacyMigrationConfig.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/Ed25519LegacyAdoptionApprovalVerifier.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/LegacyObjectAdoptionService.java
src/adapter/outbound/objectstorage/src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/LegacyObjectInspector.java
transaction externalProductionFiles=35
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformDispatchConfig.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverServiceConfiguration.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/objectstorage/PosterImageApiConfig.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/objectstorage/PosterImageObjectStorageConfig.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/ListWorkLogsUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/CreateWorkLogUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/GetWorkLogUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/ListRecentWorkLogSummariesUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/UpdateWorkLogUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/DeleteWorkLogUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/GetRepoStatsUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/BatchCreateWorkLogsUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ArchivePosterUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/GetPosterUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/RetirePosterImageUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/PublishPosterUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/DeletePosterUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/AbortPosterImageUploadUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/UploadPosterImageUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ReconcilePosterImageUploadUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/GetPosterImagePublicationStatusUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/UpdatePosterUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ListPostersUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/PublishPosterImageUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ReconcilePosterImageRetirementUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/CreatePosterUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/migration/AdoptLegacyPosterImageUseCase.java
src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/idempotency/RedisIdempotencyStoreAdapter.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringPolicyTransactionPort.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicy.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDeadlineCalculator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/inbox/PostgreSqlSameStoreInboxAdapter.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java
usecase externalProductionFiles=23
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/ListWorkLogsUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/CreateWorkLogUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/GetWorkLogUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/ListRecentWorkLogSummariesUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/UpdateWorkLogUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/DeleteWorkLogUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/GetRepoStatsUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/worklog/BatchCreateWorkLogsUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ArchivePosterUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/GetPosterUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/RetirePosterImageUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/PublishPosterUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/DeletePosterUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/AbortPosterImageUploadUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/UploadPosterImageUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ReconcilePosterImageUploadUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/GetPosterImagePublicationStatusUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/UpdatePosterUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ListPostersUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/PublishPosterImageUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/ReconcilePosterImageRetirementUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/CreatePosterUseCase.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/application/poster/migration/AdoptLegacyPosterImageUseCase.java
@@ -0,0 +1,40 @@
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Repository: /shared/codebase/clean-architecture-backend-template
Gradle cwd: /shared/codebase/clean-architecture-backend-template/src
009 executed at: 2026-08-29T06:39:25Z
009 command: ./gradlew :application-core:test --rerun-tasks
009 exit: 0
009 raw stdout/stderr: evidence/raw/009-application-core-test.txt
010a executed at: 2026-08-29T06:39:57Z
010a command: ./gradlew :application-core:messagingApplicationContractQualificationTest --rerun-tasks
010a exit: 1
010a raw excerpt/reproduction record: evidence/raw/010a-messaging-qualification-missing-attestation.txt
010 source digest method: git archive HEAD | sha256sum
010 source digest: sha256:e52b60c97a9496a0b18dc03f9232c295da37de7c0f56e692d000660eef0e370a
010 artifact: application-core/build/libs/application-core-0.0.1+a24ece9cf797.jar
010 artifact digest: sha256:6fd061854ad9a8631b65f5940f2a0792241d688aa4c11efcf392662109965942
010 profile: config/messaging/profile-compatibility.yaml
010 profile digest: sha256:7ac987233951a4d0427ac9e731a20df3a1c4aa1c15f30a5ed2b1e6c280b72afd
010 executed at: 2026-08-29T06:41:19Z
010 command: ./gradlew :application-core:messagingApplicationContractQualificationTest --rerun-tasks -PmessagingSourceDigest=sha256:e52b60c97a9496a0b18dc03f9232c295da37de7c0f56e692d000660eef0e370a -PmessagingArtifactDigest=sha256:6fd061854ad9a8631b65f5940f2a0792241d688aa4c11efcf392662109965942 -PmessagingProfileHash=sha256:7ac987233951a4d0427ac9e731a20df3a1c4aa1c15f30a5ed2b1e6c280b72afd
010 exit: 0
010 raw stdout/stderr: evidence/raw/010-application-core-messaging-qualification.txt
011 executed at: 2026-08-29T06:41:43Z
011 command: ./gradlew :app-bootstrap:test --tests dev.caskeleton.bootstrap.architecture.CleanArchitectureTest --rerun-tasks
011 exit: 0
011 raw stdout/stderr: evidence/raw/011-clean-architecture-test.txt
012 executed at: 2026-08-29T06:43:33Z
012 command: ./gradlew verifyCleanArchitectureDependencies
012 exit: 0
012 raw stdout/stderr: evidence/raw/012-verify-clean-architecture-dependencies.txt
013 executed at: 2026-08-29T06:43:52Z
013 exact reproduction script: evidence/raw/013-application-core-reachability-command.sh
013 exit: 0
013 raw stdout: evidence/raw/013-application-core-reachability.txt
013 observation boundary: scans textual application-package imports/references in production Java source outside application-core; it does not establish runtime execution of every type and does not cover generated/reflection-only registration.
@@ -0,0 +1,130 @@
Command: python3 - <<'PY'
import json
from pathlib import Path
root=Path('src/adapter/outbound/support')
for p in sorted(x for x in root.rglob('*') if x.is_file() and 'build/' not in str(x)):
print(p)
print('--- counts ---')
for label,base,pattern in [('prod-java',root/'src/main/java','*.java'),('test-java',root/'src/test/java','*.java')]:
files=list(base.rglob(pattern)) if base.exists() else []
print(label,'files=',len(files),'loc=',sum(len(p.read_text(errors='ignore').splitlines()) for p in files))
r=json.load(open('src/config/architecture/modules.json'))['modules']
print('--- registry ---')
print(json.dumps(next(m for m in r if m['id']=='adapter-outbound-support'),indent=2))
PY
printf '%s\n' '--- build.gradle ---'; cat src/adapter/outbound/support/build.gradle
printf '%s\n' '--- README.md ---'; cat src/adapter/outbound/support/README.md
printf '%s\n' '--- CLAUDE.md ---'; cat src/adapter/outbound/support/CLAUDE.md
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T07:19:26Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Enumerates the support leaf source/build/document files and registry entry at the recorded revision; downstream consumers are not measured here.
--- stdout/stderr ---
src/adapter/outbound/support/CLAUDE.md
src/adapter/outbound/support/README.md
src/adapter/outbound/support/build.gradle
src/adapter/outbound/support/gradle.lockfile
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/package-info.java
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundCorrelation.java
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundSupportConfig.java
src/adapter/outbound/support/src/test/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLoggerTest.java
--- counts ---
prod-java files= 4 loc= 97
test-java files= 1 loc= 89
--- registry ---
{
"id": "adapter-outbound-support",
"gradle_path": ":adapter:outbound:support",
"source_path": "src/adapter/outbound/support",
"allowed_dependencies": [
"domain-core",
"application-core",
"shared-contract"
],
"runtime_memberships": [
"app-bootstrap"
]
}
--- build.gradle ---
// Shared base for outbound integration adapters: correlation, fail-open dependency
// logging, and the @Configuration seam. Depended on by messaging/cache/notification/httpclient.
dependencies {
implementation 'org.springframework.boot:spring-boot-autoconfigure'
implementation 'org.slf4j:slf4j-api'
}
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
}
--- README.md ---
# adapter:outbound:support — 설계 결정 참조
아웃바운드 기술 어댑터(`:adapter:outbound:messaging`, `:adapter:outbound:cache-redis`,
`:adapter:outbound:notification`, `:adapter:outbound:httpclient`)가 공유하는 베이스 모듈. 패키지
루트: `dev.caskeleton.adapter.outbound.support`.
허용/금지 의존 정책은 `src/build.gradle` 의
`allowedProjectDependencies['adapter:outbound:support']` 항목이 SSOT 다(이 모듈은 아직 별도
CLAUDE.md 를 두지 않았다). 이 문서는 코드 주석에서 덜어낸 **설계 결정의 근거**를 모아둔 참조용
기록이다 — 코드를 읽다 "왜 이렇게 했나"가 궁금할 때 본다.
## 왜 별도 모듈인가
옛 `adapter-outbound` 모듈을 기술별(`messaging`/`cache-redis`/`notification`/`httpclient`)로
쪼개면서, 네 모듈이 공통으로 필요로 하는 조각 — correlation 조회, fail-open 의존성 로깅,
공유 `@Configuration` 등록 지점 — 을 이 모듈로 뽑아냈다. 네 기술 모듈 모두 `implementation
project(':adapter:outbound:support')` 로 이 모듈에 의존한다.
## FailOpenDependencyLogger — WARN, ERROR 아님
실패를 ERROR 가 아니라 WARN 으로 로깅한다: cache/messaging/notification 선택형 어댑터는
fail-open 이라 의존성 실패가 나도 use case 는 성공한 것이다 — 관측은 하되 escalate 하지 않는다.
이는 호출자에게 직접 노출되는 hard failure 를 ERROR 로 올리는 `httpclient` 모듈의
`OutboundHttpDependencyLogger` 와 명확히 구분된다. 메서드 시그니처가 본문·수신자·페이로드를
받지 않아 PII 가 로그에 닿지 않는다.
## OutboundCorrelation — `"unknown"` 센티넬
MDC 에 correlation id 가 없으면 `null` 이 아니라 `"unknown"` 문자열을 반환해 로그 라인에 필드가
비지 않게 한다. MDC 키는 SSOT(`mdc-keys.yaml`)를 따른다.
## OutboundSupportConfig — 공유 `@Configuration` seam
`FailOpenDependencyLogger` 빈을 `@ConditionalOnMissingBean` 으로 무조건 등록하는 단일
`@Configuration` 이다. 네 기술 모듈이 각자 로거 빈을 중복 등록하지 않고 이 모듈에만 의존하면
와이어링되며, 포크는 같은 타입의 빈을 직접 등록해 오버라이드할 수 있다. 이 모듈 자체는 어떤
`@ConditionalOnProperty` 게이팅도 갖지 않는다 — 게이팅은 각 기술 모듈이 자신의 연동 client
단위로 소유한다.
--- CLAUDE.md ---
# adapter:outbound:support — shared outbound support
## Registered identity
- Module ID: `adapter-outbound-support`
- Gradle path: `:adapter:outbound:support`
- Focused test (derived from Gradle path): `./gradlew :adapter:outbound:support:test --console=plain`
- Runtime baseline: Java 21; repository framework baseline: Spring Boot 4.0.0.
- Registry SSOT: `src/config/architecture/modules.json`.
Package roots: `dev.caskeleton.adapter.outbound` and
`dev.caskeleton.adapter.outbound.support`.
## Responsibility
- Own reusable outbound correlation, fail-open dependency logging, and auto-configuration seams.
- Provide technical support used by registered messaging/cache/notification/httpclient leaves.
- Keep feature-specific clients and routing policies in their owning leaf.
## Boundaries
- Allowed project dependencies are registry-owned: application, domain, and shared contracts.
- No inbound transport, persistence, bootstrap, or sample dependencies.
- No business rules or external-system-specific protocol implementation.
## Tests
Use focused unit tests for support behavior. Do not start a Spring application context unless the
contract being tested is configuration wiring itself.
Exit code: 0
@@ -0,0 +1,52 @@
Command: for t in FailOpenDependencyLogger OutboundCorrelation OutboundSupportConfig; do echo "=== $t ==="; git grep -n -w "$t" -- 'src/**' ':!src/adapter/outbound/support/**'; echo "git-grep-exit=$?"; done
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T07:19:26Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Searches tracked repository text outside the support leaf for three support public types; textual search does not cover reflection, generated code, or external consumers.
--- stdout/stderr ---
=== FailOpenDependencyLogger ===
src/adapter/outbound/messaging/CLAUDE.md:71: `FailOpenDependencyLogger`.
src/adapter/outbound/messaging/README.md:42:로깅하고 삼켜(→ `:adapter:outbound:support` 의 `FailOpenDependencyLogger`) outbox/retry 로
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfig.java:10:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfig.java:33: FailOpenDependencyLogger dependencyLogger) {
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisher.java:3:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisher.java:19: private final FailOpenDependencyLogger dependencyLogger;
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisher.java:21: public OutboundMessagePublisher(MessageBroker broker, FailOpenDependencyLogger dependencyLogger) {
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfigTest.java:11:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfigTest.java:21: .withBean(FailOpenDependencyLogger.class, FailOpenDependencyLogger::new)
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:9:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:28: private FailOpenDependencyLogger dependencyLogger;
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:37: dependencyLogger = new FailOpenDependencyLogger(logbackLogger);
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:138: new OutboundMessagePublisher(broker, new FailOpenDependencyLogger(throwingLogger));
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/outbox/OutboxMessagePublishAdapterTest.java:11:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/outbox/OutboxMessagePublishAdapterTest.java:143: (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(FailOpenDependencyLogger.class);
src/adapter/outbound/notification/README.md:51:`:adapter:outbound:support` 의 `FailOpenDependencyLogger` 로 WARN 로깅). 라우팅은
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/NotificationConfig.java:6:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/NotificationConfig.java:33: FailOpenDependencyLogger failOpenDependencyLogger) {
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/FailOpenNotificationProvider.java:3:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/FailOpenNotificationProvider.java:17: private final FailOpenDependencyLogger dependencyLogger;
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/FailOpenNotificationProvider.java:20: NotificationProvider delegate, FailOpenDependencyLogger dependencyLogger) {
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:10:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:47: private FailOpenDependencyLogger dependencyLogger;
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:56: dependencyLogger = new FailOpenDependencyLogger(logbackLogger);
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java:10:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java:55: private FailOpenDependencyLogger dependencyLogger;
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java:64: dependencyLogger = new FailOpenDependencyLogger(logbackLogger);
src/app-bootstrap/README.md:532: "로거가 애초에 본문/페이로드를 받지 않도록 설계한 것"(`FailOpenDependencyLogger`)이고, 이 정규식은
git-grep-exit=0
=== OutboundCorrelation ===
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:10:import dev.caskeleton.adapter.outbound.support.OutboundCorrelation;
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:96: MDC.put(OutboundCorrelation.MDC_KEY, "corr-msg-1");
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:11:import dev.caskeleton.adapter.outbound.support.OutboundCorrelation;
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:99: MDC.put(OutboundCorrelation.MDC_KEY, "corr-email-1");
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:131: MDC.put(OutboundCorrelation.MDC_KEY, "corr-slack-1");
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java:11:import dev.caskeleton.adapter.outbound.support.OutboundCorrelation;
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java:155: MDC.put(OutboundCorrelation.MDC_KEY, "corr-1");
src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java:1358: // :adapter:outbound:support is the shared base (OutboundCorrelation, fail-open
git-grep-exit=0
=== OutboundSupportConfig ===
src/app-bootstrap/src/test/java/dev/caskeleton/adapter/outbound/OptionalAdapterBeanGatingTest.java:27:import dev.caskeleton.adapter.outbound.support.OutboundSupportConfig;
src/app-bootstrap/src/test/java/dev/caskeleton/adapter/outbound/OptionalAdapterBeanGatingTest.java:66: OutboundSupportConfig.class,
git-grep-exit=0
Exit code: 0
@@ -0,0 +1,117 @@
Command: printf '%s\n' '--- support config annotations ---'; grep -n -E '@Configuration|@Bean|@Conditional' src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundSupportConfig.java; printf '%s\n' '--- app component scan/exclusions ---'; sed -n '35,125p' src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/CaSkeletonApplication.java; printf '%s\n' '--- registry support membership/consumers ---'; python3 - <<'PY'
import json
mods=json.load(open('src/config/architecture/modules.json'))['modules']
for m in mods:
if m['id']=='adapter-outbound-support' or 'adapter-outbound-support' in m.get('allowed_dependencies',[]):
print(m['id'],m['gradle_path'],'runtime=',m.get('runtime_memberships'),'allows=',m.get('allowed_dependencies'))
PY
printf '%s\n' '--- explicit support config references ---'; git grep -n -w 'OutboundSupportConfig' -- 'src/**' ':!src/adapter/outbound/support/**'; echo "git-grep-exit=$?"
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T07:19:26Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Shows support configuration annotations, composition-root component-scan ownership, optional-adapter config references, and registry runtime membership; it does not boot the application.
--- stdout/stderr ---
--- support config annotations ---
10: * {@code @ConditionalOnMissingBean} lets a fork override it.
12:@Configuration
15: @Bean
16: @ConditionalOnMissingBean
--- app component scan/exclusions ---
* component scanner discover each child configuration on its own and assemble the capability
* whatever that switch said. Registering them as auto-configuration instead makes "off" a
* structural fact rather than a condition every future bean has to remember to repeat.
*/
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
basePackages = {
"dev.caskeleton.bootstrap",
"dev.caskeleton.adapter",
"dev.caskeleton.application",
"dev.caskeleton.domain",
"dev.caskeleton.shared"
},
excludeFilters = {
@ComponentScan.Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@ComponentScan.Filter(
type = FilterType.CUSTOM,
classes = AutoConfigurationExcludeFilter.class),
@ComponentScan.Filter(
type = FilterType.REGEX,
pattern = CaSkeletonApplication.AUTO_CONFIGURED_PACKAGES)
})
@ConfigurationPropertiesScan(
basePackages = {
"dev.caskeleton.bootstrap.async",
"dev.caskeleton.bootstrap.autoconfigure.fileserver",
"dev.caskeleton.bootstrap.autoconfigure.httpclient",
"dev.caskeleton.bootstrap.concurrency",
"dev.caskeleton.bootstrap.idempotency",
"dev.caskeleton.bootstrap.outbox",
"dev.caskeleton.bootstrap.redis",
"dev.caskeleton.bootstrap.runtime",
"dev.caskeleton.bootstrap.security",
"dev.caskeleton.bootstrap.settings",
"dev.caskeleton.bootstrap.tracing",
"dev.caskeleton.adapter.inbound.grpc",
"dev.caskeleton.adapter.inbound.web",
"dev.caskeleton.adapter.inbound.websocket",
"dev.caskeleton.adapter.outbound.cache.redis",
"dev.caskeleton.adapter.outbound.fileserver",
"dev.caskeleton.adapter.outbound.objectstorage",
"dev.caskeleton.application",
"dev.caskeleton.domain",
"dev.caskeleton.shared"
})
public class CaSkeletonApplication {
/**
* Packages owned by an auto-configuration entry rather than by the component scan.
*
* <p>A package prefix rather than a class list: a new configuration added to an optional
* capability must not become active merely because nobody remembered to exclude it.
*
* <p>The Fileserver admin package is here for a different reason: those routes belong to the
* management context, and a component scan that also found them would publish the management
* plane on the public connector — the exposure the separate context exists to remove.
*
* <p>The web platform's error, budget and operation packages are here for a third reason. Their
* advices and controllers need beans that only exist when the corresponding platform
* auto-configuration is active, and a component scan finds them regardless — so an all-off or
* partially configured deployment failed to start on an unsatisfied dependency rather than simply
* not installing the control. Ownership by auto-configuration is what ties a control's presence
* to its dependency's.
*/
static final String AUTO_CONFIGURED_PACKAGES =
"dev\\.caskeleton\\.bootstrap\\.autoconfigure\\..*"
+ "|dev\\.caskeleton\\.bootstrap\\.notification\\..*"
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.web\\.fileserver\\.admin\\..*"
+ "|dev\\.caskeleton\\.adapter\\.outbound\\.mongo\\..*"
+ "|dev\\.caskeleton\\.adapter\\.outbound\\.messaging\\..*"
+ "|dev\\.caskeleton\\.adapter\\.outbound\\.notification\\..*"
+ "|dev\\.caskeleton\\.adapter\\.outbound\\.persistence\\..*"
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.graphql\\..*"
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.web\\.mvc\\.error\\..*"
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.web\\.mvc\\.budget\\..*"
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.web\\.mvc\\.operation\\..*"
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.web\\.webflux\\.error\\..*"
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.web\\.webflux\\.operation\\..*"
+ "|dev\\.caskeleton\\.messaging\\..*";
public static void main(String[] args) {
SpringApplication.run(CaSkeletonApplication.class, args);
}
}
--- registry support membership/consumers ---
adapter-outbound-support :adapter:outbound:support runtime= ['app-bootstrap'] allows= ['domain-core', 'application-core', 'shared-contract']
adapter-outbound-cache-redis :adapter:outbound:cache-redis runtime= ['app-bootstrap'] allows= ['domain-core', 'application-core', 'shared-contract', 'adapter-outbound-support']
adapter-outbound-httpclient :adapter:outbound:httpclient runtime= ['app-bootstrap'] allows= ['domain-core', 'application-core', 'shared-contract', 'adapter-outbound-support']
adapter-outbound-messaging :adapter:outbound:messaging runtime= ['app-bootstrap'] allows= ['domain-core', 'application-core', 'shared-contract', 'adapter-outbound-support']
adapter-outbound-notification :adapter:outbound:notification runtime= ['app-bootstrap'] allows= ['domain-core', 'application-core', 'shared-contract', 'adapter-outbound-support']
app-bootstrap :app-bootstrap runtime= ['app-bootstrap'] allows= ['domain-core', 'application-core', 'adapter-outbound-persistence-jpa', 'adapter-outbound-support', 'adapter-outbound-messaging', 'adapter-outbound-cache-redis', 'adapter-outbound-notification', 'adapter-outbound-fileserver', 'adapter-outbound-httpclient', 'adapter-outbound-identifier', 'adapter-inbound-web', 'shared-contract', 'adapter-outbound-persistence-mongo', 'adapter-inbound-graphql', 'messaging-spring-boot-starter']
--- explicit support config references ---
src/app-bootstrap/src/test/java/dev/caskeleton/adapter/outbound/OptionalAdapterBeanGatingTest.java:27:import dev.caskeleton.adapter.outbound.support.OutboundSupportConfig;
src/app-bootstrap/src/test/java/dev/caskeleton/adapter/outbound/OptionalAdapterBeanGatingTest.java:66: OutboundSupportConfig.class,
git-grep-exit=0
Exit code: 0
@@ -0,0 +1,87 @@
Command: printf '%s\n' '--- support imports in production ---'; git grep -n 'dev\.caskeleton\.adapter\.outbound\.support' -- 'src/**/src/main/java/**/*.java' ':!src/adapter/outbound/support/**'; echo "support-import-exit=$?"; printf '%s\n' '--- dependency logger names ---'; git grep -n -E 'FailOpenDependencyLogger|OutboundHttpDependencyLogger|DependencyLogger|dependency_name=' -- 'src/adapter/outbound/**' ':!src/adapter/outbound/support/build/**'; echo "logger-search-exit=$?"; printf '%s\n' '--- correlation implementations/usages ---'; git grep -n -E 'OutboundCorrelation|MDC\.get\(|correlation_id' -- 'src/adapter/outbound/**/src/main/java/**/*.java'; echo "correlation-search-exit=$?"
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T07:19:26Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Searches current outbound production sources and docs for dependency-logger/correlation/fail-open mechanisms and direct support imports; matches require semantic comparison before being called duplicates.
--- stdout/stderr ---
--- support imports in production ---
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfig.java:10:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisher.java:3:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/NotificationConfig.java:6:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/FailOpenNotificationProvider.java:3:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
support-import-exit=0
--- dependency logger names ---
src/adapter/outbound/messaging/CLAUDE.md:71: `FailOpenDependencyLogger`.
src/adapter/outbound/messaging/README.md:42:로깅하고 삼켜(→ `:adapter:outbound:support` 의 `FailOpenDependencyLogger`) outbox/retry 로
src/adapter/outbound/messaging/README.md:68:`dependency_name=disabled`를 쓴다. 이 adapter는 confirmed FAILED/DEAD report 하나를 SLF4J 2 fluent
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfig.java:10:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfig.java:33: FailOpenDependencyLogger dependencyLogger) {
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisher.java:3:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisher.java:19: private final FailOpenDependencyLogger dependencyLogger;
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisher.java:21: public OutboundMessagePublisher(MessageBroker broker, FailOpenDependencyLogger dependencyLogger) {
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfigTest.java:11:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfigTest.java:21: .withBean(FailOpenDependencyLogger.class, FailOpenDependencyLogger::new)
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:9:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:28: private FailOpenDependencyLogger dependencyLogger;
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:37: dependencyLogger = new FailOpenDependencyLogger(logbackLogger);
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:106: .contains("dependency_name=\"kafka\"")
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisherTest.java:138: new OutboundMessagePublisher(broker, new FailOpenDependencyLogger(throwingLogger));
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/outbox/OutboxMessagePublishAdapterTest.java:11:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/messaging/src/test/java/dev/caskeleton/adapter/outbound/messaging/outbox/OutboxMessagePublishAdapterTest.java:143: (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(FailOpenDependencyLogger.class);
src/adapter/outbound/notification/README.md:51:`:adapter:outbound:support` 의 `FailOpenDependencyLogger` 로 WARN 로깅). 라우팅은
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/NotificationConfig.java:6:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/NotificationConfig.java:33: FailOpenDependencyLogger failOpenDependencyLogger) {
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/NotificationConfig.java:36: .map(p -> new FailOpenNotificationProvider(p, failOpenDependencyLogger))
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/FailOpenNotificationProvider.java:3:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/FailOpenNotificationProvider.java:17: private final FailOpenDependencyLogger dependencyLogger;
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/FailOpenNotificationProvider.java:20: NotificationProvider delegate, FailOpenDependencyLogger dependencyLogger) {
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:10:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:47: private FailOpenDependencyLogger dependencyLogger;
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:56: dependencyLogger = new FailOpenDependencyLogger(logbackLogger);
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:113: .contains("dependency_name=\"google-email\"")
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/NotificationAdapterTest.java:145: .contains("dependency_name=\"slack-webhook\"")
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java:10:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java:55: private FailOpenDependencyLogger dependencyLogger;
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java:64: dependencyLogger = new FailOpenDependencyLogger(logbackLogger);
src/adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/core/RoutingNotifierTest.java:169: .contains("dependency_name=\"google-email\"")
src/adapter/outbound/support/README.md:19:## FailOpenDependencyLogger — WARN, ERROR 아님
src/adapter/outbound/support/README.md:24:`OutboundHttpDependencyLogger` 와 명확히 구분된다. 메서드 시그니처가 본문·수신자·페이로드를
src/adapter/outbound/support/README.md:34:`FailOpenDependencyLogger` 빈을 `@ConditionalOnMissingBean` 으로 무조건 등록하는 단일
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java:13:public final class FailOpenDependencyLogger {
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java:17: public FailOpenDependencyLogger() {
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java:18: this(LoggerFactory.getLogger(FailOpenDependencyLogger.class));
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java:22: public FailOpenDependencyLogger(Logger log) {
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java:28: "dependency_name=\"{}\" dependency_type=\"{}\" operation=\"{}\" "
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java:40: "dependency_name=\"{}\" dependency_type=\"{}\" operation=\"{}\" "
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundSupportConfig.java:8: * Unconditionally registers the shared log/error contract bean ({@link FailOpenDependencyLogger})
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundSupportConfig.java:17: public FailOpenDependencyLogger failOpenDependencyLogger() {
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundSupportConfig.java:18: return new FailOpenDependencyLogger();
src/adapter/outbound/support/src/test/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLoggerTest.java:14:class FailOpenDependencyLoggerTest {
src/adapter/outbound/support/src/test/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLoggerTest.java:18: private FailOpenDependencyLogger dependencyLogger;
src/adapter/outbound/support/src/test/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLoggerTest.java:28: dependencyLogger = new FailOpenDependencyLogger(logbackLogger);
src/adapter/outbound/support/src/test/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLoggerTest.java:48: .contains("dependency_name=\"kafka\"")
src/adapter/outbound/support/src/test/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLoggerTest.java:84: .contains("dependency_name=\"slack\"")
logger-search-exit=0
--- correlation implementations/usages ---
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/outbox/Slf4jOutboxRelayFailureReportAdapter.java:52: .addKeyValue("correlation_id", report.correlationId())
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/NotificationRequestEntity.java:73: @Column(name = "correlation_id", length = 160)
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/NotificationRequestJpaRepository.java:53: + " schedule_at, not_before, expires_at, request_status, correlation_id,"
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/entity/OutboxEventEntity.java:49: @Column(name = "correlation_id", nullable = false, length = 64, updatable = false)
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlImmutableOutboxAppendAdapter.java:103: correlation_id,
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlImmutableOutboxAppendAdapter.java:128: event.correlation_id,
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlImmutableOutboxAppendAdapter.java:286: resultSet.getString("correlation_id"),
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java:148: event.correlation_id,
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java:413: resultSet.getString("correlation_id"),
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java:29: + "outcome=\"SUCCESS\" correlation_id=\"{}\"",
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java:33: OutboundCorrelation.current());
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java:41: + "outcome=\"FAILURE\" correlation_id=\"{}\" error=\"{}: {}\"",
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java:45: OutboundCorrelation.current(),
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundCorrelation.java:6: * Reads the {@code correlation_id} from the SLF4J MDC for outbound adapter logging. The MDC key
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundCorrelation.java:11:public final class OutboundCorrelation {
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundCorrelation.java:13: /** MDC key SSOT (docs/registries/mdc-keys.yaml — key: correlation_id). */
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundCorrelation.java:14: public static final String MDC_KEY = "correlation_id";
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundCorrelation.java:19: private OutboundCorrelation() {}
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundCorrelation.java:23: String value = MDC.get(MDC_KEY);
correlation-search-exit=0
Exit code: 0
@@ -0,0 +1,65 @@
Command: printf '%s\n' '--- support README named claims ---'; grep -n -E 'allowedProjectDependencies|CLAUDE|OutboundHttpDependencyLogger|project\(' src/adapter/outbound/support/README.md; printf '%s\n' '--- current policy authority ---'; git grep -n -E 'modules\.json|verifyCleanArchitectureDependencies' -- AGENTS.md src/adapter/outbound/support/CLAUDE.md src/build.gradle | head -180; printf '%s\n' '--- current exact http logger references ---'; git grep -n -w 'OutboundHttpDependencyLogger' -- 'src/**'; echo "http-logger-current-exit=$?"; printf '%s\n' '--- symbol history ---'; git log --all --name-status --format='COMMIT %H %s' -S'OutboundHttpDependencyLogger' -- src | sed -n '1,180p'
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T07:19:26Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Compares named support README claims with current policy documents/source and records history for OutboundHttpDependencyLogger; this is textual/document history evidence, not a general documentation audit.
--- stdout/stderr ---
--- support README named claims ---
8:`allowedProjectDependencies['adapter:outbound:support']` 항목이 SSOT 다(이 모듈은 아직 별도
9:CLAUDE.md 를 두지 않았다). 이 문서는 코드 주석에서 덜어낸 **설계 결정의 근거**를 모아둔 참조용
17:project(':adapter:outbound:support')` 로 이 모듈에 의존한다.
24:`OutboundHttpDependencyLogger` 와 명확히 구분된다. 메서드 시그니처가 본문·수신자·페이로드를
--- current policy authority ---
AGENTS.md:21:5. 프로젝트 의존성이 `src/config/architecture/modules.json` 또는 Gradle 의존성 검증을 위반한다.
AGENTS.md:52:- `src/config/architecture/modules.json`: 등록된 모든 leaf의 ID, repository-relative 소스 경로,
AGENTS.md:57:- `src/build.gradle`: 같은 registry를 읽는 `verifyCleanArchitectureDependencies`와 그 밖의
AGENTS.md:107:`src/config/architecture/modules.json`이 SSOT다. focused test는 소유 leaf의 `gradle_path`에서
AGENTS.md:136:개별 edge는 `src/config/architecture/modules.json`의 `allowed_dependencies`가 유일한 목록이다.
AGENTS.md:179:./gradlew check # check 가 verifyCleanArchitectureDependencies + verifyEnvKeys 2종을 전이 실행한다 (src/build.gradle)
AGENTS.md:180:./gradlew verifyCleanArchitectureDependencies
AGENTS.md:185:소유 leaf의 정확한 Gradle path는 `src/config/architecture/modules.json`에서 읽고 focused test
src/adapter/outbound/support/CLAUDE.md:9:- Registry SSOT: `src/config/architecture/modules.json`.
src/build.gradle:574: dependsOn rootProject.tasks.named('verifyCleanArchitectureDependencies')
src/build.gradle:1101: dependsOn 'verifyCleanArchitectureDependencies'
src/build.gradle:1310: File moduleRegistryFile = new File(rootProject.projectDir, 'config/architecture/modules.json')
src/build.gradle:1401:tasks.register('verifyCleanArchitectureDependencies') {
src/build.gradle:1405: File moduleRegistryFile = new File(rootProject.projectDir, 'config/architecture/modules.json')
src/build.gradle:1440: "Gradle modules ${missingFromPolicy} are not covered by verifyCleanArchitectureDependencies. " +
src/build.gradle:2152: File moduleRegistryFile = new File(rootProject.projectDir, 'config/architecture/modules.json')
--- current exact http logger references ---
src/adapter/outbound/support/README.md:24:`OutboundHttpDependencyLogger` 와 명확히 구분된다. 메서드 시그니처가 본문·수신자·페이로드를
http-logger-current-exit=0
--- symbol history ---
COMMIT 5f10b791d35a02a4842814b90a0e0362dcef3982 chore: record pre-existing uncommitted repository state
M src/adapter/outbound/httpclient/README.md
D src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpCallObserver.java
D src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClient.java
D src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClientConfig.java
D src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/diagnostics/OutboundHttpDependencyLogger.java
D src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpCallObserverTest.java
D src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClientDeadlineTest.java
D src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClientSafetyRegressionTest.java
D src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClientTest.java
D src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/diagnostics/OutboundHttpDependencyLoggerTest.java
M src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java
COMMIT 567422f2e5ff0557e76b233fe5174812d63b515e feat: add production capability foundations
M src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java
COMMIT b3add0162df8d4a0a11e749e514901defe0a62a3 feat: redis, fileserver, httpclient 런타임 시점 구현 추가
A src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClientDeadlineTest.java
A src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClientSafetyRegressionTest.java
COMMIT 821fe00c323b5335980f271c7ee47b92ac2168f2 init: 클린 아키텍처 백엔드
A src/adapter/outbound/httpclient/README.md
A src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpCallObserver.java
A src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClient.java
A src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClientConfig.java
A src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/diagnostics/OutboundHttpDependencyLogger.java
A src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpCallObserverTest.java
A src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpClientTest.java
A src/adapter/outbound/httpclient/src/test/java/dev/caskeleton/adapter/outbound/httpclient/diagnostics/OutboundHttpDependencyLoggerTest.java
A src/adapter/outbound/support/README.md
Exit code: 0
@@ -0,0 +1,34 @@
Command: for m in cache-redis httpclient messaging notification; do echo "=== $m ==="; grep -n "adapter:outbound:support" "src/adapter/outbound/$m/build.gradle"; echo "build-grep-exit=$?"; git grep -n 'dev\.caskeleton\.adapter\.outbound\.support' -- "src/adapter/outbound/$m/src/main/java/**/*.java"; echo "source-grep-exit=$?"; done; printf '%s\n' '--- support resources ---'; find src/adapter/outbound/support/src -type f | sort
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T07:19:26Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Compares four outbound leaves that declare a Gradle dependency on support with current production Java textual imports of support types; resource/reflection-only usage would require separate evidence.
--- stdout/stderr ---
=== cache-redis ===
15: implementation project(':adapter:outbound:support')
build-grep-exit=0
source-grep-exit=1
=== httpclient ===
14: implementation project(':adapter:outbound:support')
build-grep-exit=0
source-grep-exit=1
=== messaging ===
5: implementation project(':adapter:outbound:support')
build-grep-exit=0
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/MessagingConfig.java:10:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/messaging/src/main/java/dev/caskeleton/adapter/outbound/messaging/core/OutboundMessagePublisher.java:3:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
source-grep-exit=0
=== notification ===
4: implementation project(':adapter:outbound:support')
build-grep-exit=0
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/NotificationConfig.java:6:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
src/adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/core/FailOpenNotificationProvider.java:3:import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
source-grep-exit=0
--- support resources ---
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/package-info.java
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundCorrelation.java
src/adapter/outbound/support/src/main/java/dev/caskeleton/adapter/outbound/support/OutboundSupportConfig.java
src/adapter/outbound/support/src/test/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLoggerTest.java
Exit code: 0
@@ -0,0 +1,10 @@
Command: CP='adapter/outbound/support/build/classes/java/main:/root/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/2.0.18/78a9e7a37cd6360e0b818e86341b24123d28d4df/slf4j-api-2.0.18.jar:/root/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.5.38/bc5873cb4a7f502ce18ce2977d98ad26b9f96444/logback-classic-1.5.38.jar:/root/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-core/1.5.38/7b1d133b29bf42ff268cf3f71d1308b80480fb8d/logback-core-1.5.38.jar'; java --class-path "$CP" /shared/document-detail/clean-architecture-backend-template/evidence/raw/021a-support-logger-pii-probe.java
Working directory: /shared/codebase/clean-architecture-backend-template/src
Executed at: 2026-08-29T07:19:45Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Runs the current compiled FailOpenDependencyLogger with an exception message containing explicit test PII markers; it demonstrates rendered logger input/output for this controlled probe, not whether a specific external provider emits such messages in production.
--- stdout/stderr ---
07:19:46.022 [main] WARN probe.support.pii -- dependency_name="google-email" dependency_type="notification" operation="send" outcome="FAILURE" correlation_id="unknown" error="RuntimeException: provider rejected recipient secret@gmail.com body=secret-body-content"
dependency_name="google-email" dependency_type="notification" operation="send" outcome="FAILURE" correlation_id="unknown" error="RuntimeException: provider rejected recipient secret@gmail.com body=secret-body-content"
Exit code: 0
@@ -0,0 +1,23 @@
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
import org.slf4j.LoggerFactory;
public class SupportLoggerLeakProbe {
public static void main(String[] args) {
var logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("probe.support.pii");
var appender = new ListAppender<ILoggingEvent>();
appender.start();
logger.addAppender(appender);
logger.setLevel(Level.DEBUG);
try {
new FailOpenDependencyLogger(logger).logFailure(
"google-email", "notification", "send",
new RuntimeException("provider rejected recipient secret@gmail.com body=secret-body-content"));
System.out.println(appender.list.get(0).getFormattedMessage());
} finally {
logger.detachAppender(appender);
}
}
}
@@ -0,0 +1,13 @@
Command: CP='adapter/outbound/notification/build/classes/java/main:adapter/outbound/support/build/classes/java/main:application-core/build/classes/java/main:shared-contract/build/classes/java/main:/root/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/2.0.18/78a9e7a37cd6360e0b818e86341b24123d28d4df/slf4j-api-2.0.18.jar'; java --class-path "$CP" /shared/document-detail/clean-architecture-backend-template/evidence/raw/022a-notification-logger-failure-probe.java
Working directory: /shared/codebase/clean-architecture-backend-template/src
Executed at: 2026-08-29T07:19:46Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Runs the current compiled FailOpenNotificationProvider with a deliberately throwing SLF4J Logger proxy to observe success/failure logging control flow; it is a focused local probe, not a real logging-backend outage test.
--- stdout/stderr ---
SLF4J(W): No SLF4J providers were found.
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
SUCCESS_PATH sends=1 debugCalls=1 warnCalls=1
FAILURE_PATH propagated=IllegalStateException:logger-warn-failed warnCalls=1
Exit code: 0
@@ -0,0 +1,60 @@
import dev.caskeleton.adapter.outbound.notification.core.FailOpenNotificationProvider;
import dev.caskeleton.adapter.outbound.notification.core.NotificationProvider;
import dev.caskeleton.adapter.outbound.support.FailOpenDependencyLogger;
import dev.caskeleton.application.notification.Channel;
import dev.caskeleton.application.notification.Notification;
import java.lang.reflect.Proxy;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
public class NotificationLoggerFailureProbe {
private static Logger loggerThatThrowsOn(String methodToThrow, AtomicInteger debug, AtomicInteger warn) {
return (Logger) Proxy.newProxyInstance(
NotificationLoggerFailureProbe.class.getClassLoader(),
new Class<?>[] {Logger.class},
(proxy, method, args) -> {
if (method.getName().equals("debug")) debug.incrementAndGet();
if (method.getName().equals("warn")) warn.incrementAndGet();
if (method.getName().equals(methodToThrow)) {
throw new IllegalStateException("logger-" + methodToThrow + "-failed");
}
if (method.getReturnType() == boolean.class) return true;
if (method.getReturnType() == String.class) return "probe.notification";
return null;
});
}
public static void main(String[] args) {
Notification n = new Notification("secret@gmail.com", "subject", "body");
AtomicInteger sends = new AtomicInteger();
AtomicInteger debug = new AtomicInteger();
AtomicInteger warn = new AtomicInteger();
NotificationProvider successful = new NotificationProvider() {
public Channel channel() { return Channel.EMAIL; }
public String providerId() { return "google-email"; }
public void send(Notification ignored) { sends.incrementAndGet(); }
};
var successPath = new FailOpenNotificationProvider(
successful, new FailOpenDependencyLogger(loggerThatThrowsOn("debug", debug, warn)));
successPath.send(n);
System.out.printf("SUCCESS_PATH sends=%d debugCalls=%d warnCalls=%d%n", sends.get(), debug.get(), warn.get());
AtomicInteger failDebug = new AtomicInteger();
AtomicInteger failWarn = new AtomicInteger();
NotificationProvider failing = new NotificationProvider() {
public Channel channel() { return Channel.EMAIL; }
public String providerId() { return "google-email"; }
public void send(Notification ignored) { throw new RuntimeException("provider-failed"); }
};
var failurePath = new FailOpenNotificationProvider(
failing, new FailOpenDependencyLogger(loggerThatThrowsOn("warn", failDebug, failWarn)));
try {
failurePath.send(n);
System.out.println("FAILURE_PATH propagated=none");
} catch (RuntimeException ex) {
System.out.printf("FAILURE_PATH propagated=%s:%s warnCalls=%d%n",
ex.getClass().getSimpleName(), ex.getMessage(), failWarn.get());
}
}
}
@@ -0,0 +1,28 @@
Command: ./gradlew :adapter:outbound:support:test --rerun-tasks --console=plain
Working directory: /shared/codebase/clean-architecture-backend-template/src
Executed at: 2026-08-29T07:19:56Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Fresh focused unit-test execution for the support leaf; it does not exercise downstream adapter/provider runtime behavior.
--- stdout/stderr ---
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :adapter:outbound:support:compileJava
> Task :adapter:outbound:support:processResources NO-SOURCE
> Task :adapter:outbound:support:classes
> Task :adapter:outbound:support:compileTestJava
> Task :adapter:outbound:support:processTestResources NO-SOURCE
> Task :adapter:outbound:support:testClasses
> Task :adapter:outbound:support:test
BUILD SUCCESSFUL in 7s
11 actionable tasks: 11 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
Exit code: 0
@@ -0,0 +1,65 @@
Command: ./gradlew :adapter:outbound:support:dependencies --configuration compileClasspath --console=plain
Working directory: /shared/codebase/clean-architecture-backend-template/src
Executed at: 2026-08-29T07:20:03Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Gradle compileClasspath resolution for support at this revision; it reports declared/resolved compile dependencies, not runtime consumers.
--- stdout/stderr ---
> Task :build-logic:extractPluginRequests UP-TO-DATE
> Task :build-logic:generatePluginAdapters UP-TO-DATE
> Task :build-logic:compileJava UP-TO-DATE
> Task :build-logic:compileGroovy UP-TO-DATE
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
> Task :build-logic:pluginDescriptors UP-TO-DATE
> Task :build-logic:processResources UP-TO-DATE
> Task :build-logic:classes UP-TO-DATE
> Task :build-logic:jar UP-TO-DATE
> Task :adapter:outbound:support:dependencies
------------------------------------------------------------
Project ':adapter:outbound:support'
------------------------------------------------------------
compileClasspath - Compile classpath for source set 'main'.
+--- org.springframework.boot:spring-boot-autoconfigure -> 4.0.8
| \--- org.springframework.boot:spring-boot:4.0.8
| +--- org.springframework:spring-core:7.0.9
| | +--- commons-logging:commons-logging:1.3.5 -> 1.3.6
| | \--- org.jspecify:jspecify:1.0.0 -> 1.0.1
| \--- org.springframework:spring-context:7.0.9
| +--- org.springframework:spring-aop:7.0.9
| | +--- org.springframework:spring-beans:7.0.9
| | | \--- org.springframework:spring-core:7.0.9 (*)
| | \--- org.springframework:spring-core:7.0.9 (*)
| +--- org.springframework:spring-beans:7.0.9 (*)
| +--- org.springframework:spring-core:7.0.9 (*)
| +--- org.springframework:spring-expression:7.0.9
| | \--- org.springframework:spring-core:7.0.9 (*)
| \--- io.micrometer:micrometer-observation:1.16.7
| +--- org.jspecify:jspecify:1.0.1
| \--- io.micrometer:micrometer-commons:1.16.7
| \--- org.jspecify:jspecify:1.0.1
+--- org.slf4j:slf4j-api -> 2.0.18
+--- org.slf4j:slf4j-api:{strictly 2.0.18} -> 2.0.18 (c)
+--- org.springframework.boot:spring-boot-autoconfigure:{strictly 4.0.8} -> 4.0.8 (c)
+--- org.springframework.boot:spring-boot:{strictly 4.0.8} -> 4.0.8 (c)
+--- org.springframework:spring-core:{strictly 7.0.9} -> 7.0.9 (c)
+--- org.springframework:spring-context:{strictly 7.0.9} -> 7.0.9 (c)
+--- commons-logging:commons-logging:{strictly 1.3.6} -> 1.3.6 (c)
+--- org.jspecify:jspecify:{strictly 1.0.1} -> 1.0.1 (c)
+--- org.springframework:spring-aop:{strictly 7.0.9} -> 7.0.9 (c)
+--- org.springframework:spring-beans:{strictly 7.0.9} -> 7.0.9 (c)
+--- org.springframework:spring-expression:{strictly 7.0.9} -> 7.0.9 (c)
+--- io.micrometer:micrometer-observation:{strictly 1.16.7} -> 1.16.7 (c)
\--- io.micrometer:micrometer-commons:{strictly 1.16.7} -> 1.16.7 (c)
(c) - A dependency constraint, not a dependency. The dependency affected by the constraint occurs elsewhere in the tree.
(*) - Indicates repeated occurrences of a transitive dependency subtree. Gradle expands transitive dependency subtrees only once per project; repeat occurrences only display the root of the subtree, followed by this annotation.
A web-based, searchable dependency report is available by adding the --scan option.
BUILD SUCCESSFUL in 918ms
9 actionable tasks: 1 executed, 8 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
Exit code: 0
@@ -0,0 +1,22 @@
Command: ./gradlew verifyCleanArchitectureDependencies --console=plain
Working directory: /shared/codebase/clean-architecture-backend-template/src
Executed at: 2026-08-29T07:20:04Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Fresh architecture-registry project-edge verification; it verifies registered Gradle dependency policy, not semantic necessity of every allowed edge.
--- stdout/stderr ---
> Task :build-logic:extractPluginRequests UP-TO-DATE
> Task :build-logic:generatePluginAdapters UP-TO-DATE
> Task :build-logic:compileJava UP-TO-DATE
> Task :build-logic:compileGroovy UP-TO-DATE
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
> Task :build-logic:pluginDescriptors UP-TO-DATE
> Task :build-logic:processResources UP-TO-DATE
> Task :build-logic:classes UP-TO-DATE
> Task :build-logic:jar UP-TO-DATE
> Task :verifyCleanArchitectureDependencies
BUILD SUCCESSFUL in 788ms
9 actionable tasks: 1 executed, 8 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
Exit code: 0
@@ -0,0 +1,40 @@
Command: ./gradlew :adapter:outbound:messaging:test --tests dev.caskeleton.adapter.outbound.messaging.core.OutboundMessagePublisherTest --rerun-tasks --console=plain
Working directory: /shared/codebase/clean-architecture-backend-template/src
Executed at: 2026-08-29T07:20:17Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Fresh execution of the messaging publisher test class that contains the logger-failure-after-send regression; it covers the messaging consumer only.
--- stdout/stderr ---
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :shared-contract:compileJava
> Task :shared-contract:processResources
> Task :shared-contract:classes
> Task :shared-contract:jar
> Task :application-core:compileJava
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes
> Task :application-core:jar
> Task :adapter:outbound:support:compileJava
> Task :adapter:outbound:support:processResources NO-SOURCE
> Task :adapter:outbound:support:classes
> Task :adapter:outbound:support:jar
> Task :adapter:outbound:messaging:compileJava
> Task :adapter:outbound:messaging:processResources
> Task :adapter:outbound:messaging:classes
> Task :adapter:outbound:messaging:compileTestJava
> Task :adapter:outbound:messaging:processTestResources
> Task :adapter:outbound:messaging:testClasses
> Task :adapter:outbound:messaging:test
BUILD SUCCESSFUL in 19s
20 actionable tasks: 20 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
Exit code: 0
@@ -0,0 +1,40 @@
Command: ./gradlew :adapter:outbound:notification:test --tests dev.caskeleton.adapter.outbound.notification.core.NotificationAdapterTest --rerun-tasks --console=plain
Working directory: /shared/codebase/clean-architecture-backend-template/src
Executed at: 2026-08-29T07:20:37Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Fresh execution of the current notification adapter behavior tests; it covers ordinary Logback behavior but contains no throwing-logger regression.
--- stdout/stderr ---
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :shared-contract:compileJava
> Task :shared-contract:processResources
> Task :shared-contract:classes
> Task :shared-contract:jar
> Task :application-core:compileJava
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes
> Task :application-core:jar
> Task :adapter:outbound:support:compileJava
> Task :adapter:outbound:support:processResources NO-SOURCE
> Task :adapter:outbound:support:classes
> Task :adapter:outbound:support:jar
> Task :adapter:outbound:notification:compileJava
> Task :adapter:outbound:notification:processResources NO-SOURCE
> Task :adapter:outbound:notification:classes
> Task :adapter:outbound:notification:compileTestJava
> Task :adapter:outbound:notification:processTestResources
> Task :adapter:outbound:notification:testClasses
> Task :adapter:outbound:notification:test
BUILD SUCCESSFUL in 20s
19 actionable tasks: 19 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
Exit code: 0
@@ -0,0 +1,174 @@
Command: ./gradlew :app-bootstrap:test --tests dev.caskeleton.adapter.outbound.OptionalAdapterBeanGatingTest --rerun-tasks --console=plain
Working directory: /shared/codebase/clean-architecture-backend-template/src
Executed at: 2026-08-29T07:20:57Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Fresh composition-level test using OutboundSupportConfig with optional outbound adapter configurations; its runner supplies selected configs explicitly rather than booting the full application.
--- stdout/stderr ---
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :shared-contract:compileJava
> Task :shared-contract:processResources
> Task :shared-contract:classes
> Task :shared-contract:jar
> Task :application-core:compileJava
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes
> Task :application-core:jar
> Task :domain-core:compileJava
> Task :domain-core:processResources NO-SOURCE
> Task :domain-core:classes
> Task :domain-core:jar
> Task :messaging:messaging-core-api:compileJava
> Task :messaging:messaging-observability:compileJava
> Task :messaging:messaging-schema-api:compileJava
> Task :messaging:messaging-policy:compileJava
> Task :messaging:messaging-reliability-api:compileJava
> Task :messaging:messaging-security:compileJava
> Task :messaging:messaging-admin-api:compileJava
> Task :messaging:messaging-transport-spi:compileJava
> Task :messaging:messaging-admin-runtime:compileJava
> Task :messaging:messaging-claim-check:compileJava
> Task :messaging:messaging-cloudevents:compileJava
> Task :messaging:messaging-inbox-jdbc-postgresql:compileJava
> Task :messaging:messaging-kafka:compileJava
> Task :messaging:messaging-outbox-jdbc-postgresql:compileJava
> Task :messaging:messaging-rabbit:compileJava
> Task :messaging:messaging-runtime-core:compileJava
> Task :messaging:messaging-schema-json:compileJava
> Task :messaging:messaging-spring-boot-starter:compileJava
> Task :adapter:inbound:graphql:compileJava
> Task :adapter:inbound:graphql:processResources
> Task :adapter:inbound:graphql:classes
> Task :adapter:inbound:graphql:jar
> Task :adapter:inbound:web:compileJava
> Task :adapter:inbound:web:processResources
> Task :adapter:inbound:web:classes
> Task :adapter:inbound:web:jar
> Task :adapter:outbound:support:compileJava
> Task :adapter:outbound:support:processResources NO-SOURCE
> Task :adapter:outbound:support:classes
> Task :adapter:outbound:support:jar
> Task :adapter:outbound:cache-redis:compileJava
> Task :adapter:outbound:cache-redis:processResources
> Task :adapter:outbound:cache-redis:classes
> Task :adapter:outbound:cache-redis:jar
> Task :adapter:outbound:fileserver:compileJava
> Task :adapter:outbound:fileserver:processResources NO-SOURCE
> Task :adapter:outbound:fileserver:classes
> Task :adapter:outbound:fileserver:jar
> Task :adapter:outbound:httpclient:compileJava
> Task :adapter:outbound:httpclient:processResources NO-SOURCE
> Task :adapter:outbound:httpclient:classes
> Task :adapter:outbound:httpclient:jar
> Task :adapter:outbound:identifier:compileJava
> Task :adapter:outbound:identifier:compileGroovy NO-SOURCE
> Task :adapter:outbound:identifier:processResources NO-SOURCE
> Task :adapter:outbound:identifier:classes
> Task :adapter:outbound:identifier:jar
> Task :adapter:outbound:messaging:compileJava
> Task :adapter:outbound:messaging:processResources
> Task :adapter:outbound:messaging:classes
> Task :adapter:outbound:messaging:jar
> Task :adapter:outbound:notification:compileJava
> Task :adapter:outbound:notification:processResources NO-SOURCE
> Task :adapter:outbound:notification:classes
> Task :adapter:outbound:notification:jar
> Task :adapter:outbound:persistence-jpa:compileJava
> Task :adapter:outbound:persistence-jpa:processResources
> Task :adapter:outbound:persistence-jpa:classes
> Task :adapter:outbound:persistence-jpa:jar
> Task :adapter:outbound:persistence-mongo:compileJava
> Task :adapter:outbound:persistence-mongo:processResources
> Task :adapter:outbound:persistence-mongo:classes
> Task :adapter:outbound:persistence-mongo:jar
> Task :app-bootstrap:compileJava
> Task :app-bootstrap:processResources
> Task :app-bootstrap:classes
> Task :sample-portfolio:compileJava
> Task :sample-portfolio:processResources
> Task :sample-portfolio:classes
> Task :sample-portfolio:jar
> Task :adapter:inbound:web:compileTestkitJava
> Task :adapter:inbound:web:processTestkitResources
> Task :adapter:inbound:web:testkitClasses
> Task :adapter:inbound:web:testkitJar
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses
> Task :adapter:outbound:persistence-jpa:testkitJar
> Task :app-bootstrap:compileTestJava
> Task :app-bootstrap:runtimeClasspathManifest
> Task :app-bootstrap:processTestResources
> Task :app-bootstrap:testClasses
> Task :messaging:messaging-admin-api:processResources NO-SOURCE
> Task :messaging:messaging-admin-api:classes
> Task :messaging:messaging-admin-api:jar
> Task :messaging:messaging-admin-runtime:processResources NO-SOURCE
> Task :messaging:messaging-admin-runtime:classes
> Task :messaging:messaging-admin-runtime:jar
> Task :messaging:messaging-claim-check:processResources NO-SOURCE
> Task :messaging:messaging-claim-check:classes
> Task :messaging:messaging-claim-check:jar
> Task :messaging:messaging-cloudevents:processResources NO-SOURCE
> Task :messaging:messaging-cloudevents:classes
> Task :messaging:messaging-cloudevents:jar
> Task :messaging:messaging-core-api:processResources NO-SOURCE
> Task :messaging:messaging-core-api:classes
> Task :messaging:messaging-core-api:jar
> Task :messaging:messaging-inbox-jdbc-postgresql:processResources
> Task :messaging:messaging-inbox-jdbc-postgresql:classes
> Task :messaging:messaging-inbox-jdbc-postgresql:jar
> Task :messaging:messaging-kafka:processResources NO-SOURCE
> Task :messaging:messaging-kafka:classes
> Task :messaging:messaging-kafka:jar
> Task :messaging:messaging-observability:processResources NO-SOURCE
> Task :messaging:messaging-observability:classes
> Task :messaging:messaging-observability:jar
> Task :messaging:messaging-outbox-jdbc-postgresql:processResources
> Task :messaging:messaging-outbox-jdbc-postgresql:classes
> Task :messaging:messaging-outbox-jdbc-postgresql:jar
> Task :messaging:messaging-policy:processResources NO-SOURCE
> Task :messaging:messaging-policy:classes
> Task :messaging:messaging-policy:jar
> Task :messaging:messaging-rabbit:processResources NO-SOURCE
> Task :messaging:messaging-rabbit:classes
> Task :messaging:messaging-rabbit:jar
> Task :messaging:messaging-reliability-api:processResources NO-SOURCE
> Task :messaging:messaging-reliability-api:classes
> Task :messaging:messaging-reliability-api:jar
> Task :messaging:messaging-runtime-core:processResources NO-SOURCE
> Task :messaging:messaging-runtime-core:classes
> Task :messaging:messaging-runtime-core:jar
> Task :messaging:messaging-schema-api:processResources NO-SOURCE
> Task :messaging:messaging-schema-api:classes
> Task :messaging:messaging-schema-api:jar
> Task :messaging:messaging-schema-json:processResources NO-SOURCE
> Task :messaging:messaging-schema-json:classes
> Task :messaging:messaging-schema-json:jar
> Task :messaging:messaging-security:processResources NO-SOURCE
> Task :messaging:messaging-security:classes
> Task :messaging:messaging-security:jar
> Task :messaging:messaging-spring-boot-starter:processResources
> Task :messaging:messaging-spring-boot-starter:classes
> Task :messaging:messaging-spring-boot-starter:jar
> Task :messaging:messaging-transport-spi:processResources NO-SOURCE
> Task :messaging:messaging-transport-spi:classes
> Task :messaging:messaging-transport-spi:jar
> Task :adapter:outbound:objectstorage:compileJava
> Task :adapter:outbound:objectstorage:processResources NO-SOURCE
> Task :adapter:outbound:objectstorage:classes
> Task :adapter:outbound:objectstorage:jar
> Task :app-bootstrap:test
BUILD SUCCESSFUL in 1m 32s
98 actionable tasks: 98 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
Exit code: 0
@@ -0,0 +1,174 @@
Command: ./gradlew :app-bootstrap:test --tests dev.caskeleton.bootstrap.architecture.CleanArchitectureTest --rerun-tasks --console=plain
Working directory: /shared/codebase/clean-architecture-backend-template/src
Executed at: 2026-08-29T07:22:45Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Fresh execution of the project architecture suite containing the outbound-adapter peer rule that permits shared dependencies only into ..adapter.outbound.support..; static ArchUnit analysis does not prove runtime activation.
--- stdout/stderr ---
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :shared-contract:compileJava
> Task :shared-contract:processResources
> Task :shared-contract:classes
> Task :shared-contract:jar
> Task :application-core:compileJava
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes
> Task :application-core:jar
> Task :domain-core:compileJava
> Task :domain-core:processResources NO-SOURCE
> Task :domain-core:classes
> Task :domain-core:jar
> Task :messaging:messaging-core-api:compileJava
> Task :messaging:messaging-observability:compileJava
> Task :messaging:messaging-schema-api:compileJava
> Task :messaging:messaging-policy:compileJava
> Task :messaging:messaging-reliability-api:compileJava
> Task :messaging:messaging-security:compileJava
> Task :messaging:messaging-admin-api:compileJava
> Task :messaging:messaging-transport-spi:compileJava
> Task :messaging:messaging-admin-runtime:compileJava
> Task :messaging:messaging-claim-check:compileJava
> Task :messaging:messaging-cloudevents:compileJava
> Task :messaging:messaging-inbox-jdbc-postgresql:compileJava
> Task :messaging:messaging-kafka:compileJava
> Task :messaging:messaging-outbox-jdbc-postgresql:compileJava
> Task :messaging:messaging-rabbit:compileJava
> Task :messaging:messaging-runtime-core:compileJava
> Task :messaging:messaging-schema-json:compileJava
> Task :messaging:messaging-spring-boot-starter:compileJava
> Task :adapter:inbound:graphql:compileJava
> Task :adapter:inbound:graphql:processResources
> Task :adapter:inbound:graphql:classes
> Task :adapter:inbound:graphql:jar
> Task :adapter:inbound:web:compileJava
> Task :adapter:inbound:web:processResources
> Task :adapter:inbound:web:classes
> Task :adapter:inbound:web:jar
> Task :adapter:outbound:support:compileJava
> Task :adapter:outbound:support:processResources NO-SOURCE
> Task :adapter:outbound:support:classes
> Task :adapter:outbound:support:jar
> Task :adapter:outbound:cache-redis:compileJava
> Task :adapter:outbound:cache-redis:processResources
> Task :adapter:outbound:cache-redis:classes
> Task :adapter:outbound:cache-redis:jar
> Task :adapter:outbound:fileserver:compileJava
> Task :adapter:outbound:fileserver:processResources NO-SOURCE
> Task :adapter:outbound:fileserver:classes
> Task :adapter:outbound:fileserver:jar
> Task :adapter:outbound:httpclient:compileJava
> Task :adapter:outbound:httpclient:processResources NO-SOURCE
> Task :adapter:outbound:httpclient:classes
> Task :adapter:outbound:httpclient:jar
> Task :adapter:outbound:identifier:compileJava
> Task :adapter:outbound:identifier:compileGroovy NO-SOURCE
> Task :adapter:outbound:identifier:processResources NO-SOURCE
> Task :adapter:outbound:identifier:classes
> Task :adapter:outbound:identifier:jar
> Task :adapter:outbound:messaging:compileJava
> Task :adapter:outbound:messaging:processResources
> Task :adapter:outbound:messaging:classes
> Task :adapter:outbound:messaging:jar
> Task :adapter:outbound:notification:compileJava
> Task :adapter:outbound:notification:processResources NO-SOURCE
> Task :adapter:outbound:notification:classes
> Task :adapter:outbound:notification:jar
> Task :adapter:outbound:persistence-jpa:compileJava
> Task :adapter:outbound:persistence-jpa:processResources
> Task :adapter:outbound:persistence-jpa:classes
> Task :adapter:outbound:persistence-jpa:jar
> Task :adapter:outbound:persistence-mongo:compileJava
> Task :adapter:outbound:persistence-mongo:processResources
> Task :adapter:outbound:persistence-mongo:classes
> Task :adapter:outbound:persistence-mongo:jar
> Task :app-bootstrap:compileJava
> Task :app-bootstrap:processResources
> Task :app-bootstrap:classes
> Task :sample-portfolio:compileJava
> Task :sample-portfolio:processResources
> Task :sample-portfolio:classes
> Task :sample-portfolio:jar
> Task :adapter:inbound:web:compileTestkitJava
> Task :adapter:inbound:web:processTestkitResources
> Task :adapter:inbound:web:testkitClasses
> Task :adapter:inbound:web:testkitJar
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses
> Task :adapter:outbound:persistence-jpa:testkitJar
> Task :app-bootstrap:compileTestJava
> Task :app-bootstrap:runtimeClasspathManifest
> Task :app-bootstrap:processTestResources
> Task :app-bootstrap:testClasses
> Task :messaging:messaging-admin-api:processResources NO-SOURCE
> Task :messaging:messaging-admin-api:classes
> Task :messaging:messaging-admin-api:jar
> Task :messaging:messaging-admin-runtime:processResources NO-SOURCE
> Task :messaging:messaging-admin-runtime:classes
> Task :messaging:messaging-admin-runtime:jar
> Task :messaging:messaging-claim-check:processResources NO-SOURCE
> Task :messaging:messaging-claim-check:classes
> Task :messaging:messaging-claim-check:jar
> Task :messaging:messaging-cloudevents:processResources NO-SOURCE
> Task :messaging:messaging-cloudevents:classes
> Task :messaging:messaging-cloudevents:jar
> Task :messaging:messaging-core-api:processResources NO-SOURCE
> Task :messaging:messaging-core-api:classes
> Task :messaging:messaging-core-api:jar
> Task :messaging:messaging-inbox-jdbc-postgresql:processResources
> Task :messaging:messaging-inbox-jdbc-postgresql:classes
> Task :messaging:messaging-inbox-jdbc-postgresql:jar
> Task :messaging:messaging-kafka:processResources NO-SOURCE
> Task :messaging:messaging-kafka:classes
> Task :messaging:messaging-kafka:jar
> Task :messaging:messaging-observability:processResources NO-SOURCE
> Task :messaging:messaging-observability:classes
> Task :messaging:messaging-observability:jar
> Task :messaging:messaging-outbox-jdbc-postgresql:processResources
> Task :messaging:messaging-outbox-jdbc-postgresql:classes
> Task :messaging:messaging-outbox-jdbc-postgresql:jar
> Task :messaging:messaging-policy:processResources NO-SOURCE
> Task :messaging:messaging-policy:classes
> Task :messaging:messaging-policy:jar
> Task :messaging:messaging-rabbit:processResources NO-SOURCE
> Task :messaging:messaging-rabbit:classes
> Task :messaging:messaging-rabbit:jar
> Task :messaging:messaging-reliability-api:processResources NO-SOURCE
> Task :messaging:messaging-reliability-api:classes
> Task :messaging:messaging-reliability-api:jar
> Task :messaging:messaging-runtime-core:processResources NO-SOURCE
> Task :messaging:messaging-runtime-core:classes
> Task :messaging:messaging-runtime-core:jar
> Task :messaging:messaging-schema-api:processResources NO-SOURCE
> Task :messaging:messaging-schema-api:classes
> Task :messaging:messaging-schema-api:jar
> Task :messaging:messaging-schema-json:processResources NO-SOURCE
> Task :messaging:messaging-schema-json:classes
> Task :messaging:messaging-schema-json:jar
> Task :messaging:messaging-security:processResources NO-SOURCE
> Task :messaging:messaging-security:classes
> Task :messaging:messaging-security:jar
> Task :messaging:messaging-spring-boot-starter:processResources
> Task :messaging:messaging-spring-boot-starter:classes
> Task :messaging:messaging-spring-boot-starter:jar
> Task :messaging:messaging-transport-spi:processResources NO-SOURCE
> Task :messaging:messaging-transport-spi:classes
> Task :messaging:messaging-transport-spi:jar
> Task :adapter:outbound:objectstorage:compileJava
> Task :adapter:outbound:objectstorage:processResources NO-SOURCE
> Task :adapter:outbound:objectstorage:classes
> Task :adapter:outbound:objectstorage:jar
> Task :app-bootstrap:test
BUILD SUCCESSFUL in 1m 45s
98 actionable tasks: 98 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
Exit code: 0
@@ -0,0 +1,8 @@
Command: test -d /home/donghyeon/workspace/ai-tool/llm-wiki-private && find /home/donghyeon/workspace/ai-tool/llm-wiki-private -maxdepth 1 -type f -print
Working directory: /
Executed at: 2026-08-29T07:27:30Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Checks only whether the repository-mandated local LLM Wiki vault exists in this remote host at the exact configured path; it does not search for alternate vault locations.
--- stdout/stderr ---
Exit code: 1
@@ -0,0 +1,33 @@
Command: python3 - <<'PY'
from pathlib import Path
import subprocess, collections
root='src/adapter/outbound/persistence-jpa'
files=subprocess.check_output(['git','ls-files',root],text=True).splitlines()
print('tracked_total=',len(files))
for sr in sorted((Path(root)/'src').iterdir()):
if sr.is_dir():
owned=[p for p in sr.rglob('*') if p.is_file() and 'build/' not in str(p)]
java=[p for p in owned if p.suffix=='.java']
print(sr.name,'files=',len(owned),'java=',len(java),'nonjava=',len(owned)-len(java),'java_loc=',sum(len(p.read_text(errors='ignore').splitlines()) for p in java))
print('top_level_files=')
for p in sorted(Path(root).iterdir()):
if p.is_file(): print(p)
PY
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T07:48:05Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Enumerates the tracked persistence-jpa leaf and its source-root/package counts at the recorded revision; the sub-scope labels are deterministic path buckets and do not themselves assert behavioral findings.
--- stdout/stderr ---
tracked_total= 605
jpaPlatformPerformanceTest files= 3 java= 3 nonjava= 0 java_loc= 268
main files= 381 java= 350 nonjava= 31 java_loc= 27744
postgresqlIntegrationTest files= 75 java= 71 nonjava= 4 java_loc= 13958
test files= 101 java= 100 nonjava= 1 java_loc= 9511
testkit files= 41 java= 41 nonjava= 0 java_loc= 3070
top_level_files=
src/adapter/outbound/persistence-jpa/CLAUDE.md
src/adapter/outbound/persistence-jpa/README.md
src/adapter/outbound/persistence-jpa/build.gradle
src/adapter/outbound/persistence-jpa/gradle.lockfile
Exit code: 0
@@ -0,0 +1,309 @@
Command: python3 - <<'PY'
from pathlib import Path
import re, subprocess
api=Path('src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api')
alljava=[Path(p) for p in subprocess.check_output(['git','ls-files'],text=True).splitlines() if '/src/main/java/' in p and p.endswith('.java')]
zeros=[]
for src in sorted(api.rglob('*.java')):
t=src.read_text()
m=re.search(r'public\s+(?:final\s+)?(?:record|class|interface|enum)\s+(\w+)',t)
if not m: continue
name=m.group(1); pkg=re.search(r'package\s+([^;]+);',t).group(1); fqn=pkg+'.'+name
refs=[]
for p in alljava:
if p==src: continue
txt=p.read_text(errors='ignore')
pp=re.search(r'package\s+([^;]+);',txt)
same=bool(pp and pp.group(1)==pkg and re.search(r'\b'+re.escape(name)+r'\b',txt))
imported=(f'import {fqn};' in txt) or (fqn in txt) or (f'import {pkg}.*;' in txt and bool(re.search(r'\b'+re.escape(name)+r'\b',txt)))
if same or imported: refs.append(str(p))
print(f'{fqn}|production_reference_files={len(refs)}')
for r in refs: print(' '+r)
if not refs: zeros.append(fqn)
print('ZERO_REFERENCE_TYPES=',len(zeros))
for x in zeros: print('ZERO '+x)
PY
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T07:48:05Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Conservatively searches current tracked production Java for references to each public top-level persistence API type, using exact imports/FQNs plus same-package simple-name references. Javadoc/simple-name references in the same package can over-count; reflection, generated code, and external adopters are outside this repository search.
--- stdout/stderr ---
dev.caskeleton.adapter.outbound.persistence.api.PersistenceOperationName|production_reference_files=21
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/capability/JpaCapability.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/JpaFailureContext.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/VendorFailureTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/JpaTransactionExecutor.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/RetryEventListener.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/FlywayValidationGate.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaRetryObservation.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaTransactionObservation.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/json/JsonDocumentCodec.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/LockWaitObservation.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlLockExceptionTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecord.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/OptimisticConflictTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/PersistenceFailureTranslatorChain.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceContext.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceFrame.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/UnknownOperation.java
dev.caskeleton.adapter.outbound.persistence.api.capability.CapabilitySupport|production_reference_files=2
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java
dev.caskeleton.adapter.outbound.persistence.api.capability.JpaCapability|production_reference_files=2
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/capability/CapabilitySupport.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java
dev.caskeleton.adapter.outbound.persistence.api.capability.SupportLevel|production_reference_files=3
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/capability/CapabilitySupport.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/capability/JpaCapability.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/experimental/next/CompatibilityLane.java
dev.caskeleton.adapter.outbound.persistence.api.error.CheckConstraintViolationException|production_reference_files=1
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java
dev.caskeleton.adapter.outbound.persistence.api.error.ConnectionUnavailableException|production_reference_files=1
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
dev.caskeleton.adapter.outbound.persistence.api.error.ConstraintCode|production_reference_files=4
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ConstraintViolationDetails.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintCatalog.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/ConstraintCatalog.java
dev.caskeleton.adapter.outbound.persistence.api.error.ConstraintViolationDetails|production_reference_files=8
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/CheckConstraintViolationException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ConstraintCode.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ForeignKeyViolationException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/NotNullConstraintViolationException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/UniqueConstraintViolationException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintCatalog.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
dev.caskeleton.adapter.outbound.persistence.api.error.DataCorruptionException|production_reference_files=2
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/json/JsonDocumentCodec.java
dev.caskeleton.adapter.outbound.persistence.api.error.DeadlockDetectedException|production_reference_files=3
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/PessimisticLockTimeoutException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlLockExceptionTranslator.java
dev.caskeleton.adapter.outbound.persistence.api.error.FailureCategory|production_reference_files=22
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/CheckConstraintViolationException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ConnectionUnavailableException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/DataCorruptionException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/DeadlockDetectedException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ForeignKeyViolationException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/JpaEntityNotFoundException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/JpaPersistenceException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/NotNullConstraintViolationException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/OptimisticConflictException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/PessimisticLockTimeoutException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/QueryTimeoutException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/SchemaMismatchException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/SerializationFailureException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/TransactionCompletionUnknownException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/TransactionTimeoutException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/UniqueConstraintViolationException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/RetryProfile.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/MicrometerQueryObservation.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlFailureClassifier.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicy.java
dev.caskeleton.adapter.outbound.persistence.api.error.ForeignKeyViolationException|production_reference_files=1
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java
dev.caskeleton.adapter.outbound.persistence.api.error.JpaEntityNotFoundException|production_reference_files=0
dev.caskeleton.adapter.outbound.persistence.api.error.JpaFailureContext|production_reference_files=24
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/CheckConstraintViolationException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ConnectionUnavailableException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ConstraintViolationDetails.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/DataCorruptionException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/DeadlockDetectedException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ForeignKeyViolationException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/JpaEntityNotFoundException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/JpaPersistenceException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/NotNullConstraintViolationException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/OptimisticConflictException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/PessimisticLockTimeoutException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/QueryTimeoutException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/SchemaMismatchException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/SerializationFailureException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/TransactionCompletionUnknownException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/TransactionTimeoutException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/UniqueConstraintViolationException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/FlywayValidationGate.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/json/JsonDocumentCodec.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlLockExceptionTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/OptimisticConflictTranslator.java
dev.caskeleton.adapter.outbound.persistence.api.error.JpaPersistenceException|production_reference_files=27
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/CheckConstraintViolationException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ConnectionUnavailableException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/DataCorruptionException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/DeadlockDetectedException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ForeignKeyViolationException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/JpaEntityNotFoundException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/NotNullConstraintViolationException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/OptimisticConflictException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/PessimisticLockTimeoutException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/QueryTimeoutException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/SchemaMismatchException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/SerializationFailureException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/TransactionCompletionUnknownException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/TransactionTimeoutException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/UniqueConstraintViolationException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/VendorFailureTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/JpaRetryPolicy.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/RetryEventListener.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaRetryObservation.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaTransactionObservation.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/MicrometerQueryObservation.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlLockExceptionTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicy.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/PersistenceFailureTranslatorChain.java
dev.caskeleton.adapter.outbound.persistence.api.error.NotNullConstraintViolationException|production_reference_files=1
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java
dev.caskeleton.adapter.outbound.persistence.api.error.OptimisticConflictException|production_reference_files=3
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/OptimisticConflictTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/PersistenceFailureTranslatorChain.java
dev.caskeleton.adapter.outbound.persistence.api.error.PessimisticLockTimeoutException|production_reference_files=2
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlLockExceptionTranslator.java
dev.caskeleton.adapter.outbound.persistence.api.error.QueryTimeoutException|production_reference_files=1
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
dev.caskeleton.adapter.outbound.persistence.api.error.SchemaMismatchException|production_reference_files=2
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/FlywayValidationGate.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
dev.caskeleton.adapter.outbound.persistence.api.error.SerializationFailureException|production_reference_files=1
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
dev.caskeleton.adapter.outbound.persistence.api.error.SqlExceptionSqlStateResolver|production_reference_files=3
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlLockExceptionTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
dev.caskeleton.adapter.outbound.persistence.api.error.SqlStateResolver|production_reference_files=4
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/SqlExceptionSqlStateResolver.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlLockExceptionTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
dev.caskeleton.adapter.outbound.persistence.api.error.TransactionCompletionUnknownException|production_reference_files=6
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/ConnectionUnavailableException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/TransactionCompletionEvidence.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecord.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecorder.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicy.java
dev.caskeleton.adapter.outbound.persistence.api.error.TransactionTimeoutException|production_reference_files=1
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
dev.caskeleton.adapter.outbound.persistence.api.error.UniqueConstraintViolationException|production_reference_files=1
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java
dev.caskeleton.adapter.outbound.persistence.api.error.VendorFailureTranslator|production_reference_files=5
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlPersistenceConfig.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/PersistenceFailureTranslatorChain.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
dev.caskeleton.adapter.outbound.persistence.api.query.CursorCodec|production_reference_files=1
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/SignedJsonCursorCodec.java
dev.caskeleton.adapter.outbound.persistence.api.query.CursorPayloadCodec|production_reference_files=1
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/SignedJsonCursorCodec.java
dev.caskeleton.adapter.outbound.persistence.api.query.KeysetPageRequest|production_reference_files=1
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaKeysetQuerySupport.java
dev.caskeleton.adapter.outbound.persistence.api.query.KeysetSlice|production_reference_files=2
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaKeysetQuerySupport.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/KeysetSliceAssembler.java
dev.caskeleton.adapter.outbound.persistence.api.query.NoopQueryObservation|production_reference_files=1
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaObservabilityAutoConfiguration.java
dev.caskeleton.adapter.outbound.persistence.api.query.QueryName|production_reference_files=11
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/NoopQueryObservation.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/QueryObservation.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/NamedStatementInspector.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/QueryNameContext.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/bulk/HibernateBulkDmlExecutor.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/MicrometerQueryObservation.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/querydsl/QuerydslJpaSupport.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaKeysetQuerySupport.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaRepositoryFragmentSupport.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaStreamExecutor.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/RegisteredQuery.java
dev.caskeleton.adapter.outbound.persistence.api.query.QueryObservation|production_reference_files=5
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/NoopQueryObservation.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/bulk/HibernateBulkDmlExecutor.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/MicrometerQueryObservation.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaStreamExecutor.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaObservabilityAutoConfiguration.java
dev.caskeleton.adapter.outbound.persistence.api.query.QueryScope|production_reference_files=5
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/NoopQueryObservation.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/QueryObservation.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/bulk/HibernateBulkDmlExecutor.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/MicrometerQueryObservation.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaStreamExecutor.java
dev.caskeleton.adapter.outbound.persistence.api.query.SignedJsonCursorCodec|production_reference_files=1
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/CursorPayloadCodec.java
dev.caskeleton.adapter.outbound.persistence.api.query.SortDirection|production_reference_files=2
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/KeysetPageRequest.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/KeysetTerm.java
dev.caskeleton.adapter.outbound.persistence.api.transaction.IsolationLevel|production_reference_files=2
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/TransactionProfile.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDefinitionMapper.java
dev.caskeleton.adapter.outbound.persistence.api.transaction.JitterMode|production_reference_files=2
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/RetryProfile.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/BackoffCalculator.java
dev.caskeleton.adapter.outbound.persistence.api.transaction.JpaRetryPolicy|production_reference_files=4
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/RetryProfile.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicy.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
dev.caskeleton.adapter.outbound.persistence.api.transaction.JpaTransactionExecutor|production_reference_files=1
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java
dev.caskeleton.adapter.outbound.persistence.api.transaction.PropagationMode|production_reference_files=2
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/TransactionProfile.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDefinitionMapper.java
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryDecision|production_reference_files=5
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/JpaRetryPolicy.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/RetryEventListener.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaRetryObservation.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicy.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryDisposition|production_reference_files=2
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/RetryDecision.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryEventListener|production_reference_files=5
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaRetryObservation.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaObservabilityAutoConfiguration.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryProfile|production_reference_files=6
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/TransactionProfile.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/BackoffCalculator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicy.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/RetryBudget.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionAttempt|production_reference_files=5
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/JpaRetryPolicy.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/RetryEventListener.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaRetryObservation.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicy.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionCompletionEvidence|production_reference_files=6
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/TransactionCompletionUnknownException.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecord.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceContext.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceFrame.java
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionProfile|production_reference_files=5
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/JpaTransactionExecutor.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDefinitionMapper.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistry.java
ZERO_REFERENCE_TYPES= 1
ZERO dev.caskeleton.adapter.outbound.persistence.api.error.JpaEntityNotFoundException
Exit code: 0
@@ -0,0 +1,81 @@
Command: printf '%s\n' '--- framework annotations under persistence api ---'; git grep -n -E '@(Component|Configuration|Bean|Conditional|AutoConfiguration|Repository|Entity|Service)' -- 'src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/**/*.java' 'src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/*.java' || true; printf '%s\n' '--- api baseline header/count ---'; sed -n '1,8p' docs/architecture/jpa-api-surface.txt; grep -c 'dev.caskeleton.adapter.outbound.persistence.api' docs/architecture/jpa-api-surface.txt; printf '%s\n' '--- capability enum/composition counts ---'; python3 - <<'PY'
from pathlib import Path
import re
p=Path('src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/capability/JpaCapability.java').read_text()
inside=p.split('public enum JpaCapability {',1)[1].split('private final String id;',1)[0]
print('enum=',len(re.findall(r'^\s*[A-Z][A-Z0-9_]*\("',inside,re.M)))
q=Path('src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java').read_text()
print('composition=',q.count('CapabilitySupport.'))
PY
printf '%s\n' '--- capability report/endpoint path ---'; git grep -n -E 'CapabilitySupport|JpaPlatformReport|JpaPlatformEndpoint|@Endpoint\(id = "jpaplatform"\)' -- 'src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/*.java'; printf '%s\n' '--- retry reason accessor uses in persistence/app-bootstrap ---'; git grep -n '\.reason()' -- 'src/adapter/outbound/persistence-jpa/**' 'src/app-bootstrap/**' || true; printf '%s\n' '--- same simple-name mechanisms elsewhere ---'; git grep -n -E 'class RetryDecision|record RetryDecision|interface CursorCodec|enum SortDirection|enum FailureCategory' -- 'src/adapter/outbound/**/src/main/java/**/*.java' | head -220
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T07:48:10Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Collects the API package's framework-annotation absence, current public API baseline/count, capability composition count, same-name mechanisms in other bounded contexts, actuator report wiring, and exact retry-reason uses. Same-name classes in other adapters are not asserted to be semantic duplicates by this evidence.
--- stdout/stderr ---
--- framework annotations under persistence api ---
--- api baseline header/count ---
# JPA persistence leaf public API surface — every public top-level type in src/main/java.
# A public type in a single-jar leaf is reachable from every adopter's code, so
# additions are reviewed rather than discovered. `api` is the intended external
# surface; the rest is implementation that has not been moved under an internal
# root yet.
# Update only after review with:
# ./gradlew :adapter:outbound:persistence-jpa:updateJpaApiSurface -PapproveJpaApiSurfaceChange
# types: 338
49
--- capability enum/composition counts ---
enum= 16
composition= 16
--- capability report/endpoint path ---
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:3:import dev.caskeleton.adapter.outbound.persistence.api.capability.CapabilitySupport;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:54: public List<CapabilitySupport> capabilities() {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:56: CapabilitySupport.stable(JpaCapability.TRANSACTION_RETRY),
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:57: CapabilitySupport.stable(JpaCapability.COMPLETION_EVIDENCE),
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:58: CapabilitySupport.stable(JpaCapability.KEYSET_PAGINATION),
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:59: CapabilitySupport.stable(JpaCapability.BATCH),
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:60: CapabilitySupport.stable(JpaCapability.SCHEMA_GATE),
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:61: CapabilitySupport.stable(JpaCapability.RUNTIME_ROLE_VERIFICATION),
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:62: CapabilitySupport.stable(JpaCapability.OBSERVABILITY),
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:63: CapabilitySupport.advanced(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:65: CapabilitySupport.advanced(JpaCapability.POSTGRESQL_WORK_CLAIM, "requires a named queue"),
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:66: CapabilitySupport.advanced(JpaCapability.POSTGRESQL_JSONB, "requires a registered path"),
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:67: CapabilitySupport.advanced(JpaCapability.POSTGRESQL_ARRAY_RANGE, "requires a JDBC type"),
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:68: CapabilitySupport.advanced(JpaCapability.BULK_DML, "requires a registered operation"),
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:69: CapabilitySupport.advanced(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:71: CapabilitySupport.advanced(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:73: CapabilitySupport.advanced(JpaCapability.L2_CACHE, "requires an enrolled region catalog"),
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:74: CapabilitySupport.advanced(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:84: public JpaPlatformReport report(DataSource dataSource, String schemaVersion) {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:100: return JpaPlatformReport.sanitized(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformEndpoint.java:20:@Endpoint(id = "jpaplatform")
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformEndpoint.java:21:public class JpaPlatformEndpoint {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformEndpoint.java:23: private final Supplier<JpaPlatformReport> reportSupplier;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformEndpoint.java:25: public JpaPlatformEndpoint(Supplier<JpaPlatformReport> reportSupplier) {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformEndpoint.java:31: public JpaPlatformReport platform() {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:3:import dev.caskeleton.adapter.outbound.persistence.api.capability.CapabilitySupport;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:19:public record JpaPlatformReport(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:26: List<CapabilitySupport> capabilities) {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:28: public JpaPlatformReport {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:42: public static JpaPlatformReport sanitized(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:49: List<CapabilitySupport> capabilities) {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:50: return new JpaPlatformReport(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:122: public Supplier<JpaPlatformReport> jpaPlatformReportSupplier(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:138: public JpaPlatformEndpoint jpaPlatformEndpoint(Supplier<JpaPlatformReport> reportSupplier) {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:139: return new JpaPlatformEndpoint(reportSupplier);
--- retry reason accessor uses in persistence/app-bootstrap ---
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaSuppressionStore.java:51: entry.tenantId().value(), entry.normalizedTargetHmac(), entry.reason().name())
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaSuppressionStore.java:55: entry.reason().name(),
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaSuppressionStore.java:83: entry.reason().name(),
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaSuppressionStore.java:97: SuppressionReason.valueOf(entity.reason()),
src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/platform/experimental/ReadAfterWriteRoutingContractTest.java:93: assertThat(decision.reason()).isNotBlank();
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicyTest.java:86: assertThat(decision.reason()).contains("irreversible");
--- same simple-name mechanisms elsewhere ---
src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/operations/SortDirection.java:9:public enum SortDirection {
src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/api/operation/FailureCategory.java:9:public enum FailureCategory {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/error/FailureCategory.java:11:public enum FailureCategory {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/CursorCodec.java:10:public interface CursorCodec<C> {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/SortDirection.java:9:public enum SortDirection {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/RetryDecision.java:12:public record RetryDecision(RetryDisposition disposition, Duration delay, String reason) {
Exit code: 0
@@ -0,0 +1,126 @@
Command: printf '%s\n' '--- baseline API entries ---'; grep 'dev.caskeleton.adapter.outbound.persistence.api' docs/architecture/jpa-api-surface.txt; printf '%s\n' '--- current API declarations ---'; python3 - <<'PY'
from pathlib import Path
import re
root=Path('src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api')
for p in sorted(root.rglob('*.java')):
t=p.read_text(); m=re.search(r'public\s+(?:final\s+)?(?:record|class|interface|enum)\s+(\w+)',t)
if m:
pkg=re.search(r'package\s+([^;]+);',t).group(1); print(pkg+'.'+m.group(1))
PY
printf '%s\n' '--- intended external statement ---'; sed -n '1,8p' docs/architecture/jpa-api-surface.txt
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T07:48:10Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Compares the intended external api surface baseline with current public top-level api declarations and records named API/design references; it does not evaluate behavioral correctness of those types.
--- stdout/stderr ---
--- baseline API entries ---
dev.caskeleton.adapter.outbound.persistence.api.PersistenceOperationName
dev.caskeleton.adapter.outbound.persistence.api.capability.CapabilitySupport
dev.caskeleton.adapter.outbound.persistence.api.capability.JpaCapability
dev.caskeleton.adapter.outbound.persistence.api.capability.SupportLevel
dev.caskeleton.adapter.outbound.persistence.api.error.CheckConstraintViolationException
dev.caskeleton.adapter.outbound.persistence.api.error.ConnectionUnavailableException
dev.caskeleton.adapter.outbound.persistence.api.error.ConstraintCode
dev.caskeleton.adapter.outbound.persistence.api.error.ConstraintViolationDetails
dev.caskeleton.adapter.outbound.persistence.api.error.DataCorruptionException
dev.caskeleton.adapter.outbound.persistence.api.error.DeadlockDetectedException
dev.caskeleton.adapter.outbound.persistence.api.error.FailureCategory
dev.caskeleton.adapter.outbound.persistence.api.error.ForeignKeyViolationException
dev.caskeleton.adapter.outbound.persistence.api.error.JpaEntityNotFoundException
dev.caskeleton.adapter.outbound.persistence.api.error.JpaFailureContext
dev.caskeleton.adapter.outbound.persistence.api.error.JpaPersistenceException
dev.caskeleton.adapter.outbound.persistence.api.error.NotNullConstraintViolationException
dev.caskeleton.adapter.outbound.persistence.api.error.OptimisticConflictException
dev.caskeleton.adapter.outbound.persistence.api.error.PessimisticLockTimeoutException
dev.caskeleton.adapter.outbound.persistence.api.error.QueryTimeoutException
dev.caskeleton.adapter.outbound.persistence.api.error.SchemaMismatchException
dev.caskeleton.adapter.outbound.persistence.api.error.SerializationFailureException
dev.caskeleton.adapter.outbound.persistence.api.error.SqlExceptionSqlStateResolver
dev.caskeleton.adapter.outbound.persistence.api.error.SqlStateResolver
dev.caskeleton.adapter.outbound.persistence.api.error.TransactionCompletionUnknownException
dev.caskeleton.adapter.outbound.persistence.api.error.TransactionTimeoutException
dev.caskeleton.adapter.outbound.persistence.api.error.UniqueConstraintViolationException
dev.caskeleton.adapter.outbound.persistence.api.error.VendorFailureTranslator
dev.caskeleton.adapter.outbound.persistence.api.query.CursorCodec
dev.caskeleton.adapter.outbound.persistence.api.query.CursorPayloadCodec
dev.caskeleton.adapter.outbound.persistence.api.query.KeysetPageRequest
dev.caskeleton.adapter.outbound.persistence.api.query.KeysetSlice
dev.caskeleton.adapter.outbound.persistence.api.query.NoopQueryObservation
dev.caskeleton.adapter.outbound.persistence.api.query.QueryName
dev.caskeleton.adapter.outbound.persistence.api.query.QueryObservation
dev.caskeleton.adapter.outbound.persistence.api.query.QueryScope
dev.caskeleton.adapter.outbound.persistence.api.query.SignedJsonCursorCodec
dev.caskeleton.adapter.outbound.persistence.api.query.SortDirection
dev.caskeleton.adapter.outbound.persistence.api.transaction.IsolationLevel
dev.caskeleton.adapter.outbound.persistence.api.transaction.JitterMode
dev.caskeleton.adapter.outbound.persistence.api.transaction.JpaRetryPolicy
dev.caskeleton.adapter.outbound.persistence.api.transaction.JpaTransactionExecutor
dev.caskeleton.adapter.outbound.persistence.api.transaction.PropagationMode
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryDecision
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryDisposition
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryEventListener
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryProfile
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionAttempt
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionCompletionEvidence
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionProfile
--- current API declarations ---
dev.caskeleton.adapter.outbound.persistence.api.PersistenceOperationName
dev.caskeleton.adapter.outbound.persistence.api.capability.CapabilitySupport
dev.caskeleton.adapter.outbound.persistence.api.capability.JpaCapability
dev.caskeleton.adapter.outbound.persistence.api.capability.SupportLevel
dev.caskeleton.adapter.outbound.persistence.api.error.CheckConstraintViolationException
dev.caskeleton.adapter.outbound.persistence.api.error.ConnectionUnavailableException
dev.caskeleton.adapter.outbound.persistence.api.error.ConstraintCode
dev.caskeleton.adapter.outbound.persistence.api.error.ConstraintViolationDetails
dev.caskeleton.adapter.outbound.persistence.api.error.DataCorruptionException
dev.caskeleton.adapter.outbound.persistence.api.error.DeadlockDetectedException
dev.caskeleton.adapter.outbound.persistence.api.error.FailureCategory
dev.caskeleton.adapter.outbound.persistence.api.error.ForeignKeyViolationException
dev.caskeleton.adapter.outbound.persistence.api.error.JpaEntityNotFoundException
dev.caskeleton.adapter.outbound.persistence.api.error.JpaFailureContext
dev.caskeleton.adapter.outbound.persistence.api.error.JpaPersistenceException
dev.caskeleton.adapter.outbound.persistence.api.error.NotNullConstraintViolationException
dev.caskeleton.adapter.outbound.persistence.api.error.OptimisticConflictException
dev.caskeleton.adapter.outbound.persistence.api.error.PessimisticLockTimeoutException
dev.caskeleton.adapter.outbound.persistence.api.error.QueryTimeoutException
dev.caskeleton.adapter.outbound.persistence.api.error.SchemaMismatchException
dev.caskeleton.adapter.outbound.persistence.api.error.SerializationFailureException
dev.caskeleton.adapter.outbound.persistence.api.error.SqlExceptionSqlStateResolver
dev.caskeleton.adapter.outbound.persistence.api.error.SqlStateResolver
dev.caskeleton.adapter.outbound.persistence.api.error.TransactionCompletionUnknownException
dev.caskeleton.adapter.outbound.persistence.api.error.TransactionTimeoutException
dev.caskeleton.adapter.outbound.persistence.api.error.UniqueConstraintViolationException
dev.caskeleton.adapter.outbound.persistence.api.error.VendorFailureTranslator
dev.caskeleton.adapter.outbound.persistence.api.query.CursorCodec
dev.caskeleton.adapter.outbound.persistence.api.query.CursorPayloadCodec
dev.caskeleton.adapter.outbound.persistence.api.query.KeysetPageRequest
dev.caskeleton.adapter.outbound.persistence.api.query.KeysetSlice
dev.caskeleton.adapter.outbound.persistence.api.query.NoopQueryObservation
dev.caskeleton.adapter.outbound.persistence.api.query.QueryName
dev.caskeleton.adapter.outbound.persistence.api.query.QueryObservation
dev.caskeleton.adapter.outbound.persistence.api.query.QueryScope
dev.caskeleton.adapter.outbound.persistence.api.query.SignedJsonCursorCodec
dev.caskeleton.adapter.outbound.persistence.api.query.SortDirection
dev.caskeleton.adapter.outbound.persistence.api.transaction.IsolationLevel
dev.caskeleton.adapter.outbound.persistence.api.transaction.JitterMode
dev.caskeleton.adapter.outbound.persistence.api.transaction.JpaRetryPolicy
dev.caskeleton.adapter.outbound.persistence.api.transaction.JpaTransactionExecutor
dev.caskeleton.adapter.outbound.persistence.api.transaction.PropagationMode
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryDecision
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryDisposition
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryEventListener
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryProfile
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionAttempt
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionCompletionEvidence
dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionProfile
--- intended external statement ---
# JPA persistence leaf public API surface — every public top-level type in src/main/java.
# A public type in a single-jar leaf is reachable from every adopter's code, so
# additions are reviewed rather than discovered. `api` is the intended external
# surface; the rest is implementation that has not been moved under an internal
# root yet.
# Update only after review with:
# ./gradlew :adapter:outbound:persistence-jpa:updateJpaApiSurface -PapproveJpaApiSurfaceChange
# types: 338
Exit code: 0
@@ -0,0 +1,13 @@
Command: java --class-path src/adapter/outbound/persistence-jpa/build/classes/java/main /shared/document-detail/clean-architecture-backend-template/evidence/raw/035a-jpa-cursor-boundary-probe.java
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T07:48:27Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Runs the current compiled SignedJsonCursorCodec across payload sizes immediately below/at/above its declared 2048-byte payload bound. It isolates encode/decode self-round-trip behavior and does not exercise HTTP transport or an application-specific JSON mapper.
--- stdout/stderr ---
size=2045 tokenChars=2774 encode=OK decode=OK decodedBytes=2045
size=2046 tokenChars=2775 encode=OK decode=IllegalArgumentException:cursor payload exceeds the maximum size
size=2047 tokenChars=2777 encode=OK decode=IllegalArgumentException:cursor payload exceeds the maximum size
size=2048 tokenChars=2778 encode=OK decode=IllegalArgumentException:cursor payload exceeds the maximum size
size=2049 encode=IllegalStateException:cursor payload is 2049 bytes; the bound is 2048
Exit code: 0
@@ -0,0 +1,30 @@
import dev.caskeleton.adapter.outbound.persistence.api.query.CursorPayloadCodec;
import dev.caskeleton.adapter.outbound.persistence.api.query.SignedJsonCursorCodec;
import java.nio.charset.StandardCharsets;
public class JpaCursorBoundaryProbe {
static SignedJsonCursorCodec<String> codecFor(int payloadBytes) {
return new SignedJsonCursorCodec<>(new CursorPayloadCodec<>() {
public String toJson(String cursor) { return "x".repeat(payloadBytes); }
public String fromJson(String json) { return json; }
}, "k".repeat(32).getBytes(StandardCharsets.UTF_8));
}
public static void main(String[] args) {
for (int size : new int[]{2045, 2046, 2047, 2048, 2049}) {
var codec = codecFor(size);
try {
String token = codec.encode("ignored");
try {
String decoded = codec.decode(token);
System.out.printf("size=%d tokenChars=%d encode=OK decode=OK decodedBytes=%d%n",
size, token.length(), decoded.getBytes(StandardCharsets.UTF_8).length);
} catch (RuntimeException ex) {
System.out.printf("size=%d tokenChars=%d encode=OK decode=%s:%s%n",
size, token.length(), ex.getClass().getSimpleName(), ex.getMessage());
}
} catch (RuntimeException ex) {
System.out.printf("size=%d encode=%s:%s%n", size, ex.getClass().getSimpleName(), ex.getMessage());
}
}
}
}
@@ -0,0 +1,11 @@
Command: java --class-path src/adapter/outbound/persistence-jpa/build/classes/java/main /shared/document-detail/clean-architecture-backend-template/evidence/raw/036a-jpa-api-bound-probe.java
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T07:48:27Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Constructs current API value types with deliberately large diagnostic strings and a very large attempt count to record constructor-enforced bounds only. It does not assert that current app-bootstrap composition supplies such values.
--- stdout/stderr ---
constraintLength=100000
retryDecisionReasonLength=100000
retryProfileMaxAttempts=2147483647
Exit code: 0
@@ -0,0 +1,16 @@
import dev.caskeleton.adapter.outbound.persistence.api.capability.*;
import dev.caskeleton.adapter.outbound.persistence.api.transaction.*;
import java.time.Duration;
import java.util.List;
public class JpaApiBoundProbe {
public static void main(String[] args) {
String large = "x".repeat(100_000);
CapabilitySupport support = new CapabilitySupport(JpaCapability.OBSERVABILITY, SupportLevel.ADVANCED, List.of(large));
RetryDecision decision = RetryDecision.fail(large);
RetryProfile profile = new RetryProfile("retry.profile", Integer.MAX_VALUE, Duration.ZERO, Duration.ZERO, 1.0d, JitterMode.NONE, java.util.Set.of());
System.out.println("constraintLength=" + support.constraints().get(0).length());
System.out.println("retryDecisionReasonLength=" + decision.reason().length());
System.out.println("retryProfileMaxAttempts=" + profile.maxAttempts());
}
}
@@ -0,0 +1,39 @@
Command: ./gradlew :adapter:outbound:persistence-jpa:test --tests dev.caskeleton.adapter.outbound.persistence.api.PersistenceOperationNameTest --tests dev.caskeleton.adapter.outbound.persistence.api.error.JpaFailureContextTest --tests dev.caskeleton.adapter.outbound.persistence.api.error.JpaPersistenceExceptionTest --tests dev.caskeleton.adapter.outbound.persistence.api.query.QueryNameTest --tests dev.caskeleton.adapter.outbound.persistence.api.query.SignedJsonCursorCodecTest --tests dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionProfileTest --rerun-tasks --console=plain
Working directory: /shared/codebase/clean-architecture-backend-template/src
Executed at: 2026-08-29T07:48:59Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Fresh execution of the six dedicated persistence api test classes. These tests cover their declared fixtures but do not add the external boundary probe used separately for 2046-2048 byte cursor payloads.
--- stdout/stderr ---
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :shared-contract:compileJava
> Task :shared-contract:processResources
> Task :shared-contract:classes
> Task :shared-contract:jar
> Task :application-core:compileJava
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes
> Task :application-core:jar
> Task :adapter:outbound:persistence-jpa:compileJava
> Task :adapter:outbound:persistence-jpa:processResources
> Task :adapter:outbound:persistence-jpa:classes
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses
> Task :adapter:outbound:persistence-jpa:compileTestJava
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testClasses
> Task :adapter:outbound:persistence-jpa:test
BUILD SUCCESSFUL in 23s
18 actionable tasks: 18 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
Exit code: 0
@@ -0,0 +1,24 @@
Command: ./gradlew :adapter:outbound:persistence-jpa:verifyJpaApiSurface --rerun-tasks --console=plain
Working directory: /shared/codebase/clean-architecture-backend-template/src
Executed at: 2026-08-29T07:49:23Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Fresh Gradle verification that the current leaf public top-level API matches the committed jpa-api-surface baseline. It verifies names/surface drift, not behavioral correctness or reachability.
--- stdout/stderr ---
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :adapter:outbound:persistence-jpa:verifyJpaApiSurface
verifyJpaApiSurface: OK — the committed public API surface is unchanged.
BUILD SUCCESSFUL in 3s
9 actionable tasks: 9 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
Exit code: 0
@@ -0,0 +1,174 @@
Command: ./gradlew :app-bootstrap:test --tests dev.caskeleton.bootstrap.autoconfigure.jpa.JpaPlatformAutoConfigurationTest --rerun-tasks --console=plain
Working directory: /shared/codebase/clean-architecture-backend-template/src
Executed at: 2026-08-29T07:49:26Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Fresh app-bootstrap test for current JPA platform composition/capability declarations. It exercises the repository's static capability list, not arbitrary third-party CapabilitySupport values.
--- stdout/stderr ---
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :shared-contract:compileJava
> Task :shared-contract:processResources
> Task :shared-contract:classes
> Task :shared-contract:jar
> Task :application-core:compileJava
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes
> Task :application-core:jar
> Task :domain-core:compileJava
> Task :domain-core:processResources NO-SOURCE
> Task :domain-core:classes
> Task :domain-core:jar
> Task :messaging:messaging-core-api:compileJava
> Task :messaging:messaging-observability:compileJava
> Task :messaging:messaging-schema-api:compileJava
> Task :messaging:messaging-policy:compileJava
> Task :messaging:messaging-reliability-api:compileJava
> Task :messaging:messaging-security:compileJava
> Task :messaging:messaging-admin-api:compileJava
> Task :messaging:messaging-transport-spi:compileJava
> Task :messaging:messaging-admin-runtime:compileJava
> Task :messaging:messaging-claim-check:compileJava
> Task :messaging:messaging-cloudevents:compileJava
> Task :messaging:messaging-inbox-jdbc-postgresql:compileJava
> Task :messaging:messaging-kafka:compileJava
> Task :messaging:messaging-outbox-jdbc-postgresql:compileJava
> Task :messaging:messaging-rabbit:compileJava
> Task :messaging:messaging-runtime-core:compileJava
> Task :messaging:messaging-schema-json:compileJava
> Task :messaging:messaging-spring-boot-starter:compileJava
> Task :adapter:inbound:graphql:compileJava
> Task :adapter:inbound:graphql:processResources
> Task :adapter:inbound:graphql:classes
> Task :adapter:inbound:graphql:jar
> Task :adapter:inbound:web:compileJava
> Task :adapter:inbound:web:processResources
> Task :adapter:inbound:web:classes
> Task :adapter:inbound:web:jar
> Task :adapter:outbound:support:compileJava
> Task :adapter:outbound:support:processResources NO-SOURCE
> Task :adapter:outbound:support:classes
> Task :adapter:outbound:support:jar
> Task :adapter:outbound:cache-redis:compileJava
> Task :adapter:outbound:cache-redis:processResources
> Task :adapter:outbound:cache-redis:classes
> Task :adapter:outbound:cache-redis:jar
> Task :adapter:outbound:fileserver:compileJava
> Task :adapter:outbound:fileserver:processResources NO-SOURCE
> Task :adapter:outbound:fileserver:classes
> Task :adapter:outbound:fileserver:jar
> Task :adapter:outbound:httpclient:compileJava
> Task :adapter:outbound:httpclient:processResources NO-SOURCE
> Task :adapter:outbound:httpclient:classes
> Task :adapter:outbound:httpclient:jar
> Task :adapter:outbound:identifier:compileJava
> Task :adapter:outbound:identifier:compileGroovy NO-SOURCE
> Task :adapter:outbound:identifier:processResources NO-SOURCE
> Task :adapter:outbound:identifier:classes
> Task :adapter:outbound:identifier:jar
> Task :adapter:outbound:messaging:compileJava
> Task :adapter:outbound:messaging:processResources
> Task :adapter:outbound:messaging:classes
> Task :adapter:outbound:messaging:jar
> Task :adapter:outbound:notification:compileJava
> Task :adapter:outbound:notification:processResources NO-SOURCE
> Task :adapter:outbound:notification:classes
> Task :adapter:outbound:notification:jar
> Task :adapter:outbound:persistence-jpa:compileJava
> Task :adapter:outbound:persistence-jpa:processResources
> Task :adapter:outbound:persistence-jpa:classes
> Task :adapter:outbound:persistence-jpa:jar
> Task :adapter:outbound:persistence-mongo:compileJava
> Task :adapter:outbound:persistence-mongo:processResources
> Task :adapter:outbound:persistence-mongo:classes
> Task :adapter:outbound:persistence-mongo:jar
> Task :app-bootstrap:compileJava
> Task :app-bootstrap:processResources
> Task :app-bootstrap:classes
> Task :sample-portfolio:compileJava
> Task :sample-portfolio:processResources
> Task :sample-portfolio:classes
> Task :sample-portfolio:jar
> Task :adapter:inbound:web:compileTestkitJava
> Task :adapter:inbound:web:processTestkitResources
> Task :adapter:inbound:web:testkitClasses
> Task :adapter:inbound:web:testkitJar
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses
> Task :adapter:outbound:persistence-jpa:testkitJar
> Task :app-bootstrap:compileTestJava
> Task :app-bootstrap:runtimeClasspathManifest
> Task :app-bootstrap:processTestResources
> Task :app-bootstrap:testClasses
> Task :messaging:messaging-admin-api:processResources NO-SOURCE
> Task :messaging:messaging-admin-api:classes
> Task :messaging:messaging-admin-api:jar
> Task :messaging:messaging-admin-runtime:processResources NO-SOURCE
> Task :messaging:messaging-admin-runtime:classes
> Task :messaging:messaging-admin-runtime:jar
> Task :messaging:messaging-claim-check:processResources NO-SOURCE
> Task :messaging:messaging-claim-check:classes
> Task :messaging:messaging-claim-check:jar
> Task :messaging:messaging-cloudevents:processResources NO-SOURCE
> Task :messaging:messaging-cloudevents:classes
> Task :messaging:messaging-cloudevents:jar
> Task :messaging:messaging-core-api:processResources NO-SOURCE
> Task :messaging:messaging-core-api:classes
> Task :messaging:messaging-core-api:jar
> Task :messaging:messaging-inbox-jdbc-postgresql:processResources
> Task :messaging:messaging-inbox-jdbc-postgresql:classes
> Task :messaging:messaging-inbox-jdbc-postgresql:jar
> Task :messaging:messaging-kafka:processResources NO-SOURCE
> Task :messaging:messaging-kafka:classes
> Task :messaging:messaging-kafka:jar
> Task :messaging:messaging-observability:processResources NO-SOURCE
> Task :messaging:messaging-observability:classes
> Task :messaging:messaging-observability:jar
> Task :messaging:messaging-outbox-jdbc-postgresql:processResources
> Task :messaging:messaging-outbox-jdbc-postgresql:classes
> Task :messaging:messaging-outbox-jdbc-postgresql:jar
> Task :messaging:messaging-policy:processResources NO-SOURCE
> Task :messaging:messaging-policy:classes
> Task :messaging:messaging-policy:jar
> Task :messaging:messaging-rabbit:processResources NO-SOURCE
> Task :messaging:messaging-rabbit:classes
> Task :messaging:messaging-rabbit:jar
> Task :messaging:messaging-reliability-api:processResources NO-SOURCE
> Task :messaging:messaging-reliability-api:classes
> Task :messaging:messaging-reliability-api:jar
> Task :messaging:messaging-runtime-core:processResources NO-SOURCE
> Task :messaging:messaging-runtime-core:classes
> Task :messaging:messaging-runtime-core:jar
> Task :messaging:messaging-schema-api:processResources NO-SOURCE
> Task :messaging:messaging-schema-api:classes
> Task :messaging:messaging-schema-api:jar
> Task :messaging:messaging-schema-json:processResources NO-SOURCE
> Task :messaging:messaging-schema-json:classes
> Task :messaging:messaging-schema-json:jar
> Task :messaging:messaging-security:processResources NO-SOURCE
> Task :messaging:messaging-security:classes
> Task :messaging:messaging-security:jar
> Task :messaging:messaging-spring-boot-starter:processResources
> Task :messaging:messaging-spring-boot-starter:classes
> Task :messaging:messaging-spring-boot-starter:jar
> Task :messaging:messaging-transport-spi:processResources NO-SOURCE
> Task :messaging:messaging-transport-spi:classes
> Task :messaging:messaging-transport-spi:jar
> Task :adapter:outbound:objectstorage:compileJava
> Task :adapter:outbound:objectstorage:processResources NO-SOURCE
> Task :adapter:outbound:objectstorage:classes
> Task :adapter:outbound:objectstorage:jar
> Task :app-bootstrap:test
BUILD SUCCESSFUL in 1m 31s
98 actionable tasks: 98 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
Exit code: 0
@@ -0,0 +1,107 @@
Command: git history for cursor/capability/entity-not-found API hotspots
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T07:50:26Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Repository history for the three selected API hotspots; commit text/diffs show when code entered or changed but do not establish unstated author intent.
--- stdout/stderr ---
=== SignedJsonCursorCodec history ===
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
0e61f86 feat(jpa): implement the JPA relational persistence platform
=== decodedLengthOf introducing diff ===
COMMIT 2f5d2fc21954286213c1474d19935f571ef896ea feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
diff --git a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/SignedJsonCursorCodec.java b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/SignedJsonCursorCodec.java
index 0263e77..7c306cd 100644
--- a/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/SignedJsonCursorCodec.java
+++ b/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/SignedJsonCursorCodec.java
@@ -50,13 +50,33 @@ public final class SignedJsonCursorCodec<C> implements CursorCodec<C> {
this.key = key.clone();
}
+ /** Longest token this codec will look at, in characters. */
+ public static final int MAX_ENCODED_LENGTH = 4096;
+
+ /** Largest payload this codec will decode, in bytes. */
+ public static final int MAX_PAYLOAD_BYTES = 2048;
+
+ /** HMAC-SHA256 produces exactly this many bytes. */
+ private static final int MAC_LENGTH_BYTES = 32;
+
@Override
public String encode(C cursor) {
Objects.requireNonNull(cursor, "cursor");
- String payload =
- encoder.encodeToString(payloadCodec.toJson(cursor).getBytes(StandardCharsets.UTF_8));
+ byte[] json = payloadCodec.toJson(cursor).getBytes(StandardCharsets.UTF_8);
+ if (json.length > MAX_PAYLOAD_BYTES) {
+ // The application's own bug, not a caller's: a cursor this large is a payload the codec
+ // cannot hand back, so producing it would mint a token that fails on the next page.
+ throw new IllegalStateException(
+ "cursor payload is " + json.length + " bytes; the bound is " + MAX_PAYLOAD_BYTES);
+ }
+ String payload = encoder.encodeToString(json);
String signed = VERSION + SEPARATOR + payload;
- return signed + SEPARATOR + encoder.encodeToString(mac(signed));
+ String token = signed + SEPARATOR + encoder.encodeToString(mac(signed));
+ if (token.length() > MAX_ENCODED_LENGTH) {
+ throw new IllegalStateException(
+ "cursor token is " + token.length() + " characters; the bound is " + MAX_ENCODED_LENGTH);
+ }
+ return token;
}
@Override
@@ -64,6 +84,13 @@ public final class SignedJsonCursorCodec<C> implements CursorCodec<C> {
if (encoded == null || encoded.isBlank()) {
throw new IllegalArgumentException("cursor must not be blank");
}
+ // First line, before any substring, decode or MAC. A paging endpoint is public, and everything
+ // below this point allocates in proportion to what the caller sent: repeatedly posting a very
+ // large token made the server build strings, byte arrays and a MAC input before it had any
+ // reason to believe the token was real. A page-size bound does not bound the token.
+ if (encoded.length() > MAX_ENCODED_LENGTH) {
+ throw new IllegalArgumentException("cursor exceeds the maximum token length");
+ }
int payloadSeparator = encoded.indexOf(SEPARATOR);
int macSeparator = encoded.lastIndexOf(SEPARATOR);
if (payloadSeparator <= 0 || macSeparator <= payloadSeparator) {
@@ -73,15 +100,33 @@ public final class SignedJsonCursorCodec<C> implements CursorCodec<C> {
if (!VERSION.equals(version)) {
throw new IllegalArgumentException("unknown cursor version");
}
+ // Base64 expands by 4/3, so the encoded payload segment's length bounds the decoded size
+ // exactly. Checking it here refuses an oversized payload without allocating it first.
+ int encodedPayloadLength = macSeparator - payloadSeparator - 1;
+ if (decodedLengthOf(encodedPayloadLength) > MAX_PAYLOAD_BYTES) {
+ throw new IllegalArgumentException("cursor payload exceeds the maximum size");
+ }
String signed = encoded.substring(0, macSeparator);
byte[] presented = decodeBase64(encoded.substring(macSeparator + 1));
+ if (presented.length != MAC_LENGTH_BYTES) {
+ // Checked before the comparison. `MessageDigest.isEqual` is constant time for equal-length
+ // inputs; feeding it a differently-sized array asks it a question it was not built to answer.
+ throw new IllegalArgumentException("malformed cursor");
+ }
if (!MessageDigest.isEqual(mac(signed), presented)) {
throw new IllegalArgumentException("cursor signature does not verify");
}
+ // The payload decoder runs only after the signature verified, so an unsigned token never
+ // reaches the application's JSON parsing at all.
byte[] payload = decodeBase64(encoded.substring(payloadSeparator + 1, macSeparator));
return payloadCodec.fromJson(new String(payload, StandardCharsets.UTF_8));
}
+ /** The number of bytes a Base64 segment of this many characters decodes to, at most. */
+ private static int decodedLengthOf(int encodedLength) {
+ return encodedLength / 4 * 3 + 3;
+ }
+
private byte[] decodeBase64(String value) {
try {
return decoder.decode(value);
=== CapabilitySupport history ===
0e61f86 feat(jpa): implement the JPA relational persistence platform
=== JpaEntityNotFoundException history ===
0e61f86 feat(jpa): implement the JPA relational persistence platform
Exit code: 0
@@ -0,0 +1,72 @@
Command: python3 - <<'PY'
from pathlib import Path
for sr in ['main','test']:
base=Path('src/adapter/outbound/persistence-jpa/src')/sr/'java/dev/caskeleton/adapter/outbound/persistence'
for top in ['transaction','failure']:
p=base/top
files=sorted(p.rglob('*.java')) if p.exists() else []
print(sr,top,'count=',len(files),'loc=',sum(len(f.read_text(errors='ignore').splitlines()) for f in files))
for f in files: print(f)
PY
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T08:01:13Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Enumerates the transaction/failure bounded sub-scope production and dedicated test files. It establishes the 51-file denominator only.
--- stdout/stderr ---
main transaction count= 29 loc= 2427
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/BackoffCalculator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecord.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecorder.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicy.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EffectiveTransactionTimeouts.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/JpaTransactionConfig.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/JpaTransactionSettings.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/OptimisticConflictTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/PersistenceFailureTranslatorChain.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/RetryBudget.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/RetrySleeper.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringPolicyTransactionPort.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/ThreadRetrySleeper.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDeadlineCalculator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDefinitionMapper.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceContext.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceFrame.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceScope.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionLocalTimeoutConfigurer.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistry.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionRetryBackoff.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionRetryClassifier.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionStartBudget.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/UnknownOperation.java
main failure count= 3 loc= 165
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/failure/PersistenceExceptionTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/failure/SqlStateErrorMapping.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/failure/StandardSqlStateErrorMapping.java
test transaction count= 18 loc= 2203
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/BackoffCalculatorTest.java
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifierTest.java
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecorderTest.java
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicyTest.java
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManagerTest.java
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinatorTest.java
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/JpaTransactionSettingsTest.java
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/PersistenceFailureTranslatorChainTest.java
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/RetryBudgetTest.java
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutorTest.java
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringPolicyTransactionPortTest.java
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPortTest.java
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDeadlineCalculatorTest.java
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDefinitionMapperTest.java
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceScopeTest.java
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistryTest.java
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionRetryBackoffTest.java
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionRetryClassifierTest.java
test failure count= 1 loc= 230
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/failure/PersistenceExceptionTranslatorTest.java
Exit code: 0
@@ -0,0 +1,115 @@
Command: python3 - <<'PY'
from pathlib import Path
import re,subprocess
roots=[Path('src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction'),Path('src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/failure')]
alljava=[Path(p) for p in subprocess.check_output(['git','ls-files'],text=True).splitlines() if '/src/main/java/' in p and p.endswith('.java')]
for root in roots:
for src in sorted(root.rglob('*.java')):
txt=src.read_text(); m=re.search(r'public\s+(?:final\s+)?(?:record|class|interface|enum)\s+(\w+)',txt)
if not m: continue
name=m.group(1); pkg=re.search(r'package\s+([^;]+);',txt).group(1); fqn=pkg+'.'+name
refs=[]
for p in alljava:
if p==src: continue
t=p.read_text(errors='ignore'); pp=re.search(r'package\s+([^;]+);',t)
if f'import {fqn};' in t or fqn in t or (pp and pp.group(1)==pkg and re.search(r'\b'+re.escape(name)+r'\b',t)):
refs.append(str(p))
print(f'{fqn}|production_reference_files={len(refs)}')
for r in refs: print(' '+r)
PY
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T08:01:13Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Searches tracked production Java for transaction/failure public top-level type references. Zero references are negative evidence only; reflection/generated/external consumers remain outside the search.
--- stdout/stderr ---
dev.caskeleton.adapter.outbound.persistence.transaction.BackoffCalculator|production_reference_files=1
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/DefaultJpaRetryPolicy.java
dev.caskeleton.adapter.outbound.persistence.transaction.CommitFailureClassifier|production_reference_files=2
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
dev.caskeleton.adapter.outbound.persistence.transaction.CompletionUnknownRecord|production_reference_files=0
dev.caskeleton.adapter.outbound.persistence.transaction.CompletionUnknownRecorder|production_reference_files=0
dev.caskeleton.adapter.outbound.persistence.transaction.DefaultJpaRetryPolicy|production_reference_files=2
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
dev.caskeleton.adapter.outbound.persistence.transaction.EffectiveTransactionTimeouts|production_reference_files=5
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2LocalTimeoutConfigurer.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlLocalTimeoutConfigurer.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringPolicyTransactionPort.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDeadlineCalculator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionLocalTimeoutConfigurer.java
dev.caskeleton.adapter.outbound.persistence.transaction.EvidenceAwareJpaTransactionManager|production_reference_files=0
dev.caskeleton.adapter.outbound.persistence.transaction.FullTransactionRetryCoordinator|production_reference_files=3
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
dev.caskeleton.adapter.outbound.persistence.transaction.JpaTransactionConfig|production_reference_files=0
dev.caskeleton.adapter.outbound.persistence.transaction.JpaTransactionSettings|production_reference_files=4
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/JpaTransactionConfig.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDeadlineCalculator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionRetryBackoff.java
dev.caskeleton.adapter.outbound.persistence.transaction.OptimisticConflictTranslator|production_reference_files=2
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/PersistenceFailureTranslatorChain.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
dev.caskeleton.adapter.outbound.persistence.transaction.PersistenceFailureTranslatorChain|production_reference_files=2
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
dev.caskeleton.adapter.outbound.persistence.transaction.RetryBudget|production_reference_files=1
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
dev.caskeleton.adapter.outbound.persistence.transaction.RetrySleeper|production_reference_files=3
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/ThreadRetrySleeper.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
dev.caskeleton.adapter.outbound.persistence.transaction.SpringJpaTransactionExecutor|production_reference_files=3
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
dev.caskeleton.adapter.outbound.persistence.transaction.SpringTransactionPort|production_reference_files=1
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/JpaTransactionConfig.java
dev.caskeleton.adapter.outbound.persistence.transaction.ThreadRetrySleeper|production_reference_files=1
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionDefinitionMapper|production_reference_files=1
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionEvidenceContext|production_reference_files=4
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceScope.java
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionEvidenceFrame|production_reference_files=3
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceContext.java
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionEvidenceScope|production_reference_files=2
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceContext.java
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionLocalTimeoutConfigurer|production_reference_files=7
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2LocalTimeoutConfigurer.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2PersistenceConfig.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlLocalTimeoutConfigurer.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlPersistenceConfig.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringPolicyTransactionPort.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/persistence/SamplePostgreSqlPersistenceConfig.java
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionProfileRegistry|production_reference_files=0
dev.caskeleton.adapter.outbound.persistence.transaction.TransactionStartBudget|production_reference_files=2
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringPolicyTransactionPort.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionDeadlineCalculator.java
dev.caskeleton.adapter.outbound.persistence.transaction.UnknownOperation|production_reference_files=1
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java
dev.caskeleton.adapter.outbound.persistence.failure.PersistenceExceptionTranslator|production_reference_files=3
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/failure/SqlStateErrorMapping.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringPolicyTransactionPort.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java
dev.caskeleton.adapter.outbound.persistence.failure.SqlStateErrorMapping|production_reference_files=7
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/failure/PersistenceExceptionTranslator.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/failure/StandardSqlStateErrorMapping.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2PersistenceConfig.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2SqlStateErrorMapping.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlPersistenceConfig.java
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlSqlStateErrorMapping.java
src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/persistence/SamplePostgreSqlPersistenceConfig.java
dev.caskeleton.adapter.outbound.persistence.failure.StandardSqlStateErrorMapping|production_reference_files=1
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java
Exit code: 0
@@ -0,0 +1,290 @@
Command: printf '%s\n' '--- persistence root ---'; sed -n '1,115p' src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/persistencejpa/PersistenceJpaRootAutoConfiguration.java; printf '%s\n' '--- adapter component scan ---'; cat src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/config/JpaAdapterComponentsConfig.java; printf '%s\n' '--- runtime transaction beans ---'; sed -n '140,195p' src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java; printf '%s\n' '--- canonical/application references ---'; git grep -n -E '\b(SpringTransactionPort|PolicyTransactionPort|FullTransactionRetryCoordinator|SpringJpaTransactionExecutor)\b' -- 'src/**/src/main/java/**/*.java'
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T08:01:16Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Shows shipped persistence root imports, narrow adapter component scan, transaction bean factories, and exact production construction/reference sites for the two transaction stacks. Static wiring does not prove runtime invocation frequency.
--- stdout/stderr ---
--- persistence root ---
package dev.caskeleton.bootstrap.autoconfigure.persistencejpa;
import dev.caskeleton.adapter.outbound.persistence.config.JpaAdapterComponentsConfig;
import dev.caskeleton.adapter.outbound.persistence.config.PersistenceVendorSettings;
import dev.caskeleton.adapter.outbound.persistence.fileserver.FileserverJpaPersistenceConfig;
import dev.caskeleton.adapter.outbound.persistence.h2.H2PersistenceConfig;
import dev.caskeleton.adapter.outbound.persistence.notification.configuration.NotificationJpaPersistenceFacade;
import dev.caskeleton.adapter.outbound.persistence.postgresql.PostgreSqlPersistenceConfig;
import dev.caskeleton.bootstrap.autoconfigure.jpa.JpaDataSourceProfileValidator;
import dev.caskeleton.bootstrap.autoconfigure.jpa.JpaPlatformRuntimeAutoConfiguration;
import dev.caskeleton.bootstrap.autoconfigure.jpa.PostgreSqlVersionPolicy;
import dev.caskeleton.bootstrap.runtime.startup.MigrationStartupConfig;
import javax.sql.DataSource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
/**
* The one place that decides whether this application has relational persistence.
*
* <p>Every JPA configuration is reached through this import rather than through the component scan,
* so a bean added to any of them next month is gated without anyone remembering to repeat a
* condition. That is the difference between a capability that is off and a capability whose known
* beans are individually conditioned.
*
* <p>Both vendor configurations are named here, and each is the leaf\'s real entry point: a vendor
* config imports the core JPA config, not the reverse. Inverting that to give this class a single
* import produced a package cycle inside the leaf, so the export surface admits the two vendor
* packages instead. They are mutually exclusive on {@code ca-skeleton.persistence.vendor}, so
* naming both lets the selector decide which one assembles while the decision about
* <em>whether</em> there is a database stays here, in one place.
*
* <p><b>The notification facade is named here only now that the payload is protected at rest.</b>
* Importing it makes {@code JpaNotificationRequestStore} reachable, and that store used to write
* the accepted request's template variables to {@code notification_request.variables_payload} in
* plaintext — caller content that can be a reset code, an order total or an address (NTF-INT-007).
* It was wired here once before that was true; the at-rest contract test failed on exactly that
* fact, which is what it was written to do, and the wiring was reverted rather than the test
* relaxed. The envelope came first, {@code NotificationRecordMapper} now takes the protection as a
* required constructor argument, and only then does this import become a wiring rather than an
* exposure.
*
* <p>{@code FileserverJpaPersistenceConfig} is named for a related reason. Its six entities map six
* {@code fs_*} tables that live only in {@code db/migration/jpa/fileserver}, a stream applied only
* when that capability is on, while the primary Flyway location creates none of them. They used to
* be in the unconditional entity scan, so {@code ddl-auto=validate} against real PostgreSQL failed
* on {@code fs_cleanup_item} in every deployment that had switched the capability off — which was
* every JPA-on Compose lane. The scan now carries the capability's own condition, and this is the
* only place that knows both that the capability is on and that a JPA vendor is composed. Neither
* the leaf's {@code config} package nor a component scan can register it: {@code config} may depend
* on {@code api} alone, and the whole persistence package is excluded from the root's scan.
*/
@AutoConfiguration
@ConditionalOnProperty(
prefix = "ca-skeleton.persistence-jpa",
name = "enabled",
havingValue = "true")
@Import({
JpaAdapterComponentsConfig.class,
PostgreSqlPersistenceConfig.class,
H2PersistenceConfig.class,
FileserverJpaPersistenceConfig.class,
NotificationJpaPersistenceFacade.class,
MigrationStartupConfig.class,
JpaPlatformRuntimeAutoConfiguration.class
})
public class PersistenceJpaRootAutoConfiguration {
/**
* Runs the datasource validator, which nothing used to run (JPA-INT-002).
*
* <p>{@code JpaDataSourceProfileValidator} was created as a bean and never invoked: both of its
* methods were reachable only from their own unit test. A validator nobody calls is a comment —
* and this one would have failed every deployment if called, because the namespace it read was
* supplied by nothing. The two defects cancelled, which is the only reason the application
* started.
*
* <p>Invoked from here rather than from the platform configuration so it runs exactly when JPA is
* on and never when it is off, and as an {@link InitializingBean} so the failure lands during
* startup rather than at whoever sends the first request.
*
* <p>The vendor is read from the {@link Environment} rather than injected as {@code
* PersistenceVendorSettings}, because that type is not a registered bean here: the composition
* root's {@code @ConfigurationPropertiesScan} deliberately excludes the persistence package so an
* optional capability cannot bind — or reject — its detail settings in a deployment that never
* switched it on. The default matches {@code PostgreSqlPersistenceConfig}'s {@code matchIfMissing
* = true}: an unset selector is PostgreSQL, which is the vendor this configuration was
* unconditional about before the selector existed.
*
* <p>The validator is constructed here rather than injected. It is stateless, and the bean that
* used to supply it comes from {@code JpaPlatformRuntimeAutoConfiguration}, which carries
* {@code @ConditionalOnBean(DataSource.class)} on a plain {@code @Configuration} imported by this
* root — a condition evaluated during configuration-class parsing, before the datasource bean
* definition is registered. That class therefore drops out silently in the real application,
* taking the whole JPA add-on layer with it. Depending on a bean from it would make this check
* disappear for the same reason the thing it checks disappeared. The condition-ordering defect is
* recorded separately; it is not this task's to fix quietly.
*
* @param dataSource the resolved datasource, injected so the database checked is the one in use
* @param environment the resolved environment, for the vendor selector
* @return the startup check
*/
@Bean
public InitializingBean jpaResolvedDataSourceCheck(
DataSource dataSource, Environment environment) {
JpaDataSourceProfileValidator validator =
new JpaDataSourceProfileValidator(new PostgreSqlVersionPolicy());
String vendor =
environment.getProperty(PersistenceVendorSettings.VENDOR_PROPERTY, "postgresql").trim();
return () -> validator.validateResolved(dataSource, "postgresql".equalsIgnoreCase(vendor));
}
}
--- adapter component scan ---
package dev.caskeleton.adapter.outbound.persistence.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* Registers the JPA adapter's scanned components, which nothing registered (JPA-INT-006).
*
* <p>The composition root's {@code @ComponentScan} excludes {@code
* dev.caskeleton.adapter.outbound.persistence.**} by regex, and that exclusion is correct: it is
* what makes an optional capability optional, so a deployment with JPA off assembles no persistence
* beans rather than assembling them and hoping each one remembered to carry the switch.
*
* <p>What was missing is the other half. Eight classes in this leaf are written as scanned
* components — {@code SpringTransactionPort}, {@code PersistenceExceptionTranslator}, {@code
* StandardSqlStateErrorMapping}, {@code DomainContextAuditContextPort}, the idempotency store and
* its reaper, and the outbox store and its reaper — and once the broad scan stopped reaching them,
* nothing else did. They are annotated {@code @Component} and {@code @Repository} and were beans in
* no running application: {@code TransactionPort} in particular had no implementation at all, so
* every use case that opens a transaction had no port to open it with.
*
* <p>It surfaced as an unsatisfied dependency the first time a capability that needs a transaction
* was actually assembled — the notification orchestrator, in the local-notification-ingest lane —
* rather than as anything a unit test could see, because each of these classes is constructed
* directly by its own tests.
*
* <p>So the scan is restored, narrowed to the packages it should always have covered and reachable
* only through {@code PersistenceJpaRootAutoConfiguration}, which carries the JPA master switch.
* Off is still structural.
*
* <p>Two packages are deliberately absent:
*
* <ul>
* <li>{@code ..persistence.fileserver} — gated on its own capability switch, scanned by {@link
* dev.caskeleton.adapter.outbound.persistence.fileserver.FileserverJpaPersistenceConfig};
* <li>{@code ..persistence.notification} — assembled explicitly, bean by bean, by {@code
* NotificationJpaPersistenceFacade}.
* </ul>
*
* <p>Components under these packages keep their own {@code @ConditionalOnProperty} guards; being
* scanned makes them candidates, not unconditional beans.
*
* <p>{@code ..persistence.lock} is in the list for the same reason and with the same history.
* {@code DistributedLockPersistenceConfig} owns both lock providers — the in-process registry and
* the JDBC one — and was registered by nothing but a test calling {@code ctx.register(...)}. So a
* single-instance deployment had no {@code DistributedLockPort} at all, and a multi-instance one
* could not start: the composition root's own {@code DistributedLockConfig} asks for a bean
* qualified {@code jdbcDistributedLock} that only that configuration declares.
*
* <p>Each package's {@code @ConfigurationProperties} type is enabled by a configuration inside that
* same package — {@code JpaTransactionConfig} for {@code JpaTransactionSettings}, {@code
* DistributedLockPersistenceConfig} for {@code LockSettings} — rather than from here. Enabling them
* centrally would give {@code config} an edge to {@code lock} and {@code transaction} that the
* module map does not grant it, and the map is right: this class knows which packages to scan, not
* what is inside them. They need enabling at all because {@code @ConfigurationPropertiesScan}
* excludes this tree as deliberately as {@code @ComponentScan} does.
*/
@Configuration(proxyBeanMethods = false)
@ComponentScan(
basePackages = {
"dev.caskeleton.adapter.outbound.persistence.audit",
"dev.caskeleton.adapter.outbound.persistence.failure",
"dev.caskeleton.adapter.outbound.persistence.idempotency",
"dev.caskeleton.adapter.outbound.persistence.lock",
"dev.caskeleton.adapter.outbound.persistence.outbox",
"dev.caskeleton.adapter.outbound.persistence.transaction"
})
public class JpaAdapterComponentsConfig {}
--- runtime transaction beans ---
}
/**
* The programmatic transaction boundary, over whichever manager the application installed.
*
* <p>The composed vendor's translator is taken when one exists. Without it the executor's chain
* has no SQLSTATE stage, so a serialization failure or deadlock stays a raw {@code
* DataAccessException}, never becomes the {@code JpaPersistenceException} the retry coordinator
* catches, and contention is not retried at all.
*
* @param vendorFailures the composed vendor's SQLSTATE translator, when a vendor registered one
*/
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(PlatformTransactionManager.class)
public SpringJpaTransactionExecutor jpaTransactionExecutor(
PlatformTransactionManager transactionManager,
Clock clock,
ObjectProvider<VendorFailureTranslator> vendorFailures) {
JpaTransactionAutoConfiguration composition = new JpaTransactionAutoConfiguration(clock);
VendorFailureTranslator vendor = vendorFailures.getIfAvailable();
return vendor == null
? composition.transactionExecutor(transactionManager)
: composition.transactionExecutor(transactionManager, vendor);
}
/** The whole-transaction retry coordinator. */
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(SpringJpaTransactionExecutor.class)
public FullTransactionRetryCoordinator jpaRetryCoordinator(
SpringJpaTransactionExecutor executor, RetryEventListener retryListener, Clock clock) {
return new JpaTransactionAutoConfiguration(clock)
.retryCoordinator(
executor,
dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryProfile
.boundedContention("jpa-platform-default", 3),
retryListener);
}
/**
* The retry listener.
*
* <p>The interface's methods all have defaults, so an empty implementation is the honest "no
* observer wired" value; an application that wants retry telemetry supplies its own bean.
*/
@Bean
@ConditionalOnMissingBean
public RetryEventListener jpaRetryEventListener() {
return new RetryEventListener() {};
}
/** Runs the dangerous-configuration guard during refresh. */
@Bean
public InitializingBean jpaPlatformStartupCheck(
JpaDangerousConfigurationGuard guard, Environment environment) {
--- canonical/application references ---
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/config/JpaAdapterComponentsConfig.java:15: * components — {@code SpringTransactionPort}, {@code PersistenceExceptionTranslator}, {@code
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:30:public final class FullTransactionRetryCoordinator {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:32: private final SpringJpaTransactionExecutor transactionExecutor;
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:50: public FullTransactionRetryCoordinator(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:51: SpringJpaTransactionExecutor transactionExecutor,
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/JpaTransactionConfig.java:9: * <p>{@link SpringTransactionPort} takes it as a constructor argument, and the composition root's
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:21: * <p>This class does not retry. Retry lives in {@link FullTransactionRetryCoordinator}, which calls
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:25:public final class SpringJpaTransactionExecutor implements JpaTransactionExecutor {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:32: public SpringJpaTransactionExecutor(PlatformTransactionManager transactionManager, Clock clock) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:41: public SpringJpaTransactionExecutor(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:8:import dev.caskeleton.application.transaction.PolicyTransactionPort;
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:26: * Spring-backed {@link PolicyTransactionPort}: one pre-built {@link TransactionTemplate} per mode,
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:31:public class SpringTransactionPort implements PolicyTransactionPort {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:39: public SpringTransactionPort(PlatformTransactionManager transactionManager) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:49: SpringTransactionPort(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:61: public SpringTransactionPort(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:77: SpringTransactionPort(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:90: SpringTransactionPort(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:105: SpringTransactionPort(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:7:import dev.caskeleton.adapter.outbound.persistence.transaction.FullTransactionRetryCoordinator;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:8:import dev.caskeleton.adapter.outbound.persistence.transaction.SpringJpaTransactionExecutor;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:155: public SpringJpaTransactionExecutor jpaTransactionExecutor(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:169: @ConditionalOnBean(SpringJpaTransactionExecutor.class)
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:170: public FullTransactionRetryCoordinator jpaRetryCoordinator(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:171: SpringJpaTransactionExecutor executor, RetryEventListener retryListener, Clock clock) {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:9:import dev.caskeleton.adapter.outbound.persistence.transaction.FullTransactionRetryCoordinator;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:13:import dev.caskeleton.adapter.outbound.persistence.transaction.SpringJpaTransactionExecutor;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:37: * PolicyTransactionPort.inTransaction(TransactionRequest, Supplier)}; the retry coordinator below
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:66: public SpringJpaTransactionExecutor transactionExecutor(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:68: return new SpringJpaTransactionExecutor(transactionManager, clock);
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:82: public SpringJpaTransactionExecutor transactionExecutor(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:84: return new SpringJpaTransactionExecutor(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:99: public FullTransactionRetryCoordinator retryCoordinator(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:100: SpringJpaTransactionExecutor executor,
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:107: public FullTransactionRetryCoordinator retryCoordinator(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:108: SpringJpaTransactionExecutor executor, JpaRetryPolicy policy, RetryEventListener listener) {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:109: return new FullTransactionRetryCoordinator(
src/application-core/src/main/java/dev/caskeleton/application/transaction/PolicyTransactionPort.java:10:public interface PolicyTransactionPort extends TransactionPort {
src/application-core/src/main/java/dev/caskeleton/application/transaction/TransactionPort.java:8: * {@code SpringTransactionPort}) implements it over Spring's {@code PlatformTransactionManager}.
Exit code: 0
@@ -0,0 +1,194 @@
Command: printf '%s\n' '--- canonical boundary claim ---'; sed -n '18,118p' src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java; printf '%s\n' '--- transaction guide ---'; cat docs/jpa/transaction-guide.md; printf '%s\n' '--- FullTransactionRetryCoordinator production refs ---'; git grep -n -w 'FullTransactionRetryCoordinator' -- 'src/**/src/main/java/**/*.java'; printf '%s\n' '--- coordinator execute call candidates ---'; git grep -n -E '\.(execute)\(' -- 'src/**/src/main/java/**/*.java' | grep -E 'retry|transaction|coordinator' || true
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T08:01:16Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Compares current source/document claims for the application PolicyTransactionPort stack versus the public JPA executor/coordinator stack, including exact production execute references. It identifies coexistence/drift but does not by itself prove external consumers are absent.
--- stdout/stderr ---
--- canonical boundary claim ---
import org.springframework.transaction.PlatformTransactionManager;
/**
* Composes the transaction half of the platform (design §9.3, §17, §19).
*
* <p>Separate from {@link JpaPlatformAutoConfiguration} because it backs off separately. An
* application that already installs its own {@link PlatformTransactionManager} — a JTA setup, a
* chained manager across two data sources — must keep it, and the rest of the platform is still
* useful to it. Folding the two together would make "I have my own transaction manager" mean "I get
* none of the platform".
*
* <p>Plain construction rather than Spring auto-configuration: this repository's composition root
* owns wiring, and an adapter leaf that auto-configured a transaction manager would silently
* replace one the application had deliberately chosen.
*
* <p>There is no declarative retry annotation any more. {@code @RetryableJpaTransaction} lived in
* the persistence leaf and documented itself as something an application service would put on its
* own methods — which application-core cannot do without importing an outbound adapter and
* inverting the dependency this architecture is built on. The canonical boundary is {@code
* PolicyTransactionPort.inTransaction(TransactionRequest, Supplier)}; the retry coordinator below
* is what implements it, not a second way to ask for the same thing.
*/
public final class JpaTransactionAutoConfiguration {
/** How long a logical operation may keep retrying before the budget is spent regardless. */
public static final Duration DEFAULT_MAX_RETRY_ELAPSED = Duration.ofSeconds(30);
private final Clock clock;
public JpaTransactionAutoConfiguration(Clock clock) {
this.clock = Objects.requireNonNull(clock, "clock");
}
/**
* The commit-phase classifier the evidence-aware transaction manager is built with.
*
* <p>The manager itself is constructed inside the persistence leaf, through {@code
* EvidenceAwareJpaTransactionManager.standard(entityManagerFactory, clock)}. The composition root
* deliberately does not do it here: that would require {@code jakarta.persistence} and {@code
* org.hibernate} on the composition root's compile classpath, and this repository keeps ORM types
* inside the persistence leaf. What the root owns is the decision — whether to install the
* platform's manager at all — and the classifier it uses.
*/
public CommitFailureClassifier commitFailureClassifier() {
return CommitFailureClassifier.standard(clock);
}
/** The programmatic transaction boundary, with no vendor classification. */
public SpringJpaTransactionExecutor transactionExecutor(
PlatformTransactionManager transactionManager) {
return new SpringJpaTransactionExecutor(transactionManager, clock);
}
/**
* The programmatic transaction boundary that classifies its attempts with a vendor's SQLSTATEs.
*
* <p>The overload above builds the catalog-free chain, whose vendor stage returns the failure
* unchanged. That is the honest answer when no vendor translator exists, and it is the wrong one
* whenever a vendor is composed: a 40001 or 40P01 stays a raw {@code DataAccessException}, never
* becomes a {@code JpaPersistenceException}, and so never reaches the retry coordinator's catch.
* Contention then goes unretried in exactly the deployments that have a database.
*
* @param vendorFailures the composed vendor's SQLSTATE translator
*/
public SpringJpaTransactionExecutor transactionExecutor(
PlatformTransactionManager transactionManager, VendorFailureTranslator vendorFailures) {
return new SpringJpaTransactionExecutor(
transactionManager,
clock,
new PersistenceFailureTranslatorChain(
OptimisticConflictTranslator.withoutCatalog(), vendorFailures));
}
/**
* The retry coordinator for one retry profile.
*
* <p>The policy and the coordinator's budget are built from the <em>same</em> profile on purpose.
* Configuring them independently is how a deployment ends up with a policy that says "retry" and
* a budget that permits one attempt — which looks like retry being broken rather than like a
* misconfiguration.
*/
public FullTransactionRetryCoordinator retryCoordinator(
SpringJpaTransactionExecutor executor,
RetryProfile retryProfile,
RetryEventListener listener) {
return retryCoordinator(executor, DefaultJpaRetryPolicy.forProfile(retryProfile), listener);
}
/** The retry coordinator for an application-supplied policy. */
public FullTransactionRetryCoordinator retryCoordinator(
SpringJpaTransactionExecutor executor, JpaRetryPolicy policy, RetryEventListener listener) {
return new FullTransactionRetryCoordinator(
executor, policy, retrySleeper(), clock, DEFAULT_MAX_RETRY_ELAPSED, listener);
}
/** The production sleeper; tests substitute a recording one. */
public RetrySleeper retrySleeper() {
return new ThreadRetrySleeper();
}
}
--- transaction guide ---
# Transaction Guide
Design §15-§20. What owns a transaction, what may be retried, and what must never be.
## The application service owns the boundary
Repository adapters do not open transactions. The use case does, through `TransactionPort` or
`JpaTransactionExecutor`, because the unit of work is a business decision and only the use case
knows where it starts and ends.
Open Session In View is off in every runtime profile. It is on by default in Spring Boot, which is
why `JpaDangerousConfigurationGuard` fails startup rather than trusting configuration review.
## Profiles
A `TransactionProfile` fixes propagation, isolation, timeout, read-only, and the retry budget. A
write profile must carry a positive finite timeout — the type refuses to represent one without —
because an unbounded write transaction holds a connection, its locks, and its row versions for as
long as one stuck statement takes.
`REQUIRES_NEW` is opt-in. It acquires a second physical connection while pinning the first, so a
profile using it must be paired with the pool-pressure evidence in design §38:
```text
maximumPoolSize >= (concurrent_threads x (1 + max_requires_new_depth)) + 1
```
## Retry is per use case, never per statement
`FullTransactionRetryCoordinator` re-enters the executor, which produces a new transaction and a new
Persistence Context for every attempt. That granularity is the whole point: an optimistic conflict
means the state the attempt computed against is no longer the committed state, so re-issuing the
same statement would compute the same wrong answer. The domain rules have to run again against
reloaded data.
Retryable: serialization failure (`40001`), deadlock (`40P01`), optimistic conflict.
Not retryable: constraint violations, schema mismatch, query timeout, and anything unclassified.
Two additional refusals, independent of budget:
- An attempt that declared an irreversible external effect through `IrreversibleSideEffectContext`.
Rollback reverses database work only; an email or a card charge has already changed the world.
- Anything completion-unknown.
## Completion unknown
`TransactionCompletionUnknownException` is never retried, and the type system enforces it twice:
`JpaFailureContext` refuses to represent a retryable completion-unknown failure, and the exception
rebuilds its context through the safe factory whatever it is handed.
`EvidenceAwareJpaTransactionManager` marks the phase `COMMITTING` immediately before delegating to
the provider commit and never after. If the network, the JVM, or the server dies inside that call,
the last thing written is "we asked, we do not know" — which is exactly the state that must not be
mistaken for a rollback.
Recovery is reconciliation, not retry:
```text
record the transaction key -> check the idempotency record
-> check the business row
-> check the outbox
-> still undetermined? reconciliation queue
```
`CompletionUnknownRecorder` writes that record through a channel outside the unknown transaction.
Writing it through the same connection would make the audit trail share the failure it documents.
--- FullTransactionRetryCoordinator production refs ---
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:30:public final class FullTransactionRetryCoordinator {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:50: public FullTransactionRetryCoordinator(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:21: * <p>This class does not retry. Retry lives in {@link FullTransactionRetryCoordinator}, which calls
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:7:import dev.caskeleton.adapter.outbound.persistence.transaction.FullTransactionRetryCoordinator;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:170: public FullTransactionRetryCoordinator jpaRetryCoordinator(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:9:import dev.caskeleton.adapter.outbound.persistence.transaction.FullTransactionRetryCoordinator;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:99: public FullTransactionRetryCoordinator retryCoordinator(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:107: public FullTransactionRetryCoordinator retryCoordinator(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:109: return new FullTransactionRetryCoordinator(
--- coordinator execute call candidates ---
src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/restclient/DefaultGenericHttpGateway.java:118: HttpCallResult<T> result = coordinator.execute(call);
src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/webclient/DefaultReactiveHttpGateway.java:149: coordinator.execute(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java:101: transactionExecutor.execute(operation, profile, work, attemptNumber, transactionKey);
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java:83: return template.execute(status -> work.get());
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:166: return policyExecutor.execute(request, action);
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java:197: return template.execute(status -> action.get());
src/adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/transaction/SpringMongoTransactionExecutor.java:57: return coordinator.execute(profile, work);
Exit code: 0
@@ -0,0 +1,118 @@
Command: printf '%s\n' '--- EvidenceAware manager refs ---'; git grep -n -w 'EvidenceAwareJpaTransactionManager' -- 'src/**/src/main/java/**/*.java'; printf '%s\n' '--- actual construction calls ---'; git grep -n -E 'EvidenceAwareJpaTransactionManager\.standard|new EvidenceAwareJpaTransactionManager' -- 'src/**/src/main/java/**/*.java' || true; printf '%s\n' '--- recorder/record refs ---'; git grep -n -E '\bCompletionUnknown(Recorder|Record)\b' -- 'src/**/src/main/java/**/*.java' || true; printf '%s\n' '--- metric implementation/calls ---'; git grep -n -E 'jpa\.transaction\.completion\.unknown|recordCompletionUnknown|JpaTransactionObservation' -- 'src/**/src/main/java/**/*.java' 'docs/jpa/**'; printf '%s\n' '--- observability composition refs ---'; git grep -n -w 'JpaObservabilityAutoConfiguration' -- 'src/**/src/main/java/**/*.java' || true; printf '%s\n' '--- application indeterminate path ---'; sed -n '105,185p' src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringPolicyTransactionPort.java
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T08:01:16Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Traces current production reachability for the custom evidence-aware transaction manager, completion-unknown recorder/record, transaction observation, completion-unknown meter, and application indeterminate result. It does not simulate a real lost-commit-ack database/network failure.
--- stdout/stderr ---
--- EvidenceAware manager refs ---
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java:24:public class EvidenceAwareJpaTransactionManager extends JpaTransactionManager {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java:30: public EvidenceAwareJpaTransactionManager(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java:37: public static EvidenceAwareJpaTransactionManager standard(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java:39: return new EvidenceAwareJpaTransactionManager(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:55: * EvidenceAwareJpaTransactionManager.standard(entityManagerFactory, clock)}. The composition root
--- actual construction calls ---
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java:39: return new EvidenceAwareJpaTransactionManager(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java:55: * EvidenceAwareJpaTransactionManager.standard(entityManagerFactory, clock)}. The composition root
--- recorder/record refs ---
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecord.java:19:public record CompletionUnknownRecord(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecord.java:28: public CompletionUnknownRecord {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecord.java:41: public static CompletionUnknownRecord from(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecord.java:44: return new CompletionUnknownRecord(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecorder.java:13:public interface CompletionUnknownRecorder {
--- metric implementation/calls ---
docs/jpa/observability.md:23:| `jpa.transaction.completion.unknown` | its own counter, deliberately |
docs/jpa/runbooks.md:7:**Signal:** `jpa.transaction.completion.unknown` incremented; a `CompletionUnknownRecord` in the
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaTransactionObservation.java:19:public final class JpaTransactionObservation {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaTransactionObservation.java:31: public static final String COMPLETION_UNKNOWN_METER = "jpa.transaction.completion.unknown";
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaTransactionObservation.java:36: public JpaTransactionObservation(MeterRegistry registry, String persistenceUnit) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaTransactionObservation.java:79: public void recordCompletionUnknown(PersistenceOperationName operation) {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaObservabilityAutoConfiguration.java:7:import dev.caskeleton.adapter.outbound.persistence.observation.JpaTransactionObservation;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaObservabilityAutoConfiguration.java:49: public Optional<JpaTransactionObservation> transactionObservation() {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaObservabilityAutoConfiguration.java:50: return registry.map(meters -> new JpaTransactionObservation(meters, persistenceUnit));
--- observability composition refs ---
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaObservabilityAutoConfiguration.java:26:public final class JpaObservabilityAutoConfiguration {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaObservabilityAutoConfiguration.java:35: public JpaObservabilityAutoConfiguration(
--- application indeterminate path ---
T value;
try {
value = action.get();
} catch (RuntimeException actionFailure) {
return new AttemptResult<>(
rollback(status, request.operationId(), tracker, actionFailure), physicalOwner);
}
tracker.observe(TransactionPhase.COMMIT_REQUESTED);
try {
transactionManager.commit(status);
} catch (RuntimeException commitFailure) {
if (sentinel.commitAcknowledged()) {
return new AttemptResult<>(
new TransactionResult.CommittedWithPostCommitFailure<>(
value, request.operationId(), commitFailure),
physicalOwner);
}
if (sentinel.rolledBack()
|| commitFailure instanceof UnexpectedRollbackException
|| TransactionRetryClassifier.isReplayCandidate(commitFailure)) {
return new AttemptResult<>(
new TransactionResult.DeterminateRollback<>(translate(commitFailure)), physicalOwner);
}
return new AttemptResult<>(
new TransactionResult.Indeterminate<>(
request.operationId(), tracker.lastObserved(), Optional.empty()),
physicalOwner);
}
if (!physicalOwner) {
return new AttemptResult<>(new TransactionResult.Participating<>(value), false);
}
tracker.observe(TransactionPhase.COMMIT_ACKED);
return new AttemptResult<>(
new TransactionResult.Committed<>(value, request.operationId()), true);
}
private boolean shouldRetry(
TransactionPolicyId policyId, AttemptResult<?> attemptResult, int attempt) {
if (policyId != TransactionPolicyId.COMMAND_SERIALIZABLE_REPLAY_SAFE
|| !attemptResult.physicalOwner()
|| attempt >= retryBackoff.maximumAttempts()
|| Thread.currentThread().isInterrupted()) {
return false;
}
TransactionResult<?> result = attemptResult.result();
if (result instanceof TransactionResult.DeterminateRollback<?> rollback) {
return TransactionRetryClassifier.isReplayCandidate(rollback.failure());
}
return false;
}
private <T> TransactionResult<T> rollback(
TransactionStatus status,
Optional<OperationId> operationId,
PhaseTracker tracker,
RuntimeException actionFailure) {
try {
transactionManager.rollback(status);
return new TransactionResult.DeterminateRollback<>(translate(actionFailure));
} catch (RuntimeException rollbackFailure) {
actionFailure.addSuppressed(rollbackFailure);
return new TransactionResult.Indeterminate<>(
operationId, tracker.lastObserved(), Optional.empty());
}
}
private RuntimeException translate(RuntimeException failure) {
return exceptionTranslator.translate(failure).map(RuntimeException.class::cast).orElse(failure);
}
private DefaultTransactionDefinition definition(
TransactionPolicyId policyId, TransactionStartBudget startBudget, PolicySpec policy) {
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setName("application-" + policyId.name().toLowerCase(Locale.ROOT));
definition.setPropagationBehavior(policy.propagation());
definition.setIsolationLevel(policy.isolation());
definition.setReadOnly(policy.readOnly());
definition.setTimeout(startBudget.springTimeoutSeconds());
return definition;
Exit code: 0
@@ -0,0 +1,20 @@
Command: echo '=== evidence manager ==='; git log --all --oneline --follow -- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java; echo '=== evidence manager construction symbol ==='; git log --all --oneline -S'EvidenceAwareJpaTransactionManager.standard' -- src; echo '=== completion recorder ==='; git log --all --oneline --follow -- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CompletionUnknownRecorder.java; echo '=== coordinator ==='; git log --all --oneline --follow -- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java; echo '=== canonical PolicyTransactionPort phrase ==='; git log --all --oneline -S'PolicyTransactionPort.inTransaction' -- src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T08:01:16Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Repository history for evidence manager, completion recorder, coordinator, and the later PolicyTransactionPort canonical-boundary statement. Commit history establishes evolution but not unstated intent.
--- stdout/stderr ---
=== evidence manager ===
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
0e61f86 feat(jpa): implement the JPA relational persistence platform
=== evidence manager construction symbol ===
0e61f86 feat(jpa): implement the JPA relational persistence platform
=== completion recorder ===
0e61f86 feat(jpa): implement the JPA relational persistence platform
=== coordinator ===
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
0e61f86 feat(jpa): implement the JPA relational persistence platform
=== canonical PolicyTransactionPort phrase ===
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
Exit code: 0
@@ -0,0 +1,11 @@
Command: java source-file probe with current compiled persistence/application/shared classes and Spring transaction dependencies
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T08:01:29Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Supplies a custom JpaRetryPolicy that always fails and counts invocations to FullTransactionRetryCoordinator, then executes a valid TransactionProfile with its mandatory RetryProfile. This isolates whether the application-supplied policy participates in valid coordinator execution; it does not model a real database.
--- stdout/stderr ---
thrown=SERIALIZATION_FAILURE
customPolicyCalls=0
workCalls=2
Exit code: 0
@@ -0,0 +1,43 @@
import dev.caskeleton.adapter.outbound.persistence.api.PersistenceOperationName;
import dev.caskeleton.adapter.outbound.persistence.api.error.JpaFailureContext;
import dev.caskeleton.adapter.outbound.persistence.api.error.SerializationFailureException;
import dev.caskeleton.adapter.outbound.persistence.api.transaction.*;
import dev.caskeleton.adapter.outbound.persistence.transaction.*;
import java.time.*;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.transaction.*;
import org.springframework.transaction.support.SimpleTransactionStatus;
public class JpaCustomRetryPolicyProbe {
public static void main(String[] args) {
AtomicInteger customPolicyCalls = new AtomicInteger();
AtomicInteger workCalls = new AtomicInteger();
PlatformTransactionManager tm = new PlatformTransactionManager() {
public TransactionStatus getTransaction(TransactionDefinition d) { return new SimpleTransactionStatus(true); }
public void commit(TransactionStatus s) {}
public void rollback(TransactionStatus s) {}
};
Clock clock = Clock.fixed(Instant.parse("2026-08-29T00:00:00Z"), ZoneOffset.UTC);
SpringJpaTransactionExecutor executor = new SpringJpaTransactionExecutor(tm, clock);
JpaRetryPolicy custom = (failure, attempt) -> {
customPolicyCalls.incrementAndGet();
return RetryDecision.fail("custom-policy-called");
};
FullTransactionRetryCoordinator coordinator = new FullTransactionRetryCoordinator(
executor, custom, ignored -> {}, clock, Duration.ofSeconds(30), RetryEventListener.noop());
RetryProfile retry = RetryProfile.boundedContention("probe.write", 2);
TransactionProfile profile = TransactionProfile.write("probe.write", Duration.ofSeconds(5)).withRetryProfile(retry);
PersistenceOperationName operation = new PersistenceOperationName("probe.write");
try {
coordinator.execute(operation, profile, () -> {
int call = workCalls.incrementAndGet();
throw new SerializationFailureException(
JpaFailureContext.retryable(operation, "40001", call, Duration.ZERO, null));
});
} catch (SerializationFailureException expected) {
System.out.println("thrown=" + expected.category());
}
System.out.println("customPolicyCalls=" + customPolicyCalls.get());
System.out.println("workCalls=" + workCalls.get());
}
}
@@ -0,0 +1,39 @@
Command: ./gradlew :adapter:outbound:persistence-jpa:test --tests 'dev.caskeleton.adapter.outbound.persistence.transaction.*' --tests 'dev.caskeleton.adapter.outbound.persistence.failure.*' --rerun-tasks --console=plain
Working directory: /shared/codebase/clean-architecture-backend-template/src
Executed at: 2026-08-29T08:01:40Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Fresh execution of all dedicated transaction and failure package tests in the persistence-jpa leaf; it is unit/focused evidence and does not reproduce a real database commit acknowledgement loss.
--- stdout/stderr ---
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :shared-contract:compileJava
> Task :shared-contract:processResources
> Task :shared-contract:classes
> Task :shared-contract:jar
> Task :application-core:compileJava
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes
> Task :application-core:jar
> Task :adapter:outbound:persistence-jpa:compileJava
> Task :adapter:outbound:persistence-jpa:processResources
> Task :adapter:outbound:persistence-jpa:classes
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses
> Task :adapter:outbound:persistence-jpa:compileTestJava
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testClasses
> Task :adapter:outbound:persistence-jpa:test
BUILD SUCCESSFUL in 26s
18 actionable tasks: 18 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
Exit code: 0
@@ -0,0 +1,174 @@
Command: ./gradlew :app-bootstrap:test --tests dev.caskeleton.bootstrap.autoconfigure.jpa.JpaPlatformAddonAssemblyTest --tests dev.caskeleton.bootstrap.autoconfigure.jpa.JpaPlatformRuntimeAutoConfigurationTest --tests dev.caskeleton.bootstrap.autoconfigure.persistencejpa.CapabilityEntityScanRegistrationTest --rerun-tasks --console=plain
Working directory: /shared/codebase/clean-architecture-backend-template/src
Executed at: 2026-08-29T08:02:07Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Fresh app-bootstrap tests for JPA add-on assembly, runtime executor/coordinator composition, and persistence root import/entity-scan registration. These are composition tests, not a real PostgreSQL lost-ack qualification.
--- stdout/stderr ---
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :shared-contract:compileJava
> Task :shared-contract:processResources
> Task :shared-contract:classes
> Task :shared-contract:jar
> Task :application-core:compileJava
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes
> Task :application-core:jar
> Task :domain-core:compileJava
> Task :domain-core:processResources NO-SOURCE
> Task :domain-core:classes
> Task :domain-core:jar
> Task :messaging:messaging-core-api:compileJava
> Task :messaging:messaging-observability:compileJava
> Task :messaging:messaging-schema-api:compileJava
> Task :messaging:messaging-policy:compileJava
> Task :messaging:messaging-reliability-api:compileJava
> Task :messaging:messaging-security:compileJava
> Task :messaging:messaging-admin-api:compileJava
> Task :messaging:messaging-transport-spi:compileJava
> Task :messaging:messaging-admin-runtime:compileJava
> Task :messaging:messaging-claim-check:compileJava
> Task :messaging:messaging-cloudevents:compileJava
> Task :messaging:messaging-inbox-jdbc-postgresql:compileJava
> Task :messaging:messaging-kafka:compileJava
> Task :messaging:messaging-outbox-jdbc-postgresql:compileJava
> Task :messaging:messaging-rabbit:compileJava
> Task :messaging:messaging-runtime-core:compileJava
> Task :messaging:messaging-schema-json:compileJava
> Task :messaging:messaging-spring-boot-starter:compileJava
> Task :adapter:inbound:graphql:compileJava
> Task :adapter:inbound:graphql:processResources
> Task :adapter:inbound:graphql:classes
> Task :adapter:inbound:graphql:jar
> Task :adapter:inbound:web:compileJava
> Task :adapter:inbound:web:processResources
> Task :adapter:inbound:web:classes
> Task :adapter:inbound:web:jar
> Task :adapter:outbound:support:compileJava
> Task :adapter:outbound:support:processResources NO-SOURCE
> Task :adapter:outbound:support:classes
> Task :adapter:outbound:support:jar
> Task :adapter:outbound:cache-redis:compileJava
> Task :adapter:outbound:cache-redis:processResources
> Task :adapter:outbound:cache-redis:classes
> Task :adapter:outbound:cache-redis:jar
> Task :adapter:outbound:fileserver:compileJava
> Task :adapter:outbound:fileserver:processResources NO-SOURCE
> Task :adapter:outbound:fileserver:classes
> Task :adapter:outbound:fileserver:jar
> Task :adapter:outbound:httpclient:compileJava
> Task :adapter:outbound:httpclient:processResources NO-SOURCE
> Task :adapter:outbound:httpclient:classes
> Task :adapter:outbound:httpclient:jar
> Task :adapter:outbound:identifier:compileJava
> Task :adapter:outbound:identifier:compileGroovy NO-SOURCE
> Task :adapter:outbound:identifier:processResources NO-SOURCE
> Task :adapter:outbound:identifier:classes
> Task :adapter:outbound:identifier:jar
> Task :adapter:outbound:messaging:compileJava
> Task :adapter:outbound:messaging:processResources
> Task :adapter:outbound:messaging:classes
> Task :adapter:outbound:messaging:jar
> Task :adapter:outbound:notification:compileJava
> Task :adapter:outbound:notification:processResources NO-SOURCE
> Task :adapter:outbound:notification:classes
> Task :adapter:outbound:notification:jar
> Task :adapter:outbound:persistence-jpa:compileJava
> Task :adapter:outbound:persistence-jpa:processResources
> Task :adapter:outbound:persistence-jpa:classes
> Task :adapter:outbound:persistence-jpa:jar
> Task :adapter:outbound:persistence-mongo:compileJava
> Task :adapter:outbound:persistence-mongo:processResources
> Task :adapter:outbound:persistence-mongo:classes
> Task :adapter:outbound:persistence-mongo:jar
> Task :app-bootstrap:compileJava
> Task :app-bootstrap:processResources
> Task :app-bootstrap:classes
> Task :sample-portfolio:compileJava
> Task :sample-portfolio:processResources
> Task :sample-portfolio:classes
> Task :sample-portfolio:jar
> Task :adapter:inbound:web:compileTestkitJava
> Task :adapter:inbound:web:processTestkitResources
> Task :adapter:inbound:web:testkitClasses
> Task :adapter:inbound:web:testkitJar
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses
> Task :adapter:outbound:persistence-jpa:testkitJar
> Task :app-bootstrap:compileTestJava
> Task :app-bootstrap:runtimeClasspathManifest
> Task :app-bootstrap:processTestResources
> Task :app-bootstrap:testClasses
> Task :messaging:messaging-admin-api:processResources NO-SOURCE
> Task :messaging:messaging-admin-api:classes
> Task :messaging:messaging-admin-api:jar
> Task :messaging:messaging-admin-runtime:processResources NO-SOURCE
> Task :messaging:messaging-admin-runtime:classes
> Task :messaging:messaging-admin-runtime:jar
> Task :messaging:messaging-claim-check:processResources NO-SOURCE
> Task :messaging:messaging-claim-check:classes
> Task :messaging:messaging-claim-check:jar
> Task :messaging:messaging-cloudevents:processResources NO-SOURCE
> Task :messaging:messaging-cloudevents:classes
> Task :messaging:messaging-cloudevents:jar
> Task :messaging:messaging-core-api:processResources NO-SOURCE
> Task :messaging:messaging-core-api:classes
> Task :messaging:messaging-core-api:jar
> Task :messaging:messaging-inbox-jdbc-postgresql:processResources
> Task :messaging:messaging-inbox-jdbc-postgresql:classes
> Task :messaging:messaging-inbox-jdbc-postgresql:jar
> Task :messaging:messaging-kafka:processResources NO-SOURCE
> Task :messaging:messaging-kafka:classes
> Task :messaging:messaging-kafka:jar
> Task :messaging:messaging-observability:processResources NO-SOURCE
> Task :messaging:messaging-observability:classes
> Task :messaging:messaging-observability:jar
> Task :messaging:messaging-outbox-jdbc-postgresql:processResources
> Task :messaging:messaging-outbox-jdbc-postgresql:classes
> Task :messaging:messaging-outbox-jdbc-postgresql:jar
> Task :messaging:messaging-policy:processResources NO-SOURCE
> Task :messaging:messaging-policy:classes
> Task :messaging:messaging-policy:jar
> Task :messaging:messaging-rabbit:processResources NO-SOURCE
> Task :messaging:messaging-rabbit:classes
> Task :messaging:messaging-rabbit:jar
> Task :messaging:messaging-reliability-api:processResources NO-SOURCE
> Task :messaging:messaging-reliability-api:classes
> Task :messaging:messaging-reliability-api:jar
> Task :messaging:messaging-runtime-core:processResources NO-SOURCE
> Task :messaging:messaging-runtime-core:classes
> Task :messaging:messaging-runtime-core:jar
> Task :messaging:messaging-schema-api:processResources NO-SOURCE
> Task :messaging:messaging-schema-api:classes
> Task :messaging:messaging-schema-api:jar
> Task :messaging:messaging-schema-json:processResources NO-SOURCE
> Task :messaging:messaging-schema-json:classes
> Task :messaging:messaging-schema-json:jar
> Task :messaging:messaging-security:processResources NO-SOURCE
> Task :messaging:messaging-security:classes
> Task :messaging:messaging-security:jar
> Task :messaging:messaging-spring-boot-starter:processResources
> Task :messaging:messaging-spring-boot-starter:classes
> Task :messaging:messaging-spring-boot-starter:jar
> Task :messaging:messaging-transport-spi:processResources NO-SOURCE
> Task :messaging:messaging-transport-spi:classes
> Task :messaging:messaging-transport-spi:jar
> Task :adapter:outbound:objectstorage:compileJava
> Task :adapter:outbound:objectstorage:processResources NO-SOURCE
> Task :adapter:outbound:objectstorage:classes
> Task :adapter:outbound:objectstorage:jar
> Task :app-bootstrap:test
BUILD SUCCESSFUL in 1m 32s
98 actionable tasks: 98 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
Exit code: 0
@@ -0,0 +1,12 @@
Command: java source-file probe using a fake PlatformTransactionManager whose commit throws SQLSTATE 08006, current SpringJpaTransactionExecutor, and current PostgreSQL vendor translator
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T08:04:09Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Reproduces classification when commit acknowledgement is lost through a normal PlatformTransactionManager plus current vendor translator. It does not simulate a real PostgreSQL socket loss or prove the database actually committed.
--- stdout/stderr ---
type=ConnectionUnavailableException
category=CONNECTION_UNAVAILABLE
completionUnknown=false
retryable=false
Exit code: 0
@@ -0,0 +1,42 @@
import dev.caskeleton.adapter.outbound.persistence.api.PersistenceOperationName;
import dev.caskeleton.adapter.outbound.persistence.api.error.JpaPersistenceException;
import dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionProfile;
import dev.caskeleton.adapter.outbound.persistence.api.error.ConstraintCode;
import dev.caskeleton.adapter.outbound.persistence.postgresql.error.PostgreSqlExceptionTranslator;
import dev.caskeleton.adapter.outbound.persistence.transaction.OptimisticConflictTranslator;
import dev.caskeleton.adapter.outbound.persistence.transaction.PersistenceFailureTranslatorChain;
import dev.caskeleton.adapter.outbound.persistence.transaction.SpringJpaTransactionExecutor;
import java.sql.SQLException;
import java.time.*;
import org.springframework.transaction.*;
import org.springframework.transaction.support.SimpleTransactionStatus;
public class JpaCommitAmbiguityProbe {
public static void main(String[] args) {
PlatformTransactionManager tm = new PlatformTransactionManager() {
public TransactionStatus getTransaction(TransactionDefinition d) { return new SimpleTransactionStatus(true); }
public void commit(TransactionStatus s) {
throw new TransactionSystemException("commit acknowledgement lost", new SQLException("connection lost", "08006"));
}
public void rollback(TransactionStatus s) {}
};
var translator = PostgreSqlExceptionTranslator.with(name -> new ConstraintCode("unknown.constraint"));
var executor = new SpringJpaTransactionExecutor(
tm,
Clock.fixed(Instant.parse("2026-08-29T00:00:00Z"), ZoneOffset.UTC),
new PersistenceFailureTranslatorChain(OptimisticConflictTranslator.withoutCatalog(), translator));
try {
executor.execute(
new PersistenceOperationName("probe.commit"),
TransactionProfile.write("probe.commit", Duration.ofSeconds(5)),
() -> "written",
1,
"reconcile-key-1");
} catch (JpaPersistenceException ex) {
System.out.println("type=" + ex.getClass().getSimpleName());
System.out.println("category=" + ex.category());
System.out.println("completionUnknown=" + ex.completionUnknown());
System.out.println("retryable=" + ex.retryable());
}
}
}
@@ -0,0 +1,55 @@
Command: current reachability and history for JpaTransactionConfig / TransactionProfileRegistry / removed declarative retry interceptor
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T08:05:13Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Distinguishes a zero-direct-reference Spring configuration reached by component scan from a zero-production-reference registry left after declarative retry interceptor removal. It does not prove external consumers of non-api public classes.
--- stdout/stderr ---
=== JpaTransactionConfig current refs ===
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/config/JpaAdapterComponentsConfig.java:51: * same package — {@code JpaTransactionConfig} for {@code JpaTransactionSettings}, {@code
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/JpaTransactionConfig.java:24:public class JpaTransactionConfig {}
=== adapter component scan ===
* the JDBC one — and was registered by nothing but a test calling {@code ctx.register(...)}. So a
* single-instance deployment had no {@code DistributedLockPort} at all, and a multi-instance one
* could not start: the composition root's own {@code DistributedLockConfig} asks for a bean
* qualified {@code jdbcDistributedLock} that only that configuration declares.
*
* <p>Each package's {@code @ConfigurationProperties} type is enabled by a configuration inside that
* same package — {@code JpaTransactionConfig} for {@code JpaTransactionSettings}, {@code
* DistributedLockPersistenceConfig} for {@code LockSettings} — rather than from here. Enabling them
* centrally would give {@code config} an edge to {@code lock} and {@code transaction} that the
* module map does not grant it, and the map is right: this class knows which packages to scan, not
* what is inside them. They need enabling at all because {@code @ConfigurationPropertiesScan}
* excludes this tree as deliberately as {@code @ComponentScan} does.
*/
@Configuration(proxyBeanMethods = false)
@ComponentScan(
basePackages = {
"dev.caskeleton.adapter.outbound.persistence.audit",
"dev.caskeleton.adapter.outbound.persistence.failure",
"dev.caskeleton.adapter.outbound.persistence.idempotency",
"dev.caskeleton.adapter.outbound.persistence.lock",
"dev.caskeleton.adapter.outbound.persistence.outbox",
"dev.caskeleton.adapter.outbound.persistence.transaction"
})
public class JpaAdapterComponentsConfig {}
=== TransactionProfileRegistry current refs ===
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistry.java:16:public final class TransactionProfileRegistry {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistry.java:20: public TransactionProfileRegistry(Map<String, TransactionProfile> profiles) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistry.java:37: public static TransactionProfileRegistry of(TransactionProfile... profiles) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistry.java:44: return new TransactionProfileRegistry(byName);
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistryTest.java:14: private final TransactionProfileRegistry registry =
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistryTest.java:15: TransactionProfileRegistry.of(
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistryTest.java:39: TransactionProfileRegistry.of(
=== removed interceptor history ===
COMMIT 2f5d2fc21954286213c1474d19935f571ef896ea feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
D src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/RetryableJpaTransactionInterceptor.java
D src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/RetryableJpaTransactionInterceptorTest.java
M src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
COMMIT 0e61f86eb58512fb117093c359492d2fe2958382 feat(jpa): implement the JPA relational persistence platform
A src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/RetryableJpaTransactionInterceptor.java
A src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/RetryableJpaTransactionInterceptorTest.java
A src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java
Exit code: 0
@@ -0,0 +1,39 @@
Command: ./gradlew :adapter:outbound:persistence-jpa:test --tests 'dev.caskeleton.adapter.outbound.persistence.hibernate.*' --tests 'dev.caskeleton.adapter.outbound.persistence.springdata.*' --tests 'dev.caskeleton.adapter.outbound.persistence.querydsl.*' --rerun-tasks --console=plain
Working directory: /shared/codebase/clean-architecture-backend-template/src
Executed at: 2026-08-29T08:21:21Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Fresh focused unit tests for the 53-file hibernate/springdata/querydsl bounded sub-scope; does not prove real PostgreSQL behavior or runtime composition.
--- stdout/stderr ---
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :shared-contract:compileJava
> Task :shared-contract:processResources
> Task :shared-contract:classes
> Task :shared-contract:jar
> Task :application-core:compileJava
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes
> Task :application-core:jar
> Task :adapter:outbound:persistence-jpa:compileJava
> Task :adapter:outbound:persistence-jpa:processResources
> Task :adapter:outbound:persistence-jpa:classes
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses
> Task :adapter:outbound:persistence-jpa:compileTestJava
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testClasses
> Task :adapter:outbound:persistence-jpa:test
BUILD SUCCESSFUL in 23s
18 actionable tasks: 18 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
Exit code: 0
@@ -0,0 +1,12 @@
Command: java source-file probe for BatchExecutionResult.batched, property-access IDENTITY detection, and non-null Specification returning null Predicate
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T08:21:49Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Pure contract/guard probe against current compiled classes; no real database. The Specification lambda is deliberately non-null but returns null Predicate.
--- stdout/stderr ---
oneJdbcBatch.batched=false
propertyIdentityDetected=false
propertyIdentityValidation=PASSED
nonNullNullPredicateSpecification=PASSED
Exit code: 0
@@ -0,0 +1,38 @@
import dev.caskeleton.adapter.outbound.persistence.hibernate.batch.*;
import dev.caskeleton.adapter.outbound.persistence.springdata.SpecificationPolicy;
import jakarta.persistence.*;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;
public class JpaQueryHibernateBoundaryProbe {
static class PropertyIdentityEntity {
private Long id;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
}
public static void main(String[] args) {
JpaBatchProfile profile = JpaBatchProfile.uniform("probe.batch", 10);
BatchExecutionResult oneBatch = new BatchExecutionResult(profile, 10, 1, 1, 1, 10);
System.out.println("oneJdbcBatch.batched=" + oneBatch.batched());
HibernateBatchConfigurationGuard guard = new HibernateBatchConfigurationGuard();
System.out.println("propertyIdentityDetected=" + guard.usesIdentityGeneration(PropertyIdentityEntity.class));
try {
guard.validate(profile, PropertyIdentityEntity.class);
System.out.println("propertyIdentityValidation=PASSED");
} catch (Exception e) {
System.out.println("propertyIdentityValidation=" + e.getClass().getSimpleName());
}
Specification<Object> noPredicate = (root, query, cb) -> null;
try {
SpecificationPolicy.requireBounded(noPredicate, PageRequest.of(0, 20), null);
System.out.println("nonNullNullPredicateSpecification=PASSED");
} catch (Exception e) {
System.out.println("nonNullNullPredicateSpecification=" + e.getClass().getSimpleName());
}
}
}
@@ -0,0 +1,181 @@
Command: static reachability/wiring/export/package evidence for hibernate, springdata, querydsl
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T08:22:05Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Tracked production Java/static configuration only. Zero references do not exclude reflection/external adopters; package export rules and test/integration consumers are shown separately.
--- stdout/stderr ---
=== package denominator ===
hibernate main count= 22 loc= 1111
hibernate test count= 2 loc= 149
springdata main count= 17 loc= 1028
springdata test count= 8 loc= 598
querydsl main count= 3 loc= 139
querydsl test count= 1 loc= 53
=== key implementation production refs ===
--- NamedStatementInspector
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/NamedStatementInspector.java:20:public final class NamedStatementInspector implements StatementInspector {
--- HibernateBatchConfigurationGuard
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/HibernateBatchConfigurationGuard.java:23:public final class HibernateBatchConfigurationGuard {
--- HibernateJpaBatchExecutor
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/HibernateJpaBatchExecutor.java:22:public final class HibernateJpaBatchExecutor implements JpaBatchExecutor {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/HibernateJpaBatchExecutor.java:27: public HibernateJpaBatchExecutor(
--- JpaBatchProfileRegistry
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/JpaBatchProfileRegistry.java:14:public final class JpaBatchProfileRegistry {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/JpaBatchProfileRegistry.java:18: public JpaBatchProfileRegistry(Map<String, JpaBatchProfile> profiles) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/JpaBatchProfileRegistry.java:35: public static JpaBatchProfileRegistry of(JpaBatchProfile... profiles) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/JpaBatchProfileRegistry.java:42: return new JpaBatchProfileRegistry(byName);
--- HibernateBulkDmlExecutor
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/bulk/HibernateBulkDmlExecutor.java:28:public final class HibernateBulkDmlExecutor implements BulkDmlExecutor {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/bulk/HibernateBulkDmlExecutor.java:38: public HibernateBulkDmlExecutor(
--- HibernateStatelessSessionRunner
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/stateless/HibernateStatelessSessionRunner.java:23:public final class HibernateStatelessSessionRunner implements StatelessSessionRunner {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/stateless/HibernateStatelessSessionRunner.java:28: public HibernateStatelessSessionRunner(
--- EntityGraphCatalog
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/EntityGraphCatalog.java:20:public final class EntityGraphCatalog {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/EntityGraphCatalog.java:25: public EntityGraphCatalog(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/EntityGraphCatalog.java:40: public static EntityGraphCatalog empty() {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/EntityGraphCatalog.java:41: return new EntityGraphCatalog(Map.of());
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/FetchPlanApplier.java:30: private final EntityGraphCatalog catalog;
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/FetchPlanApplier.java:33: public FetchPlanApplier(EntityGraphCatalog catalog, EntityManager entityManager) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/FetchPlanApplier.java:61: public EntityGraphCatalog catalog() {
--- FetchPlanApplier
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/FetchPlanApplier.java:22:public final class FetchPlanApplier {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/FetchPlanApplier.java:33: public FetchPlanApplier(EntityGraphCatalog catalog, EntityManager entityManager) {
--- JpaKeysetQuerySupport
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaKeysetQuerySupport.java:20:public final class JpaKeysetQuerySupport {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaKeysetQuerySupport.java:24: public JpaKeysetQuerySupport(KeysetSliceAssembler assembler) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaKeysetQuerySupport.java:29: public static JpaKeysetQuerySupport standard() {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaKeysetQuerySupport.java:30: return new JpaKeysetQuerySupport(new KeysetSliceAssembler());
--- JpaRepositoryFragmentSupport
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaRepositoryFragmentSupport.java:24:public abstract class JpaRepositoryFragmentSupport {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaRepositoryFragmentSupport.java:31: protected JpaRepositoryFragmentSupport(EntityManager entityManager) {
--- JpaStreamExecutor
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaStreamExecutor.java:27:public final class JpaStreamExecutor {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaStreamExecutor.java:41: public JpaStreamExecutor(QueryObservation observation) {
--- KeysetPredicateBuilder
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/KeysetPredicateBuilder.java:32:public final class KeysetPredicateBuilder {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/KeysetPredicateBuilder.java:34: private KeysetPredicateBuilder() {}
--- SpecificationPolicy
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/SpecificationPolicy.java:18:public final class SpecificationPolicy {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/SpecificationPolicy.java:23: private SpecificationPolicy() {}
--- QuerydslJpaSupport
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/querydsl/QuerydslJpaSupport.java:21:public final class QuerydslJpaSupport {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/querydsl/QuerydslJpaSupport.java:28: public QuerydslJpaSupport(JPAQueryFactory queryFactory) {
=== querydsl lock/declarations ===
43-com.puppycrawl.tools:checkstyle:13.5.0=checkstyle
44-com.querydsl:querydsl-core:5.1.0=compileClasspath,jpaPlatformPerformanceTestCompileClasspath,jpaPlatformPerformanceTestRuntimeClasspath,postgresqlIntegrationTestCompileClasspath,postgresqlIntegrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
45:com.querydsl:querydsl-jpa:5.1.0=compileClasspath,jpaPlatformPerformanceTestCompileClasspath,jpaPlatformPerformanceTestRuntimeClasspath,postgresqlIntegrationTestCompileClasspath,postgresqlIntegrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
46-com.sun.istack:istack-commons-runtime:4.1.2=jpaPlatformPerformanceTestRuntimeClasspath,postgresqlIntegrationTestRuntimeClasspath,runtimeClasspath,testRuntimeClasspath,testkitRuntimeClasspath
47-com.tngtech.archunit:archunit-junit5-api:1.3.0=jpaPlatformPerformanceTestCompileClasspath,jpaPlatformPerformanceTestRuntimeClasspath,postgresqlIntegrationTestCompileClasspath,postgresqlIntegrationTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath,testkitCompileClasspath,testkitRuntimeClasspath
69: compileOnly 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
72: testImplementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
=== statement inspector configuration ===
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/NamedStatementInspector.java:20:public final class NamedStatementInspector implements StatementInspector {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/JpaRepositoryFragmentSupport.java:66: * Runs {@code work} with {@code name} bound as the statement-inspector identity.
=== export lists ===
/**
* The packages a leaf outside this one may import from.
*
* <p>An export list, because 318 of 324 production files declare a public top-level type and
* "public" inside one jar is public to everybody who has the jar. The composition root imported
* twenty-two notification implementation types by name, which turned each of them into a
* cross-leaf source contract: moving one to another package, or reducing its visibility, broke
* the root's compile. The notification wiring is now a facade the leaf owns, and this names what
* the rest of the repository may reach for.
*
* <p>Everything absent from this list is internal. It may still be {@code public} — Java has no
* finer granularity inside one jar — but importing it from another leaf is a boundary violation
* this test reports, not a compile error the language provides.
*/
private static final Set<String> EXPORTED_PACKAGES =
Set.of(
// The contracts other leaves are meant to speak.
"api",
// Capability configuration facades, each assembling its own capability's internals.
"notification.configuration",
// Cross-cutting seams a composition root legitimately wires.
"transaction",
"security",
"observation",
"migration",
"hibernate",
"fileserver",
"failure",
"config");
private static final JavaClasses PRODUCTION_CLASSES =
// Future NoSQL adapter modules (for example adapter-persistence-mongodb) must be added as
// sibling modules that implement application/domain ports directly. When such a module exists,
// add an ArchUnit rule forbidding jakarta.persistence.., org.hibernate..,
// org.springframework.data.jpa.., org.flywaydb.., and
// dev.caskeleton.adapter.outbound.persistence.postgresql..
// dependencies from that module.
/**
* The composition root reaches only the persistence leaf's exported surface.
*
* <p>318 of that leaf's 324 production files declare a public top-level type, and {@code public}
* inside one jar is public to everyone holding the jar. The root imported twenty-two notification
* implementation types by name — every store, every repository, the record mapper — which made
* each of them a cross-leaf source contract: moving one to another package, or reducing its
* visibility, broke this module's compile. The notification wiring is a facade the leaf owns now,
* and this keeps the surface from re-growing one convenient import at a time.
*
* <p>The list is the leaf's export allowlist, mirrored here because this is the consumer side of
* the same boundary. {@code JpaModuleBoundaryTest} owns the definition.
*/
@ArchTest
static final ArchRule BOOTSTRAP_USES_ONLY_THE_PERSISTENCE_EXPORT_SURFACE =
noClasses()
.that()
.resideInAPackage("dev.caskeleton.bootstrap..")
.should()
.dependOnClassesThat(
new com.tngtech.archunit.base.DescribedPredicate<>(
"a persistence type outside the exported surface") {
private static final java.util.Set<String> EXPORTED =
java.util.Set.of(
"api",
"notification.configuration",
"transaction",
"security",
"observation",
"migration",
"hibernate",
"fileserver",
"failure",
"config",
// The two vendor entry points. A vendor configuration imports the core JPA
// config rather than the reverse, so there is no single internal entry the
// composition root could gate instead — inverting the import to make one
// produced a package cycle. Admitting exactly these two, and no other
// vendor
// internals, is the narrower price.
"postgresql",
"h2");
private static final String ROOT = "dev.caskeleton.adapter.outbound.persistence.";
@Override
public boolean test(com.tngtech.archunit.core.domain.JavaClass candidate) {
String packageName = candidate.getPackageName();
if (!packageName.startsWith(ROOT)) {
return false;
}
String remainder = packageName.substring(ROOT.length());
return EXPORTED.stream()
.noneMatch(
exported ->
remainder.equals(exported) || remainder.startsWith(exported + "."));
}
})
.as(
"app-bootstrap imports only the persistence leaf's exported packages: every other"
+ " public type there is internal, and importing one freezes it as a contract.")
.allowEmptyShould(true);
/**
* A persistence adapter stores what a use case decided; it does not make the decision.
*
* <p>{@code JpaNotificationRequestStore} imported {@code NotificationSubmissionService} and
Exit code: 0
@@ -0,0 +1,178 @@
Command: ./gradlew :adapter:outbound:persistence-jpa:test --tests dev.caskeleton.adapter.outbound.persistence.JpaModuleBoundaryTest :app-bootstrap:test --tests dev.caskeleton.bootstrap.architecture.CleanArchitectureTest --rerun-tasks --console=plain
Working directory: /shared/codebase/clean-architecture-backend-template/src
Executed at: 2026-08-29T08:22:20Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Fresh module-package DAG and app-bootstrap persistence export-surface architecture tests; does not prove runtime feature usage.
--- stdout/stderr ---
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :shared-contract:compileJava
> Task :shared-contract:processResources
> Task :shared-contract:classes
> Task :shared-contract:jar
> Task :application-core:compileJava
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes
> Task :application-core:jar
> Task :adapter:outbound:persistence-jpa:compileJava
> Task :adapter:outbound:persistence-jpa:processResources
> Task :adapter:outbound:persistence-jpa:classes
> Task :adapter:outbound:persistence-jpa:jar
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses
> Task :adapter:outbound:persistence-jpa:testkitJar
> Task :adapter:outbound:persistence-jpa:compileTestJava
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testClasses
> Task :adapter:outbound:persistence-jpa:test
> Task :domain-core:compileJava
> Task :domain-core:processResources NO-SOURCE
> Task :domain-core:classes
> Task :domain-core:jar
> Task :messaging:messaging-core-api:compileJava
> Task :messaging:messaging-observability:compileJava
> Task :messaging:messaging-schema-api:compileJava
> Task :messaging:messaging-policy:compileJava
> Task :messaging:messaging-reliability-api:compileJava
> Task :messaging:messaging-security:compileJava
> Task :messaging:messaging-admin-api:compileJava
> Task :messaging:messaging-transport-spi:compileJava
> Task :messaging:messaging-admin-runtime:compileJava
> Task :messaging:messaging-claim-check:compileJava
> Task :messaging:messaging-cloudevents:compileJava
> Task :messaging:messaging-inbox-jdbc-postgresql:compileJava
> Task :messaging:messaging-kafka:compileJava
> Task :messaging:messaging-outbox-jdbc-postgresql:compileJava
> Task :messaging:messaging-rabbit:compileJava
> Task :messaging:messaging-runtime-core:compileJava
> Task :messaging:messaging-schema-json:compileJava
> Task :messaging:messaging-spring-boot-starter:compileJava
> Task :adapter:inbound:graphql:compileJava
> Task :adapter:inbound:graphql:processResources
> Task :adapter:inbound:graphql:classes
> Task :adapter:inbound:graphql:jar
> Task :adapter:inbound:web:compileJava
> Task :adapter:inbound:web:processResources
> Task :adapter:inbound:web:classes
> Task :adapter:inbound:web:jar
> Task :adapter:outbound:support:compileJava
> Task :adapter:outbound:support:processResources NO-SOURCE
> Task :adapter:outbound:support:classes
> Task :adapter:outbound:support:jar
> Task :adapter:outbound:cache-redis:compileJava
> Task :adapter:outbound:cache-redis:processResources
> Task :adapter:outbound:cache-redis:classes
> Task :adapter:outbound:cache-redis:jar
> Task :adapter:outbound:fileserver:compileJava
> Task :adapter:outbound:fileserver:processResources NO-SOURCE
> Task :adapter:outbound:fileserver:classes
> Task :adapter:outbound:fileserver:jar
> Task :adapter:outbound:httpclient:compileJava
> Task :adapter:outbound:httpclient:processResources NO-SOURCE
> Task :adapter:outbound:httpclient:classes
> Task :adapter:outbound:httpclient:jar
> Task :adapter:outbound:identifier:compileJava
> Task :adapter:outbound:identifier:compileGroovy NO-SOURCE
> Task :adapter:outbound:identifier:processResources NO-SOURCE
> Task :adapter:outbound:identifier:classes
> Task :adapter:outbound:identifier:jar
> Task :adapter:outbound:messaging:compileJava
> Task :adapter:outbound:messaging:processResources
> Task :adapter:outbound:messaging:classes
> Task :adapter:outbound:messaging:jar
> Task :adapter:outbound:notification:compileJava
> Task :adapter:outbound:notification:processResources NO-SOURCE
> Task :adapter:outbound:notification:classes
> Task :adapter:outbound:notification:jar
> Task :adapter:outbound:persistence-mongo:compileJava
> Task :adapter:outbound:persistence-mongo:processResources
> Task :adapter:outbound:persistence-mongo:classes
> Task :adapter:outbound:persistence-mongo:jar
> Task :app-bootstrap:compileJava
> Task :app-bootstrap:processResources
> Task :app-bootstrap:classes
> Task :sample-portfolio:compileJava
> Task :sample-portfolio:processResources
> Task :sample-portfolio:classes
> Task :sample-portfolio:jar
> Task :adapter:inbound:web:compileTestkitJava
> Task :adapter:inbound:web:processTestkitResources
> Task :adapter:inbound:web:testkitClasses
> Task :adapter:inbound:web:testkitJar
> Task :app-bootstrap:compileTestJava
> Task :app-bootstrap:runtimeClasspathManifest
> Task :app-bootstrap:processTestResources
> Task :app-bootstrap:testClasses
> Task :messaging:messaging-admin-api:processResources NO-SOURCE
> Task :messaging:messaging-admin-api:classes
> Task :messaging:messaging-admin-api:jar
> Task :messaging:messaging-admin-runtime:processResources NO-SOURCE
> Task :messaging:messaging-admin-runtime:classes
> Task :messaging:messaging-admin-runtime:jar
> Task :messaging:messaging-claim-check:processResources NO-SOURCE
> Task :messaging:messaging-claim-check:classes
> Task :messaging:messaging-claim-check:jar
> Task :messaging:messaging-cloudevents:processResources NO-SOURCE
> Task :messaging:messaging-cloudevents:classes
> Task :messaging:messaging-cloudevents:jar
> Task :messaging:messaging-core-api:processResources NO-SOURCE
> Task :messaging:messaging-core-api:classes
> Task :messaging:messaging-core-api:jar
> Task :messaging:messaging-inbox-jdbc-postgresql:processResources
> Task :messaging:messaging-inbox-jdbc-postgresql:classes
> Task :messaging:messaging-inbox-jdbc-postgresql:jar
> Task :messaging:messaging-kafka:processResources NO-SOURCE
> Task :messaging:messaging-kafka:classes
> Task :messaging:messaging-kafka:jar
> Task :messaging:messaging-observability:processResources NO-SOURCE
> Task :messaging:messaging-observability:classes
> Task :messaging:messaging-observability:jar
> Task :messaging:messaging-outbox-jdbc-postgresql:processResources
> Task :messaging:messaging-outbox-jdbc-postgresql:classes
> Task :messaging:messaging-outbox-jdbc-postgresql:jar
> Task :messaging:messaging-policy:processResources NO-SOURCE
> Task :messaging:messaging-policy:classes
> Task :messaging:messaging-policy:jar
> Task :messaging:messaging-rabbit:processResources NO-SOURCE
> Task :messaging:messaging-rabbit:classes
> Task :messaging:messaging-rabbit:jar
> Task :messaging:messaging-reliability-api:processResources NO-SOURCE
> Task :messaging:messaging-reliability-api:classes
> Task :messaging:messaging-reliability-api:jar
> Task :messaging:messaging-runtime-core:processResources NO-SOURCE
> Task :messaging:messaging-runtime-core:classes
> Task :messaging:messaging-runtime-core:jar
> Task :messaging:messaging-schema-api:processResources NO-SOURCE
> Task :messaging:messaging-schema-api:classes
> Task :messaging:messaging-schema-api:jar
> Task :messaging:messaging-schema-json:processResources NO-SOURCE
> Task :messaging:messaging-schema-json:classes
> Task :messaging:messaging-schema-json:jar
> Task :messaging:messaging-security:processResources NO-SOURCE
> Task :messaging:messaging-security:classes
> Task :messaging:messaging-security:jar
> Task :messaging:messaging-spring-boot-starter:processResources
> Task :messaging:messaging-spring-boot-starter:classes
> Task :messaging:messaging-spring-boot-starter:jar
> Task :messaging:messaging-transport-spi:processResources NO-SOURCE
> Task :messaging:messaging-transport-spi:classes
> Task :messaging:messaging-transport-spi:jar
> Task :adapter:outbound:objectstorage:compileJava
> Task :adapter:outbound:objectstorage:processResources NO-SOURCE
> Task :adapter:outbound:objectstorage:classes
> Task :adapter:outbound:objectstorage:jar
> Task :app-bootstrap:test
BUILD SUCCESSFUL in 1m 48s
100 actionable tasks: 100 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
Exit code: 0
@@ -0,0 +1,278 @@
Command: static release-registry/task-tag/test-assertion provenance for collection-fetch-pagination
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T08:22:42Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Static provenance only. Shows which Gradle tag the registry task selects and what the named collection-fetch test actually asserts; does not execute PostgreSQL.
--- stdout/stderr ---
=== registry gate ===
{
"name": "runtime-role-no-ddl",
"task": ":adapter:outbound:persistence-jpa:jpaPlatformSecurityTest",
"blocking": true
},
{
"name": "collection-fetch-pagination",
"task": ":adapter:outbound:persistence-jpa:jpaPlatformQueryPlanTest",
"blocking": true
}
]
}
=== lane registration ===
// leaf's existing Docker-backed source set; the mapping is recorded in
// docs/jpa/repository-adaptation.md §3.
//
// Every lane fails closed. `failOnNoDiscoveredTests` matters more here than usual: a selected lane
// that discovers nothing reports success, and a contract suite that silently stopped running is
// indistinguishable from one that passes.
Closure<Void> registerJpaPlatformLane = { String taskName, String tag, String description ->
tasks.register(taskName, Test) {
group = 'verification'
it.description = description
testClassesDirs = sourceSets.postgresqlIntegrationTest.output.classesDirs
classpath = sourceSets.postgresqlIntegrationTest.runtimeClasspath
useJUnitPlatform {
includeTags tag
}
failOnNoDiscoveredTests = true
outputs.upToDateWhen { false }
jvmArgs('-Duser.timezone=UTC')
// The Stable matrix selection. An unknown or empty value is an error in
// PostgreSqlVersion.parseSelection rather than an empty run.
systemProperty 'jpa.matrix.versions',
(project.findProperty('jpa.matrix.versions') ?: '16').toString()
}
}
def jpaPlatformContractTest = registerJpaPlatformLane(
'jpaPlatformContractTest',
'jpa-contract',
'Runs the JPA platform contract suite against real PostgreSQL (design §40).')
def jpaPlatformMigrationTest = registerJpaPlatformLane(
'jpaPlatformMigrationTest',
'jpa-migration',
'Runs the Flyway upgrade snapshot scenarios (design §31).')
def jpaPlatformFailureTest = registerJpaPlatformLane(
'jpaPlatformFailureTest',
'jpa-failure',
'Reproduces deadlock, serialization, and commit-ambiguity failures (design §39).')
def jpaPlatformQueryPlanTest = registerJpaPlatformLane(
'jpaPlatformQueryPlanTest',
'jpa-queryplan',
'Asserts query plan structure and planner estimate error (design §33).')
def jpaPlatformSecurityTest = registerJpaPlatformLane(
'jpaPlatformSecurityTest',
'jpa-security',
'Verifies runtime role privileges and search_path safety (design §36).')
=== tags ===
src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/platform/HibernateCollectionFetchPaginationContractTest.java:26:@Tag("jpa-contract")
src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/platform/PostgreSqlQueryPlanContractTest.java:27:@Tag("jpa-queryplan")
=== collection-fetch test ===
package dev.caskeleton.adapter.outbound.persistence.platform;
import static org.assertj.core.api.Assertions.assertThat;
import dev.caskeleton.adapter.outbound.persistence.hibernate.HibernateStatisticsSnapshot;
import dev.caskeleton.adapter.outbound.persistence.testkit.fetch.FetchPaginationExpectation;
import dev.caskeleton.adapter.outbound.persistence.testkit.fetch.PagedChild;
import dev.caskeleton.adapter.outbound.persistence.testkit.fetch.PagedParent;
import java.util.List;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
/**
* The collection-fetch pagination gate (design §26).
*
* <p>The failure this exists to catch is in-memory pagination: older providers fetched every
* matching parent row and applied the page in Java, logging a warning and returning correct results
* while reading the whole table. Correct output, unbounded work, and nothing failing anywhere.
*
* <p>The assertion is therefore on the <em>generated SQL</em>, not on the returned page size. The
* page is identical either way; only the SQL says where the limit was applied.
*/
@Tag("jpa-contract")
class HibernateCollectionFetchPaginationContractTest {
private static final int PARENTS = 50;
private static final int CHILDREN_PER_PARENT = 4;
private static final int PAGE_SIZE = 20;
private static JpaPlatformContractSupport support;
private static JpaPlatformEntityManagerSupport jpa;
@BeforeAll
static void startServerAndSeed() {
support = JpaPlatformContractSupport.start();
jpa = JpaPlatformEntityManagerSupport.open(support, PagedParent.class, PagedChild.class);
jpa.inTransactionDo(
entityManager -> {
for (int parent = 0; parent < PARENTS; parent++) {
var paged = new PagedParent("parent-" + parent);
for (int child = 0; child < CHILDREN_PER_PARENT; child++) {
paged.addChild(new PagedChild("child-" + parent + '-' + child));
}
entityManager.persist(paged);
}
});
}
@AfterAll
static void stopServer() {
if (jpa != null) {
jpa.close();
}
if (support != null) {
support.close();
}
}
@Test
@DisplayName("a paged collection fetch bounds the parent selection in SQL")
void oneCollectionPageIsBoundedInSql() {
var expected = FetchPaginationExpectation.hibernate74PostgreSql(PAGE_SIZE);
List<PagedParent> page =
jpa.inTransaction(
entityManager ->
entityManager
.createQuery(
"select distinct p from PagedParent p left join fetch p.children"
+ " order by p.id",
PagedParent.class)
.setMaxResults(PAGE_SIZE)
.getResultList());
assertThat(page).hasSizeLessThanOrEqualTo(expected.maxReturnedParents());
assertThat(expected.requiresDatabaseLimit()).isTrue();
}
@Test
@DisplayName("the fetched page issues one statement, not one per parent")
void pagedFetchIssuesOneStatement() {
HibernateStatisticsSnapshot before = jpa.statistics().snapshot();
jpa.inTransactionDo(
entityManager -> {
List<PagedParent> page =
entityManager
.createQuery(
"select distinct p from PagedParent p left join fetch p.children"
+ " order by p.id",
PagedParent.class)
.setMaxResults(PAGE_SIZE)
.getResultList();
page.forEach(parent -> assertThat(parent.children()).isNotNull());
});
HibernateStatisticsSnapshot delta = jpa.statistics().snapshot().minus(before);
assertThat(delta.preparedStatements())
.as("a join fetch must not degrade into one statement per parent")
.isLessThanOrEqualTo(2L);
}
@Test
@DisplayName("without a fetch join the same access is an N+1")
void withoutFetchJoinTheAccessIsAnNplusOne() {
HibernateStatisticsSnapshot before = jpa.statistics().snapshot();
jpa.inTransactionDo(
entityManager -> {
List<PagedParent> page =
entityManager
.createQuery("select p from PagedParent p order by p.id", PagedParent.class)
=== query-plan producer test ===
package dev.caskeleton.adapter.outbound.persistence.platform;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import dev.caskeleton.adapter.outbound.persistence.testkit.queryplan.NormalizedPlan;
import dev.caskeleton.adapter.outbound.persistence.testkit.queryplan.PostgreSqlExplainRunner;
import dev.caskeleton.adapter.outbound.persistence.testkit.queryplan.QueryPlanAssertions;
import dev.caskeleton.adapter.outbound.persistence.testkit.queryplan.QueryPlanExpectation;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
/**
* Captures a real plan and asserts on its structure (design §33).
*
* <p>Structure, not cost: costs and timings differ on every execution and every machine, so a
* snapshot including them fails for reasons that have nothing to do with the query. What is stable
* is which node types appear, how far the planner's estimate was from reality, and whether a sort
* spilled to disk.
*/
@Tag("jpa-queryplan")
class PostgreSqlQueryPlanContractTest {
private static JpaPlatformContractSupport support;
private static PostgreSqlExplainRunner runner;
private final QueryPlanAssertions assertions = new QueryPlanAssertions();
@BeforeAll
static void startServer() throws SQLException {
support = JpaPlatformContractSupport.start();
runner = new PostgreSqlExplainRunner(support.dataSource());
try (Connection connection = support.connection();
Statement statement = connection.createStatement()) {
statement.execute("create table plan_row (id bigint primary key, bucket int not null)");
statement.execute(
"insert into plan_row(id, bucket) select generate_series(1, 5000),"
+ " (random() * 10)::int");
statement.execute("create index ix_plan_row_bucket on plan_row(bucket)");
statement.execute("analyze plan_row");
}
}
@AfterAll
static void stopServer() {
if (support != null) {
support.close();
}
}
@Test
@DisplayName("an indexed lookup produces a plan whose estimate is close to reality")
void indexedLookupHasAccurateEstimate() {
NormalizedPlan plan = runner.explain("select id from plan_row where id = ?", 42L);
assertions.assertMatches(
plan, QueryPlanExpectation.estimateOnly(10.0d).withDiskSortForbidden());
assertThat(plan.distinctNodeTypes()).isNotEmpty();
}
@Test
@DisplayName("a forbidden node type is reported with the whole plan")
void forbiddenNodeTypeIsReported() {
NormalizedPlan plan = runner.explain("select id from plan_row where bucket = ?", 3);
assertThatThrownBy(
() ->
assertions.assertMatches(
plan,
QueryPlanExpectation.estimateOnly(10.0d)
.forbidding(
"Bitmap Heap Scan", "Index Scan", "Seq Scan", "Index Only Scan")))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("plan ");
}
@Test
@DisplayName("EXPLAIN ANALYZE refuses anything that is not a SELECT")
void refusesNonSelect() {
assertThatThrownBy(() -> runner.explain("update plan_row set bucket = 1"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("only SELECT");
}
}
Exit code: 0
@@ -0,0 +1,39 @@
Command: ./gradlew :adapter:outbound:persistence-jpa:jpaPlatformContractTest --tests dev.caskeleton.adapter.outbound.persistence.platform.HibernateJpaBatchExecutorIntegrationTest --tests dev.caskeleton.adapter.outbound.persistence.platform.IdStrategyContractTest --tests dev.caskeleton.adapter.outbound.persistence.platform.HibernateCollectionFetchPaginationContractTest --rerun-tasks --console=plain
Working directory: /shared/codebase/clean-architecture-backend-template/src
Executed at: 2026-08-29T08:24:42Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Fresh real-PostgreSQL contract execution for batching, ID strategy, and collection-fetch pagination classes. A green result proves only the assertions those tests currently contain; it does not repair the collection-fetch SQL-limit assertion gap.
--- stdout/stderr ---
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :shared-contract:compileJava
> Task :shared-contract:processResources
> Task :shared-contract:classes
> Task :shared-contract:jar
> Task :application-core:compileJava
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes
> Task :application-core:jar
> Task :adapter:outbound:persistence-jpa:compileJava
> Task :adapter:outbound:persistence-jpa:processResources
> Task :adapter:outbound:persistence-jpa:classes
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses
> Task :adapter:outbound:persistence-jpa:jpaPlatformContractTest
BUILD SUCCESSFUL in 43s
19 actionable tasks: 19 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
Exit code: 0
@@ -0,0 +1,41 @@
Command: ./gradlew :adapter:outbound:persistence-jpa:jpaPlatformQueryPlanTest --rerun-tasks --console=plain
Working directory: /shared/codebase/clean-architecture-backend-template/src
Executed at: 2026-08-29T08:25:53Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Fresh execution of the exact Gradle task named by the collection-fetch-pagination release registry. Test-result XML is inspected afterward to identify executed classes.
--- stdout/stderr ---
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :shared-contract:compileJava
> Task :shared-contract:processResources
> Task :shared-contract:classes
> Task :shared-contract:jar
> Task :application-core:compileJava
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes
> Task :application-core:jar
> Task :adapter:outbound:persistence-jpa:compileJava
> Task :adapter:outbound:persistence-jpa:processResources
> Task :adapter:outbound:persistence-jpa:classes
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses
> Task :adapter:outbound:persistence-jpa:jpaPlatformQueryPlanTest
BUILD SUCCESSFUL in 25s
19 actionable tasks: 19 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
Exit code: 0
--- executed test XML suites ---
TEST-dev.caskeleton.adapter.outbound.persistence.platform.PostgreSqlQueryPlanContractTest.xml
@@ -0,0 +1,24 @@
Command: ./gradlew verifyJpaReleaseGateTasks --rerun-tasks --console=plain
Working directory: /shared/codebase/clean-architecture-backend-template/src
Executed at: 2026-08-29T08:26:28Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Fresh registry-to-Gradle-task validator. It verifies absolute path, project/task existence and Test task type; it does not inspect tag/selectors or semantic coverage.
--- stdout/stderr ---
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :verifyJpaReleaseGateTasks
verifyJpaReleaseGateTasks: OK — 6 gate task(s) resolve to real Test tasks.
BUILD SUCCESSFUL in 2s
9 actionable tasks: 9 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
Exit code: 0
@@ -0,0 +1,23 @@
Command: git history for collection-fetch evidence, batch/stream/stateless hardening, keyset/sort changes
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T08:25:25Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Repository history establishes when source/test shapes entered or changed; it does not infer unstated author intent.
--- stdout/stderr ---
=== collection fetch test ===
0e61f86 feat(jpa): implement the JPA relational persistence platform
=== collection fetch gate registry ===
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
=== batch flush-before-clear ===
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
=== stream policy supplier ===
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
=== stateless result row cap ===
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
=== keyset term per-direction ===
a24ece9 feat: web, websocket 어댑터 추가 구현
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
=== sort tie breaker direction ===
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
Exit code: 0
@@ -0,0 +1,462 @@
Command: javap current Spring Data JPA 4.0.7 Specification and JpaSpecificationExecutor
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T08:27:12Z
Source revision: repository a24ece9cf797f7ea647e33bf846b115208ed1ba5; dependency org.springframework.data:spring-data-jpa:4.0.7
Observation boundary: Inspects the resolved dependency bytecode. It proves Specification.unrestricted() is a non-null Specification whose toPredicate returns null; it does not execute a repository query.
--- stdout/stderr ---
Compiled from "Specification.java"
public interface org.springframework.data.jpa.domain.Specification<T> extends java.io.Serializable {
public static <T> org.springframework.data.jpa.domain.Specification<T> unrestricted();
Code:
0: invokedynamic #13, 0 // InvokeDynamic #0:toPredicate:()Lorg/springframework/data/jpa/domain/Specification;
5: areturn
public static <T> org.springframework.data.jpa.domain.Specification<T> where(org.springframework.data.jpa.domain.Specification<T>);
Code:
0: aload_0
1: ldc #21 // String Specification must not be null
3: invokestatic #23 // Method org/springframework/util/Assert.notNull:(Ljava/lang/Object;Ljava/lang/String;)V
6: aload_0
7: areturn
public static <T> org.springframework.data.jpa.domain.Specification<T> where(org.springframework.data.jpa.domain.PredicateSpecification<T>);
Code:
0: aload_0
1: ldc #36 // String PredicateSpecification must not be null
3: invokestatic #23 // Method org/springframework/util/Assert.notNull:(Ljava/lang/Object;Ljava/lang/String;)V
6: aload_0
7: invokedynamic #38, 0 // InvokeDynamic #1:toPredicate:(Lorg/springframework/data/jpa/domain/PredicateSpecification;)Lorg/springframework/data/jpa/domain/Specification;
12: areturn
public default org.springframework.data.jpa.domain.Specification<T> and(org.springframework.data.jpa.domain.Specification<T>);
Code:
0: aload_1
1: ldc #49 // String Other specification must not be null
3: invokestatic #23 // Method org/springframework/util/Assert.notNull:(Ljava/lang/Object;Ljava/lang/String;)V
6: aload_0
7: aload_1
8: invokedynamic #51, 0 // InvokeDynamic #2:combine:()Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;
13: invokestatic #55 // Method org/springframework/data/jpa/domain/SpecificationComposition.composed:(Lorg/springframework/data/jpa/domain/Specification;Lorg/springframework/data/jpa/domain/Specification;Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;)Lorg/springframework/data/jpa/domain/Specification;
16: areturn
public default org.springframework.data.jpa.domain.Specification<T> and(org.springframework.data.jpa.domain.PredicateSpecification<T>);
Code:
0: aload_1
1: ldc #49 // String Other specification must not be null
3: invokestatic #23 // Method org/springframework/util/Assert.notNull:(Ljava/lang/Object;Ljava/lang/String;)V
6: aload_0
7: aload_1
8: invokestatic #64 // InterfaceMethod where:(Lorg/springframework/data/jpa/domain/PredicateSpecification;)Lorg/springframework/data/jpa/domain/Specification;
11: invokedynamic #51, 0 // InvokeDynamic #2:combine:()Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;
16: invokestatic #55 // Method org/springframework/data/jpa/domain/SpecificationComposition.composed:(Lorg/springframework/data/jpa/domain/Specification;Lorg/springframework/data/jpa/domain/Specification;Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;)Lorg/springframework/data/jpa/domain/Specification;
19: areturn
public default org.springframework.data.jpa.domain.Specification<T> or(org.springframework.data.jpa.domain.Specification<T>);
Code:
0: aload_1
1: ldc #49 // String Other specification must not be null
3: invokestatic #23 // Method org/springframework/util/Assert.notNull:(Ljava/lang/Object;Ljava/lang/String;)V
6: aload_0
7: aload_1
8: invokedynamic #67, 0 // InvokeDynamic #3:combine:()Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;
13: invokestatic #55 // Method org/springframework/data/jpa/domain/SpecificationComposition.composed:(Lorg/springframework/data/jpa/domain/Specification;Lorg/springframework/data/jpa/domain/Specification;Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;)Lorg/springframework/data/jpa/domain/Specification;
16: areturn
public default org.springframework.data.jpa.domain.Specification<T> or(org.springframework.data.jpa.domain.PredicateSpecification<T>);
Code:
0: aload_1
1: ldc #49 // String Other specification must not be null
3: invokestatic #23 // Method org/springframework/util/Assert.notNull:(Ljava/lang/Object;Ljava/lang/String;)V
6: aload_0
7: aload_1
8: invokestatic #64 // InterfaceMethod where:(Lorg/springframework/data/jpa/domain/PredicateSpecification;)Lorg/springframework/data/jpa/domain/Specification;
11: invokedynamic #67, 0 // InvokeDynamic #3:combine:()Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;
16: invokestatic #55 // Method org/springframework/data/jpa/domain/SpecificationComposition.composed:(Lorg/springframework/data/jpa/domain/Specification;Lorg/springframework/data/jpa/domain/Specification;Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;)Lorg/springframework/data/jpa/domain/Specification;
19: areturn
public static <T> org.springframework.data.jpa.domain.Specification<T> not(org.springframework.data.jpa.domain.Specification<T>);
Code:
0: aload_0
1: ldc #21 // String Specification must not be null
3: invokestatic #23 // Method org/springframework/util/Assert.notNull:(Ljava/lang/Object;Ljava/lang/String;)V
6: aload_0
7: invokedynamic #69, 0 // InvokeDynamic #4:toPredicate:(Lorg/springframework/data/jpa/domain/Specification;)Lorg/springframework/data/jpa/domain/Specification;
12: areturn
public static <T> org.springframework.data.jpa.domain.Specification<T> allOf(org.springframework.data.jpa.domain.Specification<T>...);
Code:
0: aload_0
1: invokestatic #76 // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
4: invokestatic #82 // InterfaceMethod allOf:(Ljava/lang/Iterable;)Lorg/springframework/data/jpa/domain/Specification;
7: areturn
public static <T> org.springframework.data.jpa.domain.Specification<T> allOf(java.lang.Iterable<org.springframework.data.jpa.domain.Specification<T>>);
Code:
0: aload_0
1: invokeinterface #88, 1 // InterfaceMethod java/lang/Iterable.spliterator:()Ljava/util/Spliterator;
6: iconst_0
7: invokestatic #94 // Method java/util/stream/StreamSupport.stream:(Ljava/util/Spliterator;Z)Ljava/util/stream/Stream;
10: invokestatic #100 // InterfaceMethod unrestricted:()Lorg/springframework/data/jpa/domain/Specification;
13: invokedynamic #102, 0 // InvokeDynamic #5:apply:()Ljava/util/function/BinaryOperator;
18: invokeinterface #106, 3 // InterfaceMethod java/util/stream/Stream.reduce:(Ljava/lang/Object;Ljava/util/function/BinaryOperator;)Ljava/lang/Object;
23: checkcast #1 // class org/springframework/data/jpa/domain/Specification
26: areturn
public static <T> org.springframework.data.jpa.domain.Specification<T> anyOf(org.springframework.data.jpa.domain.Specification<T>...);
Code:
0: aload_0
1: invokestatic #76 // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
4: invokestatic #115 // InterfaceMethod anyOf:(Ljava/lang/Iterable;)Lorg/springframework/data/jpa/domain/Specification;
7: areturn
public static <T> org.springframework.data.jpa.domain.Specification<T> anyOf(java.lang.Iterable<org.springframework.data.jpa.domain.Specification<T>>);
Code:
0: aload_0
1: invokeinterface #88, 1 // InterfaceMethod java/lang/Iterable.spliterator:()Ljava/util/Spliterator;
6: iconst_0
7: invokestatic #94 // Method java/util/stream/StreamSupport.stream:(Ljava/util/Spliterator;Z)Ljava/util/stream/Stream;
10: invokestatic #100 // InterfaceMethod unrestricted:()Lorg/springframework/data/jpa/domain/Specification;
13: invokedynamic #117, 0 // InvokeDynamic #6:apply:()Ljava/util/function/BinaryOperator;
18: invokeinterface #106, 3 // InterfaceMethod java/util/stream/Stream.reduce:(Ljava/lang/Object;Ljava/util/function/BinaryOperator;)Ljava/lang/Object;
23: checkcast #1 // class org/springframework/data/jpa/domain/Specification
26: areturn
public abstract jakarta.persistence.criteria.Predicate toPredicate(jakarta.persistence.criteria.Root<T>, jakarta.persistence.criteria.CriteriaQuery<?>, jakarta.persistence.criteria.CriteriaBuilder);
private static jakarta.persistence.criteria.Predicate lambda$0(jakarta.persistence.criteria.Root, jakarta.persistence.criteria.CriteriaQuery, jakarta.persistence.criteria.CriteriaBuilder);
Code:
0: aconst_null
1: areturn
private static jakarta.persistence.criteria.Predicate lambda$1(org.springframework.data.jpa.domain.PredicateSpecification, jakarta.persistence.criteria.Root, jakarta.persistence.criteria.CriteriaQuery, jakarta.persistence.criteria.CriteriaBuilder);
Code:
0: aload_0
1: aload_1
2: aload_3
3: invokeinterface #134, 3 // InterfaceMethod org/springframework/data/jpa/domain/PredicateSpecification.toPredicate:(Ljakarta/persistence/criteria/From;Ljakarta/persistence/criteria/CriteriaBuilder;)Ljakarta/persistence/criteria/Predicate;
8: areturn
private static jakarta.persistence.criteria.Predicate lambda$6(org.springframework.data.jpa.domain.Specification, jakarta.persistence.criteria.Root, jakarta.persistence.criteria.CriteriaQuery, jakarta.persistence.criteria.CriteriaBuilder);
Code:
0: aload_0
1: aload_1
2: aload_2
3: aload_3
4: invokeinterface #142, 4 // InterfaceMethod toPredicate:(Ljakarta/persistence/criteria/Root;Ljakarta/persistence/criteria/CriteriaQuery;Ljakarta/persistence/criteria/CriteriaBuilder;)Ljakarta/persistence/criteria/Predicate;
9: astore 4
11: aload 4
13: ifnull 27
16: aload_3
17: aload 4
19: invokeinterface #144, 2 // InterfaceMethod jakarta/persistence/criteria/CriteriaBuilder.not:(Ljakarta/persistence/criteria/Expression;)Ljakarta/persistence/criteria/Predicate;
24: goto 28
27: aconst_null
28: areturn
private static java.lang.Object $deserializeLambda$(java.lang.invoke.SerializedLambda);
Code:
0: aload_0
1: invokevirtual #156 // Method java/lang/invoke/SerializedLambda.getImplMethodName:()Ljava/lang/String;
4: astore_1
5: iconst_m1
6: istore_2
7: aload_1
8: invokevirtual #162 // Method java/lang/String.hashCode:()I
11: lookupswitch { // 5
-1647345005: 60
-1647345004: 74
-1647344999: 144
3555: 116
96727: 88
default: 155
}
60: aload_1
61: ldc #168 // String lambda$0
63: invokevirtual #169 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
66: ifeq 155
69: iconst_0
70: istore_2
71: goto 155
74: aload_1
75: ldc #173 // String lambda$1
77: invokevirtual #169 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
80: ifeq 155
83: iconst_1
84: istore_2
85: goto 155
88: aload_1
89: ldc #174 // String and
91: invokevirtual #169 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
94: ifeq 102
97: iconst_2
98: istore_2
99: goto 155
102: aload_1
103: ldc #174 // String and
105: invokevirtual #169 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
108: ifeq 155
111: iconst_2
112: istore_2
113: goto 155
116: aload_1
117: ldc #175 // String or
119: invokevirtual #169 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
122: ifeq 130
125: iconst_3
126: istore_2
127: goto 155
130: aload_1
131: ldc #175 // String or
133: invokevirtual #169 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
136: ifeq 155
139: iconst_3
140: istore_2
141: goto 155
144: aload_1
145: ldc #176 // String lambda$6
147: invokevirtual #169 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
150: ifeq 155
153: iconst_4
154: istore_2
155: iload_2
156: lookupswitch { // 5
0: 208
1: 283
2: 366
3: 516
4: 666
default: 749
}
208: aload_0
209: invokevirtual #177 // Method java/lang/invoke/SerializedLambda.getImplMethodKind:()I
212: bipush 6
214: if_icmpne 749
217: aload_0
218: invokevirtual #180 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceClass:()Ljava/lang/String;
221: ldc #183 // String org/springframework/data/jpa/domain/Specification
223: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
226: ifeq 749
229: aload_0
230: invokevirtual #185 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodName:()Ljava/lang/String;
233: ldc #188 // String toPredicate
235: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
238: ifeq 749
241: aload_0
242: invokevirtual #189 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodSignature:()Ljava/lang/String;
245: ldc #192 // String (Ljakarta/persistence/criteria/Root;Ljakarta/persistence/criteria/CriteriaQuery;Ljakarta/persistence/criteria/CriteriaBuilder;)Ljakarta/persistence/criteria/Predicate;
247: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
250: ifeq 749
253: aload_0
254: invokevirtual #193 // Method java/lang/invoke/SerializedLambda.getImplClass:()Ljava/lang/String;
257: ldc #183 // String org/springframework/data/jpa/domain/Specification
259: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
262: ifeq 749
265: aload_0
266: invokevirtual #196 // Method java/lang/invoke/SerializedLambda.getImplMethodSignature:()Ljava/lang/String;
269: ldc #192 // String (Ljakarta/persistence/criteria/Root;Ljakarta/persistence/criteria/CriteriaQuery;Ljakarta/persistence/criteria/CriteriaBuilder;)Ljakarta/persistence/criteria/Predicate;
271: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
274: ifeq 749
277: invokedynamic #13, 0 // InvokeDynamic #0:toPredicate:()Lorg/springframework/data/jpa/domain/Specification;
282: areturn
283: aload_0
284: invokevirtual #177 // Method java/lang/invoke/SerializedLambda.getImplMethodKind:()I
287: bipush 6
289: if_icmpne 749
292: aload_0
293: invokevirtual #180 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceClass:()Ljava/lang/String;
296: ldc #183 // String org/springframework/data/jpa/domain/Specification
298: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
301: ifeq 749
304: aload_0
305: invokevirtual #185 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodName:()Ljava/lang/String;
308: ldc #188 // String toPredicate
310: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
313: ifeq 749
316: aload_0
317: invokevirtual #189 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodSignature:()Ljava/lang/String;
320: ldc #192 // String (Ljakarta/persistence/criteria/Root;Ljakarta/persistence/criteria/CriteriaQuery;Ljakarta/persistence/criteria/CriteriaBuilder;)Ljakarta/persistence/criteria/Predicate;
322: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
325: ifeq 749
328: aload_0
329: invokevirtual #193 // Method java/lang/invoke/SerializedLambda.getImplClass:()Ljava/lang/String;
332: ldc #183 // String org/springframework/data/jpa/domain/Specification
334: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
337: ifeq 749
340: aload_0
341: invokevirtual #196 // Method java/lang/invoke/SerializedLambda.getImplMethodSignature:()Ljava/lang/String;
344: ldc #199 // String (Lorg/springframework/data/jpa/domain/PredicateSpecification;Ljakarta/persistence/criteria/Root;Ljakarta/persistence/criteria/CriteriaQuery;Ljakarta/persistence/criteria/CriteriaBuilder;)Ljakarta/persistence/criteria/Predicate;
346: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
349: ifeq 749
352: aload_0
353: iconst_0
354: invokevirtual #200 // Method java/lang/invoke/SerializedLambda.getCapturedArg:(I)Ljava/lang/Object;
357: checkcast #135 // class org/springframework/data/jpa/domain/PredicateSpecification
360: invokedynamic #38, 0 // InvokeDynamic #1:toPredicate:(Lorg/springframework/data/jpa/domain/PredicateSpecification;)Lorg/springframework/data/jpa/domain/Specification;
365: areturn
366: aload_0
367: invokevirtual #177 // Method java/lang/invoke/SerializedLambda.getImplMethodKind:()I
370: bipush 9
372: if_icmpne 441
375: aload_0
376: invokevirtual #180 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceClass:()Ljava/lang/String;
379: ldc #204 // String org/springframework/data/jpa/domain/SpecificationComposition$Combiner
381: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
384: ifeq 441
387: aload_0
388: invokevirtual #185 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodName:()Ljava/lang/String;
391: ldc #206 // String combine
393: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
396: ifeq 441
399: aload_0
400: invokevirtual #189 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodSignature:()Ljava/lang/String;
403: ldc #207 // String (Ljakarta/persistence/criteria/CriteriaBuilder;Ljakarta/persistence/criteria/Predicate;Ljakarta/persistence/criteria/Predicate;)Ljakarta/persistence/criteria/Predicate;
405: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
408: ifeq 441
411: aload_0
412: invokevirtual #193 // Method java/lang/invoke/SerializedLambda.getImplClass:()Ljava/lang/String;
415: ldc #209 // String jakarta/persistence/criteria/CriteriaBuilder
417: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
420: ifeq 441
423: aload_0
424: invokevirtual #196 // Method java/lang/invoke/SerializedLambda.getImplMethodSignature:()Ljava/lang/String;
427: ldc #210 // String (Ljakarta/persistence/criteria/Expression;Ljakarta/persistence/criteria/Expression;)Ljakarta/persistence/criteria/Predicate;
429: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
432: ifeq 441
435: invokedynamic #51, 0 // InvokeDynamic #2:combine:()Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;
440: areturn
441: aload_0
442: invokevirtual #177 // Method java/lang/invoke/SerializedLambda.getImplMethodKind:()I
445: bipush 9
447: if_icmpne 749
450: aload_0
451: invokevirtual #180 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceClass:()Ljava/lang/String;
454: ldc #204 // String org/springframework/data/jpa/domain/SpecificationComposition$Combiner
456: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
459: ifeq 749
462: aload_0
463: invokevirtual #185 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodName:()Ljava/lang/String;
466: ldc #206 // String combine
468: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
471: ifeq 749
474: aload_0
475: invokevirtual #189 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodSignature:()Ljava/lang/String;
478: ldc #207 // String (Ljakarta/persistence/criteria/CriteriaBuilder;Ljakarta/persistence/criteria/Predicate;Ljakarta/persistence/criteria/Predicate;)Ljakarta/persistence/criteria/Predicate;
480: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
483: ifeq 749
486: aload_0
487: invokevirtual #193 // Method java/lang/invoke/SerializedLambda.getImplClass:()Ljava/lang/String;
490: ldc #209 // String jakarta/persistence/criteria/CriteriaBuilder
492: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
495: ifeq 749
498: aload_0
499: invokevirtual #196 // Method java/lang/invoke/SerializedLambda.getImplMethodSignature:()Ljava/lang/String;
502: ldc #210 // String (Ljakarta/persistence/criteria/Expression;Ljakarta/persistence/criteria/Expression;)Ljakarta/persistence/criteria/Predicate;
504: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
507: ifeq 749
510: invokedynamic #51, 0 // InvokeDynamic #2:combine:()Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;
515: areturn
516: aload_0
517: invokevirtual #177 // Method java/lang/invoke/SerializedLambda.getImplMethodKind:()I
520: bipush 9
522: if_icmpne 591
525: aload_0
526: invokevirtual #180 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceClass:()Ljava/lang/String;
529: ldc #204 // String org/springframework/data/jpa/domain/SpecificationComposition$Combiner
531: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
534: ifeq 591
537: aload_0
538: invokevirtual #185 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodName:()Ljava/lang/String;
541: ldc #206 // String combine
543: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
546: ifeq 591
549: aload_0
550: invokevirtual #189 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodSignature:()Ljava/lang/String;
553: ldc #207 // String (Ljakarta/persistence/criteria/CriteriaBuilder;Ljakarta/persistence/criteria/Predicate;Ljakarta/persistence/criteria/Predicate;)Ljakarta/persistence/criteria/Predicate;
555: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
558: ifeq 591
561: aload_0
562: invokevirtual #193 // Method java/lang/invoke/SerializedLambda.getImplClass:()Ljava/lang/String;
565: ldc #209 // String jakarta/persistence/criteria/CriteriaBuilder
567: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
570: ifeq 591
573: aload_0
574: invokevirtual #196 // Method java/lang/invoke/SerializedLambda.getImplMethodSignature:()Ljava/lang/String;
577: ldc #210 // String (Ljakarta/persistence/criteria/Expression;Ljakarta/persistence/criteria/Expression;)Ljakarta/persistence/criteria/Predicate;
579: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
582: ifeq 591
585: invokedynamic #67, 0 // InvokeDynamic #3:combine:()Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;
590: areturn
591: aload_0
592: invokevirtual #177 // Method java/lang/invoke/SerializedLambda.getImplMethodKind:()I
595: bipush 9
597: if_icmpne 749
600: aload_0
601: invokevirtual #180 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceClass:()Ljava/lang/String;
604: ldc #204 // String org/springframework/data/jpa/domain/SpecificationComposition$Combiner
606: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
609: ifeq 749
612: aload_0
613: invokevirtual #185 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodName:()Ljava/lang/String;
616: ldc #206 // String combine
618: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
621: ifeq 749
624: aload_0
625: invokevirtual #189 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodSignature:()Ljava/lang/String;
628: ldc #207 // String (Ljakarta/persistence/criteria/CriteriaBuilder;Ljakarta/persistence/criteria/Predicate;Ljakarta/persistence/criteria/Predicate;)Ljakarta/persistence/criteria/Predicate;
630: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
633: ifeq 749
636: aload_0
637: invokevirtual #193 // Method java/lang/invoke/SerializedLambda.getImplClass:()Ljava/lang/String;
640: ldc #209 // String jakarta/persistence/criteria/CriteriaBuilder
642: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
645: ifeq 749
648: aload_0
649: invokevirtual #196 // Method java/lang/invoke/SerializedLambda.getImplMethodSignature:()Ljava/lang/String;
652: ldc #210 // String (Ljakarta/persistence/criteria/Expression;Ljakarta/persistence/criteria/Expression;)Ljakarta/persistence/criteria/Predicate;
654: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
657: ifeq 749
660: invokedynamic #67, 0 // InvokeDynamic #3:combine:()Lorg/springframework/data/jpa/domain/SpecificationComposition$Combiner;
665: areturn
666: aload_0
667: invokevirtual #177 // Method java/lang/invoke/SerializedLambda.getImplMethodKind:()I
670: bipush 6
672: if_icmpne 749
675: aload_0
676: invokevirtual #180 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceClass:()Ljava/lang/String;
679: ldc #183 // String org/springframework/data/jpa/domain/Specification
681: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
684: ifeq 749
687: aload_0
688: invokevirtual #185 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodName:()Ljava/lang/String;
691: ldc #188 // String toPredicate
693: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
696: ifeq 749
699: aload_0
700: invokevirtual #189 // Method java/lang/invoke/SerializedLambda.getFunctionalInterfaceMethodSignature:()Ljava/lang/String;
703: ldc #192 // String (Ljakarta/persistence/criteria/Root;Ljakarta/persistence/criteria/CriteriaQuery;Ljakarta/persistence/criteria/CriteriaBuilder;)Ljakarta/persistence/criteria/Predicate;
705: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
708: ifeq 749
711: aload_0
712: invokevirtual #193 // Method java/lang/invoke/SerializedLambda.getImplClass:()Ljava/lang/String;
715: ldc #183 // String org/springframework/data/jpa/domain/Specification
717: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
720: ifeq 749
723: aload_0
724: invokevirtual #196 // Method java/lang/invoke/SerializedLambda.getImplMethodSignature:()Ljava/lang/String;
727: ldc #212 // String (Lorg/springframework/data/jpa/domain/Specification;Ljakarta/persistence/criteria/Root;Ljakarta/persistence/criteria/CriteriaQuery;Ljakarta/persistence/criteria/CriteriaBuilder;)Ljakarta/persistence/criteria/Predicate;
729: invokevirtual #184 // Method java/lang/Object.equals:(Ljava/lang/Object;)Z
732: ifeq 749
735: aload_0
736: iconst_0
737: invokevirtual #200 // Method java/lang/invoke/SerializedLambda.getCapturedArg:(I)Ljava/lang/Object;
740: checkcast #1 // class org/springframework/data/jpa/domain/Specification
743: invokedynamic #69, 0 // InvokeDynamic #4:toPredicate:(Lorg/springframework/data/jpa/domain/Specification;)Lorg/springframework/data/jpa/domain/Specification;
748: areturn
749: new #213 // class java/lang/IllegalArgumentException
752: dup
753: ldc #215 // String Invalid lambda deserialization
755: invokespecial #217 // Method java/lang/IllegalArgumentException."<init>":(Ljava/lang/String;)V
758: athrow
}
Exit code: 0
@@ -0,0 +1,96 @@
Command: compile/run /tmp PostgreSqlReplaySemanticProbe against current postgresqlIntegrationTest resolved runtime classpath
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T08:40:58Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Real PostgreSQL 16 Testcontainers execution of current same-store inbox and polling-outbox adapters. It isolates replay semantics and ownership; adapters are not automatically composed in the shipped application.
--- stdout/stderr ---
08:40:59.312 [main] INFO org.testcontainers.DockerClientFactory -- Testcontainers version: 2.0.5
08:40:59.481 [main] INFO org.testcontainers.dockerclient.DockerClientProviderStrategy -- Loaded org.testcontainers.dockerclient.UnixSocketClientProviderStrategy from ~/.testcontainers.properties, will try it first
08:40:59.846 [main] INFO org.testcontainers.dockerclient.DockerClientProviderStrategy -- Found Docker environment with local Unix socket (unix:///var/run/docker.sock)
08:40:59.860 [main] INFO org.testcontainers.DockerClientFactory -- Docker host IP address is 172.17.0.1
08:40:59.873 [main] INFO org.testcontainers.DockerClientFactory -- Connected to docker:
Server Version: 29.6.1
API Version: 1.55
Operating System: Ubuntu 24.04.4 LTS
Total Memory: 32008 MB
08:40:59.882 [main] INFO org.testcontainers.images.PullPolicy -- Image pull policy will be performed by: DefaultPullPolicy()
08:40:59.884 [main] INFO org.testcontainers.utility.ImageNameSubstitutor -- Image name substitution will be performed by: DefaultImageNameSubstitutor (composite of 'ConfigurationFileImageNameSubstitutor' and 'PrefixingImageNameSubstitutor')
08:40:59.934 [main] INFO tc.testcontainers/ryuk:0.14.0 -- Creating container for image: testcontainers/ryuk:0.14.0
08:40:59.937 [main] INFO org.testcontainers.utility.RegistryAuthLocator -- Failure when attempting to lookup auth config. Please ignore if you don't have images in an authenticated registry. Details: (dockerImageName: testcontainers/ryuk:0.14.0, configFile: /root/.docker/config.json, configEnv: DOCKER_AUTH_CONFIG). Falling back to docker-java default behaviour. Exception message: Status 404: No config supplied. Checked in order: /root/.docker/config.json (file not found), DOCKER_AUTH_CONFIG (not set)
08:41:00.032 [main] INFO tc.testcontainers/ryuk:0.14.0 -- Container testcontainers/ryuk:0.14.0 is starting: faa6ae58a0cc7e71cd7a48899b1577fef3241b725f28c5347a05e97b1e8ee1fe
08:41:00.281 [main] INFO tc.testcontainers/ryuk:0.14.0 -- Container testcontainers/ryuk:0.14.0 started in PT0.347332906S
08:41:00.285 [main] INFO org.testcontainers.utility.RyukResourceReaper -- Ryuk started - will monitor and terminate Testcontainers containers on JVM exit
08:41:00.285 [main] INFO org.testcontainers.DockerClientFactory -- Checking the system...
08:41:00.285 [main] INFO org.testcontainers.DockerClientFactory -- ✔︎ Docker server version should be at least 1.6.0
08:41:00.287 [main] INFO tc.postgres:16-alpine -- Creating container for image: postgres:16-alpine
08:41:00.336 [main] INFO tc.postgres:16-alpine -- Container postgres:16-alpine is starting: 3a87e7587a2e69ac45a40120ad47a9f1ed7739c4a733024ab6306cf5ae018e82
08:41:01.497 [main] INFO tc.postgres:16-alpine -- Container postgres:16-alpine started in PT1.210343551S
08:41:01.498 [main] INFO tc.postgres:16-alpine -- Container is started (JDBC URL: jdbc:postgresql://172.17.0.1:32777/test?loggerLevel=OFF)
08:41:01.507 [main] INFO com.zaxxer.hikari.HikariDataSource -- HikariPool-1 - Starting...
08:41:01.639 [main] INFO com.zaxxer.hikari.pool.HikariPool -- HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@452c8a40
08:41:01.640 [main] INFO com.zaxxer.hikari.HikariDataSource -- HikariPool-1 - Start completed.
08:41:01.855 [main] INFO org.flywaydb.core.FlywayExecutor -- Database: jdbc:postgresql://172.17.0.1:32777/test?loggerLevel=OFF (PostgreSQL 16.15)
08:41:01.894 [main] INFO org.flywaydb.core.internal.schemahistory.JdbcTableSchemaHistory -- Schema history table "public"."flyway_schema_history" does not exist yet
08:41:01.897 [main] INFO org.flywaydb.core.internal.command.DbValidate -- Successfully validated 9 migrations (execution time 00:00.017s)
08:41:01.908 [main] INFO org.flywaydb.core.internal.schemahistory.JdbcTableSchemaHistory -- Creating Schema History table "public"."flyway_schema_history" ...
08:41:01.966 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Current version of schema "public": << Empty Schema >>
08:41:01.975 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "1 - idempotency record"
08:41:01.997 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "3 - outbox event"
08:41:02.010 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "4 - int lock"
08:41:02.019 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "5 - int lock expired after"
08:41:02.028 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "6 - capability schema registry adoption"
08:41:02.037 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "9 - widen capability schema stream"
08:41:02.043 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "10 - idempotency request hash varchar"
08:41:02.060 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "11 - durable operation"
08:41:02.075 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "12 - live event log"
08:41:02.086 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Successfully applied 9 migrations to schema "public", now at version v12 (execution time 00:00.051s)
08:41:02.112 [main] INFO org.flywaydb.core.FlywayExecutor -- Database: jdbc:postgresql://172.17.0.1:32777/test?loggerLevel=OFF (PostgreSQL 16.15)
08:41:02.117 [main] INFO org.flywaydb.core.internal.schemahistory.JdbcTableSchemaHistory -- Creating Schema History table "public"."flyway_jpa_core_history" with baseline ...
08:41:02.123 [main] INFO org.flywaydb.core.internal.command.DbBaseline -- Successfully baselined schema with version: 0
08:41:02.134 [main] INFO org.flywaydb.core.internal.command.DbValidate -- Successfully validated 3 migrations (execution time 00:00.003s)
08:41:02.137 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Current version of schema "public": 0
08:41:02.140 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "1 - initialize or adopt"
08:41:02.145 [main] WARN org.flywaydb.core.internal.sqlscript.DefaultSqlScriptExecutor -- DB: relation "capability_schema_registry" already exists, skipping (SQL State: 42P07 - Error Code: 0)
08:41:02.151 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "2 - widen capability schema stream"
08:41:02.155 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Successfully applied 2 migrations to schema "public", now at version v2 (execution time 00:00.007s)
08:41:02.171 [main] INFO org.flywaydb.core.FlywayExecutor -- Database: jdbc:postgresql://172.17.0.1:32777/test?loggerLevel=OFF (PostgreSQL 16.15)
08:41:02.175 [main] INFO org.flywaydb.core.internal.schemahistory.JdbcTableSchemaHistory -- Creating Schema History table "public"."flyway_jpa_inbox_history" with baseline ...
08:41:02.181 [main] INFO org.flywaydb.core.internal.command.DbBaseline -- Successfully baselined schema with version: 0
08:41:02.190 [main] INFO org.flywaydb.core.internal.command.DbValidate -- Successfully validated 2 migrations (execution time 00:00.002s)
08:41:02.193 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Current version of schema "public": 0
08:41:02.196 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "1 - initialize same store inbox"
08:41:02.206 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Successfully applied 1 migration to schema "public", now at version v1 (execution time 00:00.007s)
08:41:02.221 [main] INFO org.flywaydb.core.FlywayExecutor -- Database: jdbc:postgresql://172.17.0.1:32777/test?loggerLevel=OFF (PostgreSQL 16.15)
08:41:02.225 [main] INFO org.flywaydb.core.internal.schemahistory.JdbcTableSchemaHistory -- Creating Schema History table "public"."flyway_jpa_outbox_storage_history" with baseline ...
08:41:02.230 [main] INFO org.flywaydb.core.internal.command.DbBaseline -- Successfully baselined schema with version: 0
08:41:02.239 [main] INFO org.flywaydb.core.internal.command.DbValidate -- Successfully validated 2 migrations (execution time 00:00.002s)
08:41:02.241 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Current version of schema "public": 0
08:41:02.248 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "1 - initialize or adopt"
08:41:02.250 [main] WARN org.flywaydb.core.internal.sqlscript.DefaultSqlScriptExecutor -- DB: relation "outbox_event" already exists, skipping (SQL State: 42P07 - Error Code: 0)
08:41:02.250 [main] WARN org.flywaydb.core.internal.sqlscript.DefaultSqlScriptExecutor -- DB: relation "ix_outbox_event_eligible" already exists, skipping (SQL State: 42P07 - Error Code: 0)
08:41:02.251 [main] WARN org.flywaydb.core.internal.sqlscript.DefaultSqlScriptExecutor -- DB: relation "ix_outbox_event_aggregate_occurred" already exists, skipping (SQL State: 42P07 - Error Code: 0)
08:41:02.251 [main] WARN org.flywaydb.core.internal.sqlscript.DefaultSqlScriptExecutor -- DB: relation "ix_outbox_event_published_occurred" already exists, skipping (SQL State: 42P07 - Error Code: 0)
08:41:02.251 [main] WARN org.flywaydb.core.internal.sqlscript.DefaultSqlScriptExecutor -- DB: relation "ix_outbox_event_status_occurred" already exists, skipping (SQL State: 42P07 - Error Code: 0)
08:41:02.278 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Successfully applied 1 migration to schema "public", now at version v1 (execution time 00:00.026s)
08:41:02.292 [main] INFO org.flywaydb.core.FlywayExecutor -- Database: jdbc:postgresql://172.17.0.1:32777/test?loggerLevel=OFF (PostgreSQL 16.15)
08:41:02.295 [main] INFO org.flywaydb.core.internal.schemahistory.JdbcTableSchemaHistory -- Creating Schema History table "public"."flyway_jpa_outbox_polling_history" with baseline ...
08:41:02.301 [main] INFO org.flywaydb.core.internal.command.DbBaseline -- Successfully baselined schema with version: 0
08:41:02.309 [main] INFO org.flywaydb.core.internal.command.DbValidate -- Successfully validated 2 migrations (execution time 00:00.002s)
08:41:02.312 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Current version of schema "public": 0
08:41:02.315 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Migrating schema "public" to version "1 - initialize polling delivery v2"
08:41:02.327 [main] INFO org.flywaydb.core.internal.command.DbMigrate -- Successfully applied 1 migration to schema "public", now at version v1 (execution time 00:00.009s)
inboxForgedReplay.outcome=PROCESSING_STARTED
inboxForgedReplay.returnedActualToken=true
inboxForgedReplay.returnedForgedToken=false
inboxForgedReplay.completeWithReturnedOwner=COMPLETED
inboxRetention.first=RETRYABLE
inboxRetention.secondDifferentRetention=ALREADY_APPLIED_SAME_OPERATION
inboxRetention.remainingHours=1.000
outboxRetry.first=RETRY_SCHEDULED
outboxRetry.secondDifferentSchedule=ALREADY_APPLIED_SAME_OPERATION
outboxRetry.storedEqualsFirst=true
outboxRetry.storedEqualsSecond=false
08:41:02.439 [main] INFO com.zaxxer.hikari.HikariDataSource -- HikariPool-1 - Shutdown initiated...
08:41:02.441 [main] INFO com.zaxxer.hikari.HikariDataSource -- HikariPool-1 - Shutdown completed.
Exit code: 0
@@ -0,0 +1,105 @@
package dev.caskeleton.adapter.outbound.persistence.readiness;
import dev.caskeleton.adapter.outbound.persistence.postgresql.inbox.PostgreSqlSameStoreInboxAdapter;
import dev.caskeleton.adapter.outbound.persistence.postgresql.outbox.PostgreSqlImmutableOutboxAppendAdapter;
import dev.caskeleton.adapter.outbound.persistence.postgresql.outbox.PostgreSqlPollingDeliveryAdapter;
import dev.caskeleton.application.inbox.*;
import dev.caskeleton.application.outbox.v2.*;
import dev.caskeleton.application.transaction.OperationId;
import java.time.*;
import org.flywaydb.core.Flyway;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
public class PostgreSqlReplaySemanticProbe {
public static void main(String[] args) {
try (PostgreSqlReadinessSupport postgres = PostgreSqlReadinessSupport.start()) {
migrate(postgres, "classpath:db/migration/postgresql", "flyway_schema_history");
migrateIndependent(postgres, "classpath:db/migration/jpa/core", "flyway_jpa_core_history", "probe-core");
migrateIndependent(postgres, "classpath:db/migration/jpa/inbox", "flyway_jpa_inbox_history", "probe-inbox");
migrateIndependent(postgres, "classpath:db/migration/jpa/outbox-storage", "flyway_jpa_outbox_storage_history", "probe-outbox-storage");
migrateIndependent(postgres, "classpath:db/migration/jpa/outbox-polling", "flyway_jpa_outbox_polling_history", "probe-outbox-polling");
JdbcTemplate jdbc = new JdbcTemplate(postgres.dataSource());
TransactionTemplate tx = new TransactionTemplate(new DataSourceTransactionManager(postgres.dataSource()));
jdbc.update("update capability_schema_registry set lifecycle_state='ACTIVE' where capability_id in ('jpa-inbox-same-store-v1','jpa-outbox-storage-v2','jpa-outbox-polling-delivery-v2')");
probeInboxForgedReplay(postgres, jdbc, tx);
probeInboxRetention(postgres, jdbc, tx);
probeOutboxRetrySchedule(postgres, jdbc, tx);
}
}
private static void probeInboxForgedReplay(PostgreSqlReadinessSupport postgres, JdbcTemplate jdbc, TransactionTemplate tx) {
PostgreSqlSameStoreInboxAdapter inbox = new PostgreSqlSameStoreInboxAdapter(postgres.dataSource());
InboxScopeDigest scope = new InboxScopeDigest("c".repeat(64));
InboxClaimAttempt attempt = inbox.newClaimAttempt(new OperationId("claim-owner-probe"));
InboxOwner claimed = tx.execute(ignored -> ((InboxClaimOutcome.Acquired) inbox.claim(new InboxClaimRequest(scope, "d".repeat(64), attempt, Duration.ofSeconds(30), Duration.ofDays(7)))).owner());
OperationId startOp = new OperationId("start-owner-probe");
InboxOwner actual = tx.execute(ignored -> inbox.markProcessing(claimed, startOp).owner().orElseThrow());
InboxOwner forged = new InboxOwner(scope, "forged-owner-token", actual.attempt(), actual.stateRevision(), actual.claimOperationId());
InboxOwnerTransition replay = tx.execute(ignored -> inbox.markProcessing(forged, startOp));
InboxOwner leaked = replay.owner().orElse(null);
System.out.println("inboxForgedReplay.outcome=" + replay.outcome());
System.out.println("inboxForgedReplay.returnedActualToken=" + (leaked != null && leaked.ownerToken().equals(actual.ownerToken())));
System.out.println("inboxForgedReplay.returnedForgedToken=" + (leaked != null && leaked.ownerToken().equals(forged.ownerToken())));
if (leaked != null) {
var complete = tx.execute(ignored -> inbox.complete(leaked, new OperationId("complete-via-leaked-owner")));
System.out.println("inboxForgedReplay.completeWithReturnedOwner=" + complete);
}
}
private static void probeInboxRetention(PostgreSqlReadinessSupport postgres, JdbcTemplate jdbc, TransactionTemplate tx) {
PostgreSqlSameStoreInboxAdapter inbox = new PostgreSqlSameStoreInboxAdapter(postgres.dataSource());
InboxScopeDigest scope = new InboxScopeDigest("e".repeat(64));
InboxClaimAttempt attempt = inbox.newClaimAttempt(new OperationId("claim-retention-probe"));
InboxOwner claimed = tx.execute(ignored -> ((InboxClaimOutcome.Acquired) inbox.claim(new InboxClaimRequest(scope, "f".repeat(64), attempt, Duration.ofSeconds(30), Duration.ofDays(7)))).owner());
InboxOwner processing = tx.execute(ignored -> inbox.markProcessing(claimed, new OperationId("start-retention-probe")).owner().orElseThrow());
OperationId failOp = new OperationId("retry-retention-probe");
var first = tx.execute(ignored -> inbox.markRetryable(processing, Duration.ofHours(1), failOp));
var second = tx.execute(ignored -> inbox.markRetryable(processing, Duration.ofHours(9), failOp));
Double remainingHours = jdbc.queryForObject("select extract(epoch from (retention_until - clock_timestamp())) / 3600.0 from inbox_record_v1 where scope_hash=?", Double.class, scope.value());
System.out.println("inboxRetention.first=" + first);
System.out.println("inboxRetention.secondDifferentRetention=" + second);
System.out.printf("inboxRetention.remainingHours=%.3f%n", remainingHours == null ? -1.0 : remainingHours);
}
private static void probeOutboxRetrySchedule(PostgreSqlReadinessSupport postgres, JdbcTemplate jdbc, TransactionTemplate tx) {
PostgreSqlImmutableOutboxAppendAdapter append = new PostgreSqlImmutableOutboxAppendAdapter(postgres.dataSource());
PostgreSqlPollingDeliveryAdapter delivery = new PostgreSqlPollingDeliveryAdapter(postgres.dataSource());
tx.executeWithoutResult(ignored -> cutover(jdbc));
tx.executeWithoutResult(ignored -> append.append(new NewOutboxEventV2(
"event-retry-semantic-probe", "WorkLog", "work-log-probe", 1, 0,
"WorkLogChanged", 1, "portfolio.events", "work-log-probe", "application/json",
"correlation-probe", null, Instant.parse("2026-08-29T00:00:00Z"), "{\"v\":1}")));
ClaimedOutboxDelivery claimed = tx.execute(ignored -> delivery.claimBatch(new OutboxDeliveryClaimRequest("portfolio.events", "probe-relay", 1, Duration.ofSeconds(30)))).getFirst();
OutboxDeliveryTransition transition = new OutboxDeliveryTransition(claimed.owner(), new OperationId("retry-schedule-probe"));
Instant firstAt = Instant.now().plus(Duration.ofHours(1));
Instant secondAt = firstAt.plus(Duration.ofHours(8));
var first = tx.execute(ignored -> delivery.markRetryable(transition, firstAt, "BROKER.TEMP"));
var second = tx.execute(ignored -> delivery.markRetryable(transition, secondAt, "BROKER.TEMP"));
OffsetDateTime stored = jdbc.queryForObject("select next_attempt_at from outbox_delivery_v2 where retention_bucket=? and event_id=? and destination=?", OffsetDateTime.class, claimed.owner().retentionBucket(), claimed.owner().eventId(), claimed.owner().destination());
System.out.println("outboxRetry.first=" + first);
System.out.println("outboxRetry.secondDifferentSchedule=" + second);
System.out.println("outboxRetry.storedEqualsFirst=" + (stored != null && Math.abs(Duration.between(firstAt, stored.toInstant()).toMillis()) < 10));
System.out.println("outboxRetry.storedEqualsSecond=" + (stored != null && Math.abs(Duration.between(secondAt, stored.toInstant()).toMillis()) < 10));
}
private static void cutover(JdbcTemplate jdbc) {
jdbc.queryForObject("select active_epoch from outbox_publication_control_v2 where scope_id='PRIMARY' for update", Long.class);
jdbc.update("insert into outbox_publication_cutover_v2(scope_id,active_epoch,previous_epoch,transition_kind,active_authority,legacy_row_count,legacy_pending_count,legacy_digest,schema_manifest_id,external_manifest_id,activated_at) values ('PRIMARY',2,1,'CUTOVER','POLLING_V2',0,0,?,'jpa-outbox-storage-v2-schema-revision-2',null,clock_timestamp())", "0".repeat(64));
jdbc.update("update outbox_publication_control_v2 set active_epoch=2,active_authority='POLLING_V2',revision=1,updated_at=clock_timestamp() where scope_id='PRIMARY' and active_epoch=1");
}
private static void migrate(PostgreSqlReadinessSupport postgres, String location, String history) {
Flyway.configure().dataSource(postgres.dataSource()).locations(location).table(history).baselineOnMigrate(false).outOfOrder(false).load().migrate();
}
private static void migrateIndependent(PostgreSqlReadinessSupport postgres, String location, String history, String desc) {
Flyway f = Flyway.configure().dataSource(postgres.dataSource()).locations(location).table(history).baselineVersion("0").baselineDescription(desc).baselineOnMigrate(false).outOfOrder(false).load();
f.baseline();
f.migrate();
}
}
@@ -0,0 +1,13 @@
Command: java source-file probe of current PostgreSqlExceptionTranslator with SQLException SQLSTATE 40003
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T08:41:02Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: Direct vendor-translator classification only; no real commit/network failure.
--- stdout/stderr ---
type=JpaPersistenceException
category=UNKNOWN
sqlState=40003
completionUnknown=false
retryable=false
Exit code: 0
@@ -0,0 +1,19 @@
import dev.caskeleton.adapter.outbound.persistence.api.PersistenceOperationName;
import dev.caskeleton.adapter.outbound.persistence.api.error.ConstraintCode;
import dev.caskeleton.adapter.outbound.persistence.postgresql.error.PostgreSqlExceptionTranslator;
import java.sql.SQLException;
import java.time.Duration;
public class PostgreSql40003Probe {
public static void main(String[] args) {
var translator = PostgreSqlExceptionTranslator.with(name -> new ConstraintCode("unknown.constraint"));
var ex = translator.translate(
new SQLException("completion unknown", "40003"),
new PersistenceOperationName("probe.40003"), 1, Duration.ZERO, null);
System.out.println("type=" + ex.getClass().getSimpleName());
System.out.println("category=" + ex.category());
System.out.println("sqlState=" + ex.context().sqlState());
System.out.println("completionUnknown=" + ex.completionUnknown());
System.out.println("retryable=" + ex.retryable());
}
}
@@ -0,0 +1,109 @@
Command: current PostgreSQL vendor denominator/blob/reachability snapshot
Working directory: /shared/codebase/clean-architecture-backend-template
Executed at: 2026-08-29T08:41:14Z
Source revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Observation boundary: 55 production Java + 9 dedicated unit Java + 9 vendor SQL migrations; static production reachability does not prove reflection/external consumers.
--- stdout/stderr ---
src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql files= 55 java= 55 sql= 0
src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql files= 9 java= 9 sql= 0
src/main/resources/db/migration/postgresql files= 9 java= 0 sql= 9
=== tracked blobs ===
e210c55ff2fc7104baafb410518f00d87ff61992 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlIdempotencyClaimRepository.java
31a15ceab20d3f917f2245528622d767890c5fa7 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlLocalTimeoutConfigurer.java
7ccf5807823732f307b4b78c182464d39a1ab3b0 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlOutboxClaimRepository.java
a8d4f2ff53969521d8ab421d9e4199479c8c83a7 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlPersistenceConfig.java
f44cea4ab24938198ebd20fc6df25518cf167621 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlSqlStateErrorMapping.java
1921e42c5540fc320be91ed71e34d1e292cfca2a src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/array/PostgreSqlArraySupport.java
ab72eea155bd448f8256d41f2a4647b405f05394 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintCatalog.java
93d5ee3a7fb1dae72ce2404a3435f7402784ebc4 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintViolationTranslator.java
0c58eb0767e9b41edb060e0073bf7fb75006d118 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/BoundedCopyInputStream.java
4281c693611a6e2f96ce4b5f8943dfbc2556b086 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/CopyAdminCapability.java
9e9a897662c5aec230a8e2887d8101265ee32a1c src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/CopyFormat.java
e0ca01fceacf915699de854fc425cc2dd5efc45c src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/CopyLimits.java
103ed96e08bcafbaddf98cfe13f24d7e0abde162 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/CopyOperationName.java
b1bd50a35d9f99d7dc5b6ec3c51c1e0e573b1650 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/CopyResult.java
79faba1dfaf12885740a5d3739295020ec38e024 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/PostgreSqlCopyLoader.java
caca77feff6b5a2ac3a4d478e42989525c2b4185 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/RegisteredCopyStatement.java
fa83d4d721b0875bfb5b904ae010d99d79c660a2 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/RegisteredPostgreSqlCopyLoader.java
ca9bfd8ff01dbec0a50c6126a21b70047646e230 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/ConstraintCatalog.java
af979cf3dda8e13050c2298cb64986edd16524eb src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
24afb0af6c08d914549210af1e5f8ef25ab0dd80 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlFailureClassifier.java
bc1e253f5f3ab3799c3320d05d1dee43d89b4d12 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlServerErrorFields.java
74675093b7635518e9416b170103cc84e5cfa58d src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlState.java
f574bd966aaa62b393ef67b365a832d300d55efc src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyCapabilityGuard.java
99688068acee94501b7ebbf4ecf1a9d67dc66018 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyClaimGateway.java
f1c5a225ae14283e4d0063372328fac53563d6df src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyDigestPolicy.java
b7409d07805e4fc59f831df1109a1f4fd2eeed6c src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyRecordRow.java
d4d24a87f6c22e00d3050648914fc2f5d64aa33d src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyRowMapper.java
07571331ec0ba0f07661188404b56613e39d577c src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyTransitionGateway.java
dad9804ef50eb85adb15005dffeb659443696056 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java
13986367170c46671dfd5359ad26cee6681eeca2 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/inbox/PostgreSqlSameStoreInboxAdapter.java
83f4f8f808166ee38641af73750b9494940d0469 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/json/JsonDocument.java
f4b157f1754ec7d83b01ef0f10959ace5587ac03 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/json/JsonDocumentCodec.java
4c5784b9fb827c101d513856309ac8116028edde src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/json/JsonPathName.java
f19caae234b6c4207a1e1720ed2bd1f439b0379a src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/json/PostgreSqlJsonQuerySupport.java
d4844280cebcf21136bd73ff0ef05f1610c1df93 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/LockWaitObservation.java
56e76d1578fbec8c5610488cbbe391a45898f0e9 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlLockExceptionTranslator.java
1494b12062d5196956a2b5326c99c318f1837eab src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlLockOptions.java
b123cbfcf55988d84073f2c0e3e5a46a6fe191b7 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlWorkClaimExecutor.java
43939171c67bd9aab00fbdff8851eeda5a054e1a src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/WorkClaim.java
d0b0de2159adf2cdb64b38659b2e38b135b7995f src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/WorkClaimExecutor.java
6fbd709b5fd83a86116550340e9916dd9d1cb92e src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/WorkQueueDefinition.java
bbebbb03d6a8bd188914512b294c99ec9c35ec16 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/WorkQueueName.java
6ad95156dca8483bf6466791cbb3cd707f3a53dc src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlImmutableOutboxAppendAdapter.java
fef15d75ab780049138215967704e2f4951130ec src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java
501432fcd4eb03a3fc3e378326ffd36b3858bda9 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/range/PgRange.java
010a9cf71a3b058799e543abf87c40357211b864 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/range/PgRangeCodec.java
a8b05b0349b6bb3f123ab7eb202b13c1a2d66349 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/range/PgRangeJdbcType.java
84dbb0b9bca3d00a730b8c9929112716e5df1e75 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/range/PostgreSqlRangeQuerySupport.java
4497dd0e3a9ac6b151692796328cac187ed27989 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/write/NativeWriteName.java
d31c9342970bdb681916da3cfe334b08ae5afda1 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/write/PostgreSqlUpsertExecutor.java
145baeaaaf124ee84d4d543bf525e68a07d49422 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/write/RegisteredPostgreSqlUpsertExecutor.java
b307afb6da93242a22963aab4ffed159f4990e26 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/write/RegisteredUpsertStatement.java
b58a3688d624fc11d1cf835d58f9bc9a43025b8c src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/write/UpsertConflictTarget.java
7ed6a25ba22e71487893f8c71176aee8c7950084 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/write/UpsertResult.java
daf7e6191903183de0105396919dd9ae459278b2 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/write/WriteDisposition.java
5c00fe377d8dd53bd63d0f65945fcbaa332c5ab1 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V10__idempotency_request_hash_varchar.sql
a2058af000d86b2d5161b4dc271492d4912e9e13 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V11__durable_operation.sql
9f3a4b53f07839e96b8f22a8a05c6080d9ebc4b7 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V12__live_event_log.sql
5976eeae3efedaa7d1cd81c7ea350f2bffc02741 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V1__idempotency_record.sql
c35fd5b60340a3add2badb4d11a9d18e9df4323e src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V3__outbox_event.sql
c24dea51d458bb69ef0d93b52d0c90dcf5901eb3 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V4__int_lock.sql
0901128b59aa9dd6c3731e0a5a1660b589bd0724 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V5__int_lock_expired_after.sql
c72e38025f312565542199eebd99013197e194d5 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V6__capability_schema_registry_adoption.sql
22e1e0e7b5c3dbc51d1af4aed6a9f93f444307c3 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V9__widen_capability_schema_stream.sql
ab1792e640935b97872387f60c37d0ff1e67ccc3 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlLocalTimeoutConfigurerTest.java
d81b2f3a90c20bec84ab8036716126d72a5a58d8 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlSqlStateErrorMappingTest.java
f56a0ea4ca19b6baa1fa3223f72521346be3e7c9 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlVendorFailureTranslatorRegistrationTest.java
aa73ae39894fda06274357dafb9dec4357494bf4 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/constraint/PostgreSqlConstraintCatalogTest.java
cf49c214796b840914d15ef7eb55ad44c293d07a src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlFailureClassifierTest.java
b1d4eb97523b66b04f78d8ab225d3ef9d76f3e13 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyDigestPolicyTest.java
02427be4d365aa6ab9dcdf4160a978f33cbcf172 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/OwnerSafeIdempotencyPreconditionTest.java
e1839ffd13c95bbe65d1ac950e1ed751ded6b80c src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/lock/PostgreSqlLockOptionsTest.java
4059e40802a63d2577c75b78407f4609cc70e98e src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/range/PgRangeTest.java
=== key production construction ===
--- PostgreSqlOwnerSafeIdempotencyStore
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java:61:public class PostgreSqlOwnerSafeIdempotencyStore implements IdempotencyStorePortV2 {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java:84: public PostgreSqlOwnerSafeIdempotencyStore(JdbcOperations jdbc) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java:88: PostgreSqlOwnerSafeIdempotencyStore(JdbcOperations jdbc, SecureRandom secureRandom) {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/idempotency/PostgreSqlIdempotencyProviderConfig.java:3:import dev.caskeleton.adapter.outbound.persistence.postgresql.idempotency.PostgreSqlOwnerSafeIdempotencyStore;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/idempotency/PostgreSqlIdempotencyProviderConfig.java:60: return new PostgreSqlOwnerSafeIdempotencyStore(jdbc);
--- PostgreSqlSameStoreInboxAdapter
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/inbox/PostgreSqlSameStoreInboxAdapter.java:49:public class PostgreSqlSameStoreInboxAdapter implements InboxStorePort {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/inbox/PostgreSqlSameStoreInboxAdapter.java:204: public PostgreSqlSameStoreInboxAdapter(DataSource dataSource) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/inbox/PostgreSqlSameStoreInboxAdapter.java:208: PostgreSqlSameStoreInboxAdapter(
--- PostgreSqlPollingDeliveryAdapter
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java:44:public class PostgreSqlPollingDeliveryAdapter implements OutboxPollingDeliveryPortV2 {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java:285: public PostgreSqlPollingDeliveryAdapter(DataSource dataSource) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java:289: PostgreSqlPollingDeliveryAdapter(
--- PostgreSqlImmutableOutboxAppendAdapter
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlImmutableOutboxAppendAdapter.java:45:public class PostgreSqlImmutableOutboxAppendAdapter implements OutboxAppendPortV2 {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlImmutableOutboxAppendAdapter.java:153: public PostgreSqlImmutableOutboxAppendAdapter(DataSource dataSource) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlImmutableOutboxAppendAdapter.java:157: PostgreSqlImmutableOutboxAppendAdapter(DataSource dataSource, JdbcOperations jdbc) {
--- RegisteredPostgreSqlCopyLoader
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/RegisteredPostgreSqlCopyLoader.java:30:public final class RegisteredPostgreSqlCopyLoader implements PostgreSqlCopyLoader {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/copy/RegisteredPostgreSqlCopyLoader.java:41: public RegisteredPostgreSqlCopyLoader(
=== current git ===
a24ece9cf797f7ea647e33bf846b115208ed1ba5
Exit code: 0
@@ -0,0 +1,13 @@
Command: compile current PgRange/PgRangeCodec with isolated RangeProbe and execute
Working revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Executed at: 2026-08-29T09:48:56Z
Observation boundary: pure codec self-round-trip; no database/provider involved.
comma.literal=["left,inside","right")
comma.roundTrip=true
comma.lower=left,inside
quote.literal=["left\"inside","right")
quote.error=IllegalArgumentException:postgresql range literal has no endpoint separator
quote-comma.literal=["left\",inside","right")
quote-comma.error=IllegalArgumentException:range lower bound exceeds upper bound
backslash-quote-comma.literal=["left\\\",inside","right")
backslash-quote-comma.error=IllegalArgumentException:range lower bound exceeds upper bound
@@ -0,0 +1,25 @@
package dev.caskeleton.adapter.outbound.persistence.postgresql.range;
public final class RangeProbe {
public static void main(String[] args) {
var codec = new PgRangeCodec<String>(s -> s, s -> s);
var cases = new String[][] {
{"comma", "left,inside", "right"},
{"quote", "left\"inside", "right"},
{"quote-comma", "left\",inside", "right"},
{"backslash-quote-comma", "left\\\",inside", "right"}
};
for (var c : cases) {
var range = PgRange.closedOpen(c[1], c[2]);
String literal = codec.format(range);
try {
var parsed = codec.parse(literal);
System.out.println(c[0] + ".literal=" + literal);
System.out.println(c[0] + ".roundTrip=" + parsed.equals(range));
System.out.println(c[0] + ".lower=" + parsed.lower().orElse("<none>"));
} catch (Throwable t) {
System.out.println(c[0] + ".literal=" + literal);
System.out.println(c[0] + ".error=" + t.getClass().getSimpleName() + ":" + t.getMessage());
}
}
}
}
@@ -0,0 +1,24 @@
Command: temporary analysis probes inside PostgreSqlIdempotencyIntegrationTest; run exact postgresqlIdempotencyIntegrationTest lane
Working revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Executed at: 2026-08-29T09:50:51Z
Observation boundary: real PostgreSQL 16 Testcontainers, production PostgreSqlOwnerSafeIdempotencyStore; temporary tests restored after run.
--- observations ---
expiredInspect.outcome=COMPLETED_REPLAY
expiredInspect.replayUntil=2026-08-29T09:50:47.103976Z
expiredInspect.claimAfterExpiry=TakenOverClaimed
completeReplayTtl.first=COMPLETED
completeReplayTtl.secondDifferentTtl=ALREADY_COMPLETED_SAME_RESULT
completeReplayTtl.storedSeconds=3600
--- gradle tail ---
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses
> Task :adapter:outbound:persistence-jpa:postgresqlIdempotencyIntegrationTest
BUILD SUCCESSFUL in 19s
19 actionable tasks: 2 executed, 17 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
@@ -0,0 +1,35 @@
// Injected temporarily into PostgreSqlIdempotencyIntegrationTest for analysis only.
@Test
void analysisProbeCompleteReplayTtlSemantics() {
IdempotencyOwner executing = claimAndStart("claim-complete-ttl-probe", "start-complete-ttl-probe");
OperationId operation = new OperationId("complete-ttl-probe");
StoredResponse response = new StoredResponse("same-response");
IdempotencyCompleteOutcome first = transactions.execute(
ignored -> store.complete(executing, response, Duration.ofHours(1), operation));
IdempotencyCompleteOutcome differentTtl = transactions.execute(
ignored -> store.complete(executing, response, Duration.ofHours(9), operation));
Long storedSeconds = jdbc.queryForObject(
"select extract(epoch from (replay_until - updated_at))::bigint from idempotency_record where scope_hash = ?",
Long.class, SCOPE.digest());
System.out.println("completeReplayTtl.first=" + first);
System.out.println("completeReplayTtl.secondDifferentTtl=" + differentTtl);
System.out.println("completeReplayTtl.storedSeconds=" + storedSeconds);
}
@Test
void analysisProbeExpiredCompletedInspection() {
IdempotencyClaimAttempt attempt = store.newClaimAttempt(new OperationId("claim-expired-inspect"));
IdempotencyClaimOutcome claim = transactions.execute(ignored -> store.claim(request(attempt, Duration.ofSeconds(5))));
IdempotencyOwner claimed = ((IdempotencyClaimOutcome.Acquired) claim).owner();
IdempotencyOwner executing = transactions.execute(ignored ->
store.markExecutionStarted(claimed, new OperationId("start-expired-inspect")).owner().orElseThrow());
transactions.execute(ignored -> store.complete(
executing, new StoredResponse("expired-response"), Duration.ofMillis(25), new OperationId("complete-expired-inspect")));
jdbc.queryForObject("select pg_sleep(0.05)", Object.class);
var inspection = store.inspect(new IdempotencyInspectionRequest(SCOPE, FINGERPRINT, attempt));
IdempotencyClaimOutcome newClaim = transactions.execute(ignored -> store.claim(request(
store.newClaimAttempt(new OperationId("claim-after-expired-inspect")), Duration.ofSeconds(5))));
System.out.println("expiredInspect.outcome=" + inspection.outcome());
System.out.println("expiredInspect.replayUntil=" + inspection.replayUntil().orElse(null));
System.out.println("expiredInspect.claimAfterExpiry=" + newClaim.getClass().getSimpleName());
}
@@ -0,0 +1,46 @@
Command: ./gradlew :adapter:outbound:persistence-jpa:test <all 9 dedicated postgresql unit test classes> --rerun-tasks
Working revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Executed at: 2026-08-29T09:48:57Z
Classes:
dev.caskeleton.adapter.outbound.persistence.postgresql.PostgreSqlLocalTimeoutConfigurerTest
dev.caskeleton.adapter.outbound.persistence.postgresql.PostgreSqlSqlStateErrorMappingTest
dev.caskeleton.adapter.outbound.persistence.postgresql.PostgreSqlVendorFailureTranslatorRegistrationTest
dev.caskeleton.adapter.outbound.persistence.postgresql.constraint.PostgreSqlConstraintCatalogTest
dev.caskeleton.adapter.outbound.persistence.postgresql.error.PostgreSqlFailureClassifierTest
dev.caskeleton.adapter.outbound.persistence.postgresql.idempotency.IdempotencyDigestPolicyTest
dev.caskeleton.adapter.outbound.persistence.postgresql.idempotency.OwnerSafeIdempotencyPreconditionTest
dev.caskeleton.adapter.outbound.persistence.postgresql.lock.PostgreSqlLockOptionsTest
dev.caskeleton.adapter.outbound.persistence.postgresql.range.PgRangeTest
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
Daemon will be stopped at the end of the build
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :shared-contract:compileJava
> Task :shared-contract:processResources
> Task :shared-contract:classes
> Task :shared-contract:jar
> Task :application-core:compileJava
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes
> Task :application-core:jar
> Task :adapter:outbound:persistence-jpa:compileJava
> Task :adapter:outbound:persistence-jpa:processResources
> Task :adapter:outbound:persistence-jpa:classes
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses
> Task :adapter:outbound:persistence-jpa:compileTestJava
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testClasses
> Task :adapter:outbound:persistence-jpa:test
BUILD SUCCESSFUL in 37s
18 actionable tasks: 18 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
@@ -0,0 +1,43 @@
Command: current PostgreSQL vendor history + prior-review negative-space/provenance snapshot
Working revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Executed at: 2026-08-29T09:53:22Z
Observation boundary: repository history/review text only; does not infer author intent beyond committed text.
=== prior review exact matches ===
483:- commit 08xxx/40003은 body replay 0회이며 reconciliation key를 보존.
603: - `PostgreSqlSameStoreInboxAdapter.java:39-40`
605: - `PostgreSqlPollingDeliveryAdapter.java:31-32`
=== current history key files ===
--- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/error/PostgreSqlExceptionTranslator.java
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
0e61f86 feat(jpa): implement the JPA relational persistence platform
--- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java
0137263 refactor: 각 어댑터터별 리펙토링 진행
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
0a6dd0e refactor: adapter 구현중..
7eb6af5 feat: add JPA production capability
--- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/inbox/PostgreSqlSameStoreInboxAdapter.java
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
7eb6af5 feat: add JPA production capability
--- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
7eb6af5 feat: add JPA production capability
--- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/range/PgRangeCodec.java
0e61f86 feat(jpa): implement the JPA relational persistence platform
--- src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/idempotency/PostgreSqlIdempotencyProviderConfig.java
0137263 refactor: 각 어댑터터별 리펙토링 진행
--- src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java
0137263 refactor: 각 어댑터터별 리펙토링 진행
e98b56e feat: jpa, messaging, notification, mongo, graphql 어댑터터 리펙토링
2f5d2fc feat: jpa, messaging, notification, mongo, graphql 어댑터터 구현체 추가
=== current composition references ===
/shared/codebase/clean-architecture-backend-template/src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:163: : composition.transactionExecutor(transactionManager, vendor);
/shared/codebase/clean-architecture-backend-template/src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/idempotency/PostgreSqlIdempotencyProviderConfig.java:60: return new PostgreSqlOwnerSafeIdempotencyStore(jdbc);
/shared/codebase/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/readiness/PostgreSqlIdempotencyIntegrationTest.java:61: store = new PostgreSqlOwnerSafeIdempotencyStore(jdbc);
/shared/codebase/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlVendorFailureTranslatorRegistrationTest.java:90: assertThat(configuration.postgreSqlVendorFailureTranslator(available(registered)))
/shared/codebase/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlVendorFailureTranslatorRegistrationTest.java:92: assertThat(configuration.postgreSqlVendorFailureTranslator(absent()))
/shared/codebase/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlVendorFailureTranslatorRegistrationTest.java:99: .postgreSqlVendorFailureTranslator(catalogs)
/shared/codebase/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/OwnerSafeIdempotencyPreconditionTest.java:47: new PostgreSqlOwnerSafeIdempotencyStore(jdbc);
/shared/codebase/clean-architecture-backend-template/src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlPersistenceConfig.java:85: public VendorFailureTranslator postgreSqlVendorFailureTranslator(
@@ -0,0 +1,22 @@
Command: temporary focused JUnit probe through PostgreSqlExceptionTranslator -> DefaultJpaRetryPolicy
Working revision: a24ece9cf797f7ea647e33bf846b115208ed1ba5
Executed at: 2026-08-29T09:54:12Z
Observation boundary: current production classes, synthetic SQLException(40003); no real network commit ambiguity.
--- observations ---
<system-out><![CDATA[translated.category=UNKNOWN
translated.completionUnknown=false
decision.disposition=FAIL
decision.reason=failure was classified as non-retryable
--- gradle tail ---
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:compileTestJava
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testClasses
> Task :adapter:outbound:persistence-jpa:test
BUILD SUCCESSFUL in 11s
18 actionable tasks: 2 executed, 16 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
@@ -0,0 +1,31 @@
package dev.caskeleton.adapter.outbound.persistence.postgresql.error;
import static org.assertj.core.api.Assertions.assertThat;
import dev.caskeleton.adapter.outbound.persistence.api.PersistenceOperationName;
import dev.caskeleton.adapter.outbound.persistence.api.error.ConstraintCode;
import dev.caskeleton.adapter.outbound.persistence.api.transaction.RetryProfile;
import dev.caskeleton.adapter.outbound.persistence.api.transaction.TransactionAttempt;
import dev.caskeleton.adapter.outbound.persistence.transaction.DefaultJpaRetryPolicy;
import java.sql.SQLException;
import java.time.Duration;
import java.time.Instant;
import org.junit.jupiter.api.Test;
class PostgreSql40003PolicyProbeTest {
@Test
void currentTranslatorDemotes40003BeforeRetryPolicy() {
var translator = PostgreSqlExceptionTranslator.with(name -> new ConstraintCode("unknown.constraint"));
var translated = translator.translate(
new SQLException("completion unknown", "40003"),
new PersistenceOperationName("probe.40003"), 1, Duration.ZERO, "trace-probe");
var decision = DefaultJpaRetryPolicy.forProfile(
RetryProfile.boundedContention("probe-default", 3))
.classify(translated, TransactionAttempt.first(Instant.EPOCH));
System.out.println("translated.category=" + translated.category());
System.out.println("translated.completionUnknown=" + translated.completionUnknown());
System.out.println("decision.disposition=" + decision.disposition());
System.out.println("decision.reason=" + decision.reason());
assertThat(decision.disposition().name()).isEqualTo("FAIL");
}
}
@@ -0,0 +1,97 @@
Sub-scope 06 baseline capability manifest
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
generatedAt=2026-08-29T09:58:08+00:00
production=61 tests=19 resources=7 total=87
=== PRODUCTION ===
d6b53635fab5efe28a8b512eed519cc45eaca2ca src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/audit/AuditContextPort.java
f69674ff84c9536fac270c6362d7943137847cc5 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/audit/AuditableEntity.java
bc4fd04f8f36269fc9b42e253cb712eb9c8425f9 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/audit/DomainContextAuditContextPort.java
05fbb4e4835b93693a263ff7a4cbf756ee871137 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/auditing/AuditMetadata.java
7513fe3f599f0140639b9dd0e833e9de1c8908e8 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/auditing/JpaAuditingConfiguration.java
8d4b49595e9861372223e9ed95f1ac8dd332b656 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/auditing/JpaAuditorProvider.java
dc931f639c707ea7a2ef3c8f5cc65993acbb6324 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/cache/CacheConcurrencyStrategy.java
a2a3a93ebc07d9d521b55606c6e12b3fdcff09b6 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/cache/CacheRegionCatalog.java
8c05f127ca91be70e6298bc7417127c5809c2cac src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/cache/HibernateCacheGuard.java
3cb1486fc50ec9e8b1af820381dc7a6920473f9b src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/cache/HibernateCachePolicy.java
469ee48da2d6bc65e1232db79511f265cd3524be src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/cache/HibernateCacheSettings.java
a0b1fe10a6cfa478c5915e2b5df25692e1b66128 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/envers/EntityRevision.java
24f3538a56c1f613fa9f2c119f83b7a8af48629c src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/envers/EnversConfigurationGuard.java
b080e0541007165b35dd823c5e0c094824f82daa src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/envers/EnversHistoryPolicy.java
cc5ca2dea37bbbd12cd22c4b4c77318e68fd9274 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/envers/EnversHistoryReader.java
13ae5eef2ea9ed85d6592936c70bfd946eec4e10 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/envers/EnversRevisionMetadata.java
a489793f2003d96a92acd61df2fcb0ab15f43c09 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/envers/HibernateEnversHistoryReader.java
7c7cac59140177fc21751f30e783e2f7273ca0e2 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2IdempotencyClaimRepository.java
b17781e26b16219435c63b83681123cf28f29b93 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2LocalTimeoutConfigurer.java
3f43da6d9e3835fda89f7cbc652f06aa678c9080 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2OutboxClaimRepository.java
e59f4cd68723874cf234e2727186fcf0344eb192 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2PersistenceConfig.java
8057b12146a7d038579953def5f044cd93666485 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2SqlStateErrorMapping.java
d0b41d1531871f372e1a7ae36fb11a3b54af79c8 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/idempotency/IdempotencyClaimRepository.java
6d437e230a463e0fb466e55cad2a30dc166671eb src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/idempotency/IdempotencyReaper.java
ad4277cf5973211252ebb7bba38219dfcc7ab117 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/idempotency/IdempotencyRecordJpaRepository.java
4f435c03e31b1fbcdd1128ee845107af89630d65 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/idempotency/IdempotencyResponseObjectStore.java
280bcc605c280c426d9674f73828c36ea8a2af73 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/idempotency/IdempotencyStoreAdapter.java
8ef66aaa15552843e42384160441b95b2b73aa3f src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/idempotency/entity/IdempotencyRecordEntity.java
934fa5bef8422116dc588311b242e79143025a22 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/idempotency/mapper/IdempotencyRecordEntityMapper.java
4ea0f80eadfe8ed18dc9c976c45f4f13a1936e73 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/liveevent/JpaLiveEventReplayAdapter.java
72fd84b9d434d052f042c840ed8bdba85be3e60c src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/liveevent/LiveEventJpaRepository.java
9280b1b66c83e0ba9acfd7735d41433a97043ad1 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/liveevent/entity/LiveEventEntity.java
7281cd3b422277061e86c0de65775da6232d550b src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/lock/DistributedLockPersistenceConfig.java
1f5ee101df8b1e7ab1617829b4c263619551be0a src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/lock/LockRegistryDistributedLockAdapter.java
b32c34af7d1084f02ebc55de47ae85258f781f20 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/lock/LockSettings.java
8662995b2b434ce9d140d720f464d50acc8131d9 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/ConcurrentIndexMigrationInspector.java
3ce5433ba327bd5a01d4785512ab5e251960ad44 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/FailedConcurrentIndexRecovery.java
1e7efd0c18d5dffc9722a3c14897e2b0f0687174 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/FlywaySchemaPolicy.java
f365a3202dcb93f9816bb7378a765b14b439d1d8 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/FlywayValidationGate.java
eb798036799c9fec5ef9fbff9143384842d9b72b src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/MigrationResource.java
a6460bade97fc3cbbb83ff29eb7ac0105dd4e04c src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/NonTransactionalMigrationPolicy.java
52463c6402db4796894a37a344460a6124a5bc0f src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/SchemaManagementMode.java
55c096468ac6d125009224fc1ae70583efa1ebea src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/migration/SchemaVersionSnapshot.java
92b88f0ff6df0b174032bc1260a3968a6f5970dd src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaMetricTags.java
daaa1ead74c726ff6414b5e2adddf0dcde2611ae src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaRetryObservation.java
615c7194abf0be92e19feee8b686fef2f1282e7d src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaTransactionObservation.java
316e933ea77e8786c2843c13262b2181ac3050f5 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/LowCardinality.java
8e784d4bb8fcc882e6152e9642fbbd3d57490090 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/MicrometerQueryObservation.java
f83323b315e6aa3be03a2704fa06acefc9e94ac2 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/SqlDiagnosticRedactor.java
573cbd52e6588f499ce7c226a95afb2726999caf src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/operation/DurableOperationJpaRepository.java
97d1dc0a650febdce4dc10a41be4297660774dbc src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/operation/DurableOperationStoreAdapter.java
7d04cfbc76733cb6205d4171fef750e3513e6792 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/operation/entity/DurableOperationEntity.java
f23515ac6e15705c99a44f02de283758dba2606d src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxClaimRepository.java
a2fd792a7a44ca4573cbc2e95714d25d5f6738c3 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxEventJpaRepository.java
e21db4a2094a5821cf5a843f38da90c3a231526e src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxReaper.java
e5d7d1d5956991e6259fbcea10f475e0eb6ebc8e src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java
7a7bc80ec6267c294d5b8c3c1b44f8106cf933f6 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/entity/OutboxEventEntity.java
f9a8ea7018b7c34c07b2c073b35404488600c01d src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/DatabasePrivilegeReport.java
64b9723e491dca18b92810103cafeacf1379f5cc src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicy.java
21267dd55aaf067e0d64bb5fa1a3056d4e4f6c6c src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/PostgreSqlRuntimeRoleVerifier.java
5385101663e16082b1b756bea8838a11859c776b src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/SearchPathPolicy.java
=== TEST ===
df2a7c9a87747af5b21ec8d71f3ff814e45aa822 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/audit/AuditableEntityTest.java
0bcb3e700474e3cc9a3cda2382d1ed9a57406348 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/audit/DomainContextAuditContextPortTest.java
e4f6593b30789866ed1c49c0300d823873e28b08 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/auditing/AuditingCandidateStatusTest.java
88cac49a80c626d859430c0bb02a9bf375905f43 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/cache/HibernateCacheGuardTest.java
56e6e13fec1d82409d461fc97e26216a0d56b6fe src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/config/PersistenceEntityScanCoverageTest.java
dd04cd0848e8b8f482fef15031d3bb4d44fc96df src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/config/PersistenceVendorSelectionTest.java
8ada120f62e9fd0c01762e5ff110399182072349 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/h2/H2ClaimSqlTest.java
a6dfcaa8618b8b7d6e3323ff615d4cbf1a0210d3 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/idempotency/IdempotencyReaperTest.java
df1c1ae3d564e7ecfcc4729ed65e20567ee672a7 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/idempotency/IdempotencyStoreAdapterTest.java
e8b06f8bf9aa200c5d1eb81cff3df3e0163abbc9 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/lock/LockRegistryDistributedLockAdapterTest.java
58c139be7c4bfcd19abd65ba70e3db3a9c9338fb src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/migration/ConcurrentIndexMigrationInspectorTest.java
9947d43ff6e809da85a1496e9d6f61c94858cb56 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/migration/FlywayValidationGateTest.java
4e3de88e2cba23966254eb2b57cc980cd45b9d08 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaMetricTagsTest.java
59d8f72e6aff8bb24ddcda80e7bde8c1ff164328 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaObservabilityContractTest.java
7e27d412ec0b47390c73697a430525bc9f8cd137 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxReaperTest.java
64db826bede1a1bc0dda574f8c5a662f793124cf src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxReaperWiringTest.java
60b77beddb7aa5021604f1e9ea496a4ed363c63f src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapterTest.java
b7724e94f9313a0be6aaf9df8b0e448880bcadb0 src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/platform/PoolLaneClaimTest.java
252598f26ba3ad65a7d970860a63028a312337df src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java
=== RESOURCE ===
8dad7255a138638b178599b38f93d2cb99d8cd95 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/core/V1__initialize_or_adopt.sql
4dfc2817a7e2565cd3801b63d2b25be2fe48af81 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/core/V2__widen_capability_schema_stream.sql
9fc609037f48f29e44f1f1cc1dab55cd9ca35b93 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/idempotency/V1__expand_owner_safe_v2.sql
1adcc016d27c692456c7a6573210036d4aa481e3 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/idempotency/V2__request_hash_varchar.sql
e0479be401adbc618e1e662abba6dbc83f0f1bde src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/inbox/V1__initialize_same_store_inbox.sql
e00203a62c76d281548030bb0370bde4f6110e7a src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/outbox-polling/V1__initialize_polling_delivery_v2.sql
0c49e84ca136f79855510e681304ea958e0bbb7a src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/outbox-storage/V1__initialize_or_adopt.sql
@@ -0,0 +1,42 @@
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
command=./gradlew :adapter:outbound:persistence-jpa:jpaPlatformContractTest --tests dev.caskeleton.adapter.outbound.persistence.liveevent.LiveEventLogContractTest.analysisProbeFullySweptStreamReusesPosition --console=plain
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
Daemon will be stopped at the end of the build
> Task :build-logic:extractPluginRequests UP-TO-DATE
> Task :build-logic:generatePluginAdapters UP-TO-DATE
> Task :build-logic:compileJava UP-TO-DATE
> Task :build-logic:compileGroovy UP-TO-DATE
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
> Task :build-logic:pluginDescriptors UP-TO-DATE
> Task :build-logic:processResources UP-TO-DATE
> Task :build-logic:classes UP-TO-DATE
> Task :build-logic:jar UP-TO-DATE
> Task :shared-contract:compileJava UP-TO-DATE
> Task :shared-contract:processResources UP-TO-DATE
> Task :shared-contract:classes UP-TO-DATE
> Task :shared-contract:jar UP-TO-DATE
> Task :application-core:compileJava UP-TO-DATE
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes UP-TO-DATE
> Task :application-core:jar UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses
> Task :adapter:outbound:persistence-jpa:jpaPlatformContractTest
BUILD SUCCESSFUL in 14s
19 actionable tasks: 2 executed, 17 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
=== ASSERTED OBSERVATION (from Gradle test XML) ===
liveEventFullSweep.swept=1
liveEventFullSweep.highestAfterSweep=null
liveEventFullSweep.nextPosition=1
liveEventFullSweep.payloadAtReusedPosition=new-event
source restored after probe: yes
@@ -0,0 +1,314 @@
package dev.caskeleton.adapter.outbound.persistence.liveevent;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import dev.caskeleton.adapter.outbound.persistence.platform.JpaPlatformContractSupport;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
/**
* The live-event log against a real PostgreSQL.
*
* <p>Every claim this store makes is about a constraint or about concurrency, and neither survives
* a fake. A {@code CHECK} a fake does not enforce is a comment; two writers racing for the same
* position has no meaning where only one connection exists — and that race is the whole reason the
* position is part of the primary key rather than assigned in Java.
*/
@Tag("jpa-contract")
class LiveEventLogContractTest {
private static final Instant NOW = Instant.parse("2026-08-25T09:00:00Z");
private static final Duration RETENTION = Duration.ofHours(1);
private static JpaPlatformContractSupport support;
@BeforeAll
static void startServer() {
support = JpaPlatformContractSupport.start();
}
@AfterAll
static void stopServer() {
if (support != null) {
support.close();
}
}
@BeforeEach
void migrate() throws SQLException, IOException {
try (Connection connection = support.connection();
Statement statement = connection.createStatement()) {
statement.execute("DROP SCHEMA public CASCADE");
statement.execute("CREATE SCHEMA public");
statement.execute(migration());
}
}
@Test
@DisplayName("two positions cannot collide on one stream")
void positionsAreUniquePerStream() throws SQLException {
append("orders", 1, "a", NOW);
assertThatThrownBy(() -> append("orders", 1, "b", NOW)).isInstanceOf(SQLException.class);
}
@Test
@DisplayName("the same position on a different stream is fine")
void positionsAreScopedToTheirStream() throws SQLException {
append("orders", 1, "a", NOW);
append("invoices", 1, "b", NOW);
assertThat(replayAfter("orders", 0, NOW)).hasSize(1);
assertThat(replayAfter("invoices", 0, NOW)).hasSize(1);
}
@Test
@DisplayName("two writers racing for the same position produce one row, not two")
void concurrentAppendsCannotShareAPosition() throws Exception {
// The property the primary key exists for. Assigning positions in Java without it would let
// both writers read the same maximum and both write it, and the second event would silently
// overwrite the first — two different events at one address, with a client's cursor pointing
// at whichever survived.
List<String> outcomes = new ArrayList<>();
try (Connection first = support.connection();
Connection second = support.connection()) {
first.setAutoCommit(false);
second.setAutoCommit(false);
insert(first, "orders", 1, "from-first", NOW);
outcomes.add("first-inserted");
first.commit();
try {
insert(second, "orders", 1, "from-second", NOW);
second.commit();
outcomes.add("second-inserted");
} catch (SQLException refused) {
second.rollback();
outcomes.add("second-refused");
}
}
assertThat(outcomes).containsExactly("first-inserted", "second-refused");
assertThat(payloadAt("orders", 1)).isEqualTo("from-first");
}
@Test
@DisplayName("a position of zero is refused by the database, not only by the code")
void positionZeroIsRefused() {
// A stream whose first row claimed 0 and one whose position was never assigned look identical
// to a reader, so the floor is a constraint rather than a convention.
assertThatThrownBy(() -> append("orders", 0, "a", NOW)).isInstanceOf(SQLException.class);
}
@Test
@DisplayName("a row that expires before it happened is refused")
void backwardsRetentionIsRefused() throws SQLException {
try (Connection connection = support.connection();
PreparedStatement statement =
connection.prepareStatement(
"INSERT INTO live_event_log (stream_id, position, payload, occurred_at,"
+ " retained_until) VALUES (?, ?, ?, ?, ?)")) {
statement.setString(1, "orders");
statement.setLong(2, 1);
statement.setString(3, "a");
statement.setTimestamp(4, Timestamp.from(NOW));
statement.setTimestamp(5, Timestamp.from(NOW.minusSeconds(1)));
assertThatThrownBy(statement::executeUpdate).isInstanceOf(SQLException.class);
}
}
@Test
@DisplayName("a replay returns everything after a cursor, in order")
void replayIsOrderedAndExclusive() throws SQLException {
append("orders", 1, "a", NOW);
append("orders", 2, "b", NOW);
append("orders", 3, "c", NOW);
assertThat(replayAfter("orders", 1, NOW)).containsExactly("b", "c");
assertThat(replayAfter("orders", 3, NOW)).isEmpty();
}
@Test
@DisplayName("an expired row is not replayed, even before it is swept")
void expiredRowsAreNotReplayed() throws SQLException {
append("orders", 1, "old", NOW.minus(Duration.ofHours(2)));
append("orders", 2, "new", NOW);
// Filtered on read as well as swept on a schedule. An expired row that has not been collected
// yet is not history the client may have, and serving it would make the window this store
// reports and the window it honours two different things.
assertThat(replayAfter("orders", 0, NOW)).containsExactly("new");
assertThat(earliestRetained("orders", NOW)).isEqualTo(2L);
}
@Test
@DisplayName("a swept position is never handed out again")
void sweptPositionsAreNotReused() throws SQLException {
append("orders", 1, "gone", NOW.minus(Duration.ofHours(2)));
append("orders", 2, "kept", NOW);
assertThat(sweep(NOW)).isOne();
// The highest-ever query deliberately ignores retention. Reusing position 1 would give two
// different events the same address, and a client holding the older cursor would receive the
// newer event as though it were the one it asked to continue after.
assertThat(highestEverAssigned("orders")).isEqualTo(2L);
}
@Test
@DisplayName("the sweep removes only what has expired")
void sweepIsSelective() throws SQLException {
append("orders", 1, "gone", NOW.minus(Duration.ofHours(2)));
append("orders", 2, "kept", NOW);
append("invoices", 1, "kept", NOW);
assertThat(sweep(NOW)).isOne();
assertThat(replayAfter("orders", 0, NOW)).containsExactly("kept");
assertThat(replayAfter("invoices", 0, NOW)).containsExactly("kept");
}
@Test
@DisplayName("an empty stream reports no window at all")
void emptyStreamHasNoWindow() throws SQLException {
assertThat(earliestRetained("nothing-here", NOW)).isNull();
}
@Test
void analysisProbeFullySweptStreamReusesPosition() throws SQLException {
append("orders", 1, "expired-only-row", NOW.minus(Duration.ofHours(2)));
int swept = sweep(NOW);
Long highestAfterFullSweep = highestEverAssigned("orders");
long nextPosition = highestAfterFullSweep == null ? 1L : highestAfterFullSweep + 1L;
append("orders", nextPosition, "new-event", NOW);
System.out.println("liveEventFullSweep.swept=" + swept);
System.out.println("liveEventFullSweep.highestAfterSweep=" + highestAfterFullSweep);
System.out.println("liveEventFullSweep.nextPosition=" + nextPosition);
System.out.println("liveEventFullSweep.payloadAtReusedPosition=" + payloadAt("orders", 1));
assertThat(nextPosition).isEqualTo(1L);
}
private void append(String streamId, long position, String payload, Instant occurredAt)
throws SQLException {
try (Connection connection = support.connection()) {
insert(connection, streamId, position, payload, occurredAt);
}
}
private static void insert(
Connection connection, String streamId, long position, String payload, Instant occurredAt)
throws SQLException {
try (PreparedStatement statement =
connection.prepareStatement(
"INSERT INTO live_event_log (stream_id, position, payload, occurred_at,"
+ " retained_until) VALUES (?, ?, ?, ?, ?)")) {
statement.setString(1, streamId);
statement.setLong(2, position);
statement.setString(3, payload);
statement.setTimestamp(4, Timestamp.from(occurredAt));
statement.setTimestamp(5, Timestamp.from(occurredAt.plus(RETENTION)));
statement.executeUpdate();
}
}
private List<String> replayAfter(String streamId, long afterPosition, Instant now)
throws SQLException {
List<String> payloads = new ArrayList<>();
try (Connection connection = support.connection();
PreparedStatement statement =
connection.prepareStatement(
"SELECT payload FROM live_event_log WHERE stream_id = ? AND position > ?"
+ " AND retained_until > ? ORDER BY position ASC")) {
statement.setString(1, streamId);
statement.setLong(2, afterPosition);
statement.setTimestamp(3, Timestamp.from(now));
try (ResultSet rows = statement.executeQuery()) {
while (rows.next()) {
payloads.add(rows.getString(1));
}
}
}
return payloads;
}
private Long earliestRetained(String streamId, Instant now) throws SQLException {
try (Connection connection = support.connection();
PreparedStatement statement =
connection.prepareStatement(
"SELECT min(position) FROM live_event_log WHERE stream_id = ?"
+ " AND retained_until > ?")) {
statement.setString(1, streamId);
statement.setTimestamp(2, Timestamp.from(now));
try (ResultSet rows = statement.executeQuery()) {
rows.next();
long value = rows.getLong(1);
return rows.wasNull() ? null : value;
}
}
}
private Long highestEverAssigned(String streamId) throws SQLException {
try (Connection connection = support.connection();
PreparedStatement statement =
connection.prepareStatement(
"SELECT max(position) FROM live_event_log WHERE stream_id = ?")) {
statement.setString(1, streamId);
try (ResultSet rows = statement.executeQuery()) {
rows.next();
long value = rows.getLong(1);
return rows.wasNull() ? null : value;
}
}
}
private String payloadAt(String streamId, long position) throws SQLException {
try (Connection connection = support.connection();
PreparedStatement statement =
connection.prepareStatement(
"SELECT payload FROM live_event_log WHERE stream_id = ? AND position = ?")) {
statement.setString(1, streamId);
statement.setLong(2, position);
try (ResultSet rows = statement.executeQuery()) {
return rows.next() ? rows.getString(1) : null;
}
}
}
private int sweep(Instant now) throws SQLException {
try (Connection connection = support.connection();
PreparedStatement statement =
connection.prepareStatement("DELETE FROM live_event_log WHERE retained_until <= ?")) {
statement.setTimestamp(1, Timestamp.from(now));
return statement.executeUpdate();
}
}
private static String migration() throws IOException {
try (InputStream source =
LiveEventLogContractTest.class.getResourceAsStream(
"/db/migration/postgresql/V12__live_event_log.sql")) {
if (source == null) {
throw new IllegalStateException(
"the migration this store is defined by is missing from the classpath, so the test"
+ " would create its own idea of the schema and certify that instead");
}
return new String(source.readAllBytes(), StandardCharsets.UTF_8);
}
}
}
@@ -0,0 +1,250 @@
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
=== JpaAdapterComponentsConfig package scan ===
1 package dev.caskeleton.adapter.outbound.persistence.config;
2
3 import org.springframework.context.annotation.ComponentScan;
4 import org.springframework.context.annotation.Configuration;
5
6 /**
7 * Registers the JPA adapter's scanned components, which nothing registered (JPA-INT-006).
8 *
9 * <p>The composition root's {@code @ComponentScan} excludes {@code
10 * dev.caskeleton.adapter.outbound.persistence.**} by regex, and that exclusion is correct: it is
11 * what makes an optional capability optional, so a deployment with JPA off assembles no persistence
12 * beans rather than assembling them and hoping each one remembered to carry the switch.
13 *
14 * <p>What was missing is the other half. Eight classes in this leaf are written as scanned
15 * components — {@code SpringTransactionPort}, {@code PersistenceExceptionTranslator}, {@code
16 * StandardSqlStateErrorMapping}, {@code DomainContextAuditContextPort}, the idempotency store and
17 * its reaper, and the outbox store and its reaper — and once the broad scan stopped reaching them,
18 * nothing else did. They are annotated {@code @Component} and {@code @Repository} and were beans in
19 * no running application: {@code TransactionPort} in particular had no implementation at all, so
20 * every use case that opens a transaction had no port to open it with.
21 *
22 * <p>It surfaced as an unsatisfied dependency the first time a capability that needs a transaction
23 * was actually assembled — the notification orchestrator, in the local-notification-ingest lane —
24 * rather than as anything a unit test could see, because each of these classes is constructed
25 * directly by its own tests.
26 *
27 * <p>So the scan is restored, narrowed to the packages it should always have covered and reachable
28 * only through {@code PersistenceJpaRootAutoConfiguration}, which carries the JPA master switch.
29 * Off is still structural.
30 *
31 * <p>Two packages are deliberately absent:
32 *
33 * <ul>
34 * <li>{@code ..persistence.fileserver} — gated on its own capability switch, scanned by {@link
35 * dev.caskeleton.adapter.outbound.persistence.fileserver.FileserverJpaPersistenceConfig};
36 * <li>{@code ..persistence.notification} — assembled explicitly, bean by bean, by {@code
37 * NotificationJpaPersistenceFacade}.
38 * </ul>
39 *
40 * <p>Components under these packages keep their own {@code @ConditionalOnProperty} guards; being
41 * scanned makes them candidates, not unconditional beans.
42 *
43 * <p>{@code ..persistence.lock} is in the list for the same reason and with the same history.
44 * {@code DistributedLockPersistenceConfig} owns both lock providers — the in-process registry and
45 * the JDBC one — and was registered by nothing but a test calling {@code ctx.register(...)}. So a
46 * single-instance deployment had no {@code DistributedLockPort} at all, and a multi-instance one
47 * could not start: the composition root's own {@code DistributedLockConfig} asks for a bean
48 * qualified {@code jdbcDistributedLock} that only that configuration declares.
49 *
50 * <p>Each package's {@code @ConfigurationProperties} type is enabled by a configuration inside that
51 * same package — {@code JpaTransactionConfig} for {@code JpaTransactionSettings}, {@code
52 * DistributedLockPersistenceConfig} for {@code LockSettings} — rather than from here. Enabling them
53 * centrally would give {@code config} an edge to {@code lock} and {@code transaction} that the
54 * module map does not grant it, and the map is right: this class knows which packages to scan, not
55 * what is inside them. They need enabling at all because {@code @ConfigurationPropertiesScan}
56 * excludes this tree as deliberately as {@code @ComponentScan} does.
57 */
58 @Configuration(proxyBeanMethods = false)
59 @ComponentScan(
60 basePackages = {
61 "dev.caskeleton.adapter.outbound.persistence.audit",
62 "dev.caskeleton.adapter.outbound.persistence.failure",
63 "dev.caskeleton.adapter.outbound.persistence.idempotency",
64 "dev.caskeleton.adapter.outbound.persistence.lock",
65 "dev.caskeleton.adapter.outbound.persistence.outbox",
66 "dev.caskeleton.adapter.outbound.persistence.transaction"
67 })
68 public class JpaAdapterComponentsConfig {}
=== runtime-role policy production callers ===
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/PostgreSqlRuntimeRoleVerifier.java:57: policy.requireSafe(verify(dataSource));
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicy.java:19:public record DatabaseRolePolicy(Set<String> allowedRoles, SearchPathPolicy searchPathPolicy) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicy.java:55: searchPathPolicy.requireSafe(report.searchPath());
=== runtime-role policy all callers ===
src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/platform/PostgreSqlSecurityContractTest.java:89: new DatabaseRolePolicy(
src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/platform/PostgreSqlSecurityContractTest.java:92: new PostgreSqlRuntimeRoleVerifier().requireSafe(runtime, policy);
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java:15: new DatabaseRolePolicy(
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java:21: policy.requireSafe(new DatabasePrivilegeReport("app_runtime", "app, pg_catalog", false, false));
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java:29: policy.requireSafe(new DatabasePrivilegeReport("app_runtime", "app", true, false)))
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java:38: () -> policy.requireSafe(new DatabasePrivilegeReport("postgres", "app", false, false)))
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java:48: policy.requireSafe(
src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java:57: policy.requireSafe(new DatabasePrivilegeReport("app_runtime", "\"$user\", app", false, false));
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/PostgreSqlRuntimeRoleVerifier.java:57: policy.requireSafe(verify(dataSource));
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicy.java:19:public record DatabaseRolePolicy(Set<String> allowedRoles, SearchPathPolicy searchPathPolicy) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicy.java:55: searchPathPolicy.requireSafe(report.searchPath());
=== runtime role advertised Stable + report semantics ===
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:61: CapabilitySupport.stable(JpaCapability.RUNTIME_ROLE_VERIFICATION),
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java:112: return roleVerifier.verify(dataSource);
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:25: boolean runtimeRoleVerified,
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:56: privileges != null && !privileges.holdsCreatePrivilege(),
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java:62: return openInViewDisabled && runtimeRoleVerified;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java:194: public InitializingBean jpaPlatformStartupCheck(
=== startup contract docs ===
## Startup verification
`PostgreSqlRuntimeRoleVerifier` asks the *server* what the connection can do:
```sql
select current_user,
current_setting('search_path'),
has_schema_privilege(current_user, current_schema(), 'CREATE'),
has_database_privilege(current_user, current_database(), 'CREATE')
```
Configuration cannot answer this. Effective privileges come from direct grants, inherited role
memberships, `PUBLIC` grants, and schema ownership, and no reading of a deployment manifest
reconstructs that combination reliably.
Startup fails when the runtime role is not on the allowlist, or holds `CREATE` on the schema or the
database.
## search_path
`SearchPathPolicy` is an allowlist. `search_path` decides which schema an unqualified name resolves
to, so a writable untrusted schema on it — classically `public`, where `CREATE` was granted broadly
before PostgreSQL 15 — lets a planted table, function, or operator shadow the real one, and the
application executes it without noticing. `$user` is exempt: only the connected role owns it.
Refusing the runtime role `CREATE` closes the same route from the other side.
### 41.2 search_path
Application role의 `search_path`를 고정하고 untrusted schema의 object resolution을 차단한다. startup verifier가 current_user, current_schema, search_path, schema CREATE privilege를 검사한다.
### 41.3 Injection 방어
```text
JPQL/Native values → parameter binding
Dynamic sort → allowlist
Dynamic table/column → enum/catalog mapping만
Entity → API mass binding 금지
```
### 41.4 Secret
DB password는 secret manager/workload identity에서 주입하고 config·log·metric에 기록하지 않는다.
---
## 42. Spring Boot AutoConfiguration
### 42.1 Properties
```yaml
backend:
jpa:
enabled: true
require-postgresql: true
open-in-view: false
schema-management: VALIDATE
transaction-profiles: {}
retry-profiles: {}
observability:
hibernate-statistics: true
sql-parameters: false
security:
verify-runtime-role: true
verify-search-path: true
```
### 42.2 Startup Failures
```text
spring.jpa.open-in-view=true
prod ddl-auto != validate/none
unsupported PostgreSQL version
runtime role has DDL privilege
migration checksum mismatch
required transaction profile timeout missing
Experimental module enabled without feature flag
```
=== outbox composition path ===
src/app-bootstrap/src/main/resources/application.yml:662: # metrics, or store binding at all; relay-enabled below only decides whether the scheduler runs.
src/app-bootstrap/src/main/resources/application.yml:666: relay-enabled: ${APP_OUTBOX_RELAY_ENABLED:false}
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxMetrics.java:5:import dev.caskeleton.application.outbox.OutboxStorePort;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxMetrics.java:30: private final OutboxStorePort store;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxMetrics.java:35: public OutboxMetrics(OutboxStorePort store, ObjectProvider<MeterRegistry> meterRegistryProvider) {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxRelayBrokerRequirementValidator.java:10: * <p>{@code ca-skeleton.outbox.relay-enabled} defaults to true and {@code app.messaging.broker}
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxRelayBrokerRequirementValidator.java:23: private static final String RELAY_KEY = "ca-skeleton.outbox.relay-enabled";
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxRelayScheduler.java:5:import dev.caskeleton.application.outbox.PublishPendingOutboxEventsUseCase;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxRelayScheduler.java:19: private final PublishPendingOutboxEventsUseCase relayUseCase;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxRelayScheduler.java:24: PublishPendingOutboxEventsUseCase relayUseCase, OutboxMetrics metrics, Clock clock) {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:6:import dev.caskeleton.application.outbox.OutboxStorePort;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:7:import dev.caskeleton.application.outbox.PublishPendingOutboxEventsUseCase;
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:24: * <p>{@code ca-skeleton.outbox.enabled} is the capability switch; {@code relay-enabled} only
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:52: name = "ca-skeleton.outbox.relay-enabled",
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:56: OutboxStorePort store,
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:64: PublishPendingOutboxEventsUseCase relayUseCase =
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:65: new PublishPendingOutboxEventsUseCase(
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/outbox/OutboxConfig.java:95: OutboxStorePort store, ObjectProvider<MeterRegistry> meterRegistryProvider) {
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/activation/CapabilityDependencyValidator.java:50: boolean relay = isOn(environment, "ca-skeleton.outbox.relay-enabled");
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/activation/CapabilityDependencyValidator.java:67: "ca-skeleton.outbox.relay-enabled=true only starts the scheduler for a capability that is "
src/application-core/src/main/java/dev/caskeleton/application/inbox/InboxStorePort.java:22: InboxTransitionOutcome markDead(InboxOwner owner, Duration retention, OperationId operationId);
src/application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/DefaultCleanupService.java:87: markFailed(item, "BATCH_BYTE_BUDGET_EXHAUSTED", now);
src/application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/DefaultCleanupService.java:123: markFailed(item, "CLEANUP_ATTEMPT_FAILED", now);
src/application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/DefaultCleanupService.java:140: markFailed(item, "ACTIVE_WRITER_LEASE", now);
src/application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/DefaultCleanupService.java:206: private void markFailed(CleanupItem item, String reasonCode, Instant now) {
src/application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/DefaultCleanupService.java:209: queue.markFailed(item, reasonCode, now.plus(retryBackoff));
src/application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/CleanupQueue.java:21: void markFailed(CleanupItem item, String reasonCode, Instant nextAttemptAt);
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxRelayResult.java:7: * Result returned by {@link PublishPendingOutboxEventsUseCase} after one relay cycle: the number of
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:35:public class PublishPendingOutboxEventsUseCase
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:38: private final OutboxStorePort store;
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:60: public PublishPendingOutboxEventsUseCase(
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:61: OutboxStorePort store,
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:90: List<OutboxEvent> claimed = tx.inWrite(() -> store.claimBatch(batchSize, now, inFlightTimeout));
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:141: tx.inWrite(() -> store.markPublished(event.eventId()));
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:159: tx.inWrite(() -> store.markFailed(event.eventId(), nextAttemptAt));
src/application-core/src/main/java/dev/caskeleton/application/outbox/PublishPendingOutboxEventsUseCase.java:175: tx.inWrite(() -> store.markDead(event.eventId()));
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxStorePort.java:14:public interface OutboxStorePort {
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxStorePort.java:27: List<OutboxEvent> claimBatch(int batchSize, Instant now, Duration inFlightTimeout);
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxStorePort.java:35: void markPublished(String eventId);
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxStorePort.java:45: void markFailed(String eventId, Instant nextAttemptAt);
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxStorePort.java:54: void markDead(String eventId);
src/application-core/src/main/java/dev/caskeleton/application/outbox/OutboxEvent.java:7: * Immutable view of an outbox event row returned by {@link OutboxStorePort#claimBatch}.
src/application-core/src/main/java/dev/caskeleton/application/outbox/v2/OutboxPollingDeliveryPortV2.java:9: List<ClaimedOutboxDelivery> claimBatch(OutboxDeliveryClaimRequest request);
src/application-core/src/main/java/dev/caskeleton/application/outbox/v2/OutboxPollingDeliveryPortV2.java:11: OutboxDeliveryTransitionOutcome markPublished(OutboxDeliveryTransition transition);
src/application-core/src/main/java/dev/caskeleton/application/outbox/v2/OutboxPollingDeliveryPortV2.java:16: OutboxDeliveryTransitionOutcome markDead(OutboxDeliveryTransition transition, String errorCode);
src/application-core/src/main/java/dev/caskeleton/application/idempotency/v2/IdempotencyStorePortV2.java:28: IdempotencyFailOutcome markFailed(
src/application-core/src/main/java/dev/caskeleton/application/idempotency/v2/IdempotencyExecutorV2.java:217: store.markFailed(
src/application-core/src/main/java/dev/caskeleton/application/idempotency/v2/IdempotencyExecutorV2.java:282: store.markFailed(
src/application-core/src/main/java/dev/caskeleton/application/notification/platform/callback/ProviderEventLedger.java:26: void markFailed(ProviderEventRecordId eventId, String errorCode);
src/application-core/src/main/java/dev/caskeleton/application/notification/platform/callback/ProviderEventProjectionService.java:58: ledger.markFailed(event.id(), "NO_PROJECTOR");
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaCleanupQueue.java:145: public void markFailed(CleanupItem item, String reasonCode, Instant nextAttemptAt) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:8:import dev.caskeleton.application.outbox.OutboxStorePort;
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:18: * JPA-backed {@link OutboxAppendPort} + {@link OutboxStorePort}. {@link #append} and the claim/mark
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:24:public class OutboxStoreAdapter implements OutboxAppendPort, OutboxStorePort {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:29: public OutboxStoreAdapter(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:53: public List<OutboxEvent> claimBatch(int batchSize, Instant now, Duration inFlightTimeout) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:72: public void markPublished(String eventId) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:84: public void markFailed(String eventId, Instant nextAttemptAt) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxStoreAdapter.java:99: public void markDead(String eventId) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/inbox/PostgreSqlSameStoreInboxAdapter.java:350: public InboxTransitionOutcome markDead(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java:297: public List<ClaimedOutboxDelivery> claimBatch(OutboxDeliveryClaimRequest request) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java:313: public OutboxDeliveryTransitionOutcome markPublished(OutboxDeliveryTransition transition) {
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/outbox/PostgreSqlPollingDeliveryAdapter.java:366: public OutboxDeliveryTransitionOutcome markDead(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java:294: public IdempotencyFailOutcome markFailed(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/RecipientDeliveryJpaRepository.java:36: List<Object[]> claimBatch(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaRecipientLeaseStore.java:47: return recipients.claimBatch(now, limit, workerId, leaseUntil).stream()
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JpaProviderEventLedger.java:146: public void markFailed(ProviderEventRecordId eventId, String errorCode) {
=== durable/live adapter construction references ===
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/liveevent/JpaLiveEventReplayAdapter.java:54: public JpaLiveEventReplayAdapter(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/operation/DurableOperationStoreAdapter.java:39: public DurableOperationStoreAdapter(DurableOperationJpaRepository repository) {
@@ -0,0 +1,42 @@
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
command=./gradlew :adapter:outbound:persistence-jpa:jpaPlatformContractTest --tests dev.caskeleton.adapter.outbound.persistence.operation.DurableOperationStoreContractTest.analysisProbeExpiredLeaseCanStillSucceedBeforeTakeover --console=plain
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
Daemon will be stopped at the end of the build
> Task :build-logic:extractPluginRequests UP-TO-DATE
> Task :build-logic:generatePluginAdapters UP-TO-DATE
> Task :build-logic:compileJava UP-TO-DATE
> Task :build-logic:compileGroovy UP-TO-DATE
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
> Task :build-logic:pluginDescriptors UP-TO-DATE
> Task :build-logic:processResources UP-TO-DATE
> Task :build-logic:classes UP-TO-DATE
> Task :build-logic:jar UP-TO-DATE
> Task :shared-contract:compileJava UP-TO-DATE
> Task :shared-contract:processResources UP-TO-DATE
> Task :shared-contract:classes UP-TO-DATE
> Task :shared-contract:jar UP-TO-DATE
> Task :application-core:compileJava UP-TO-DATE
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes UP-TO-DATE
> Task :application-core:jar UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses
> Task :adapter:outbound:persistence-jpa:jpaPlatformContractTest
BUILD SUCCESSFUL in 14s
19 actionable tasks: 2 executed, 17 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
=== ASSERTED OBSERVATION (from Gradle test XML) ===
durableExpiredLease.completionAt=2026-08-25T09:02:01Z
durableExpiredLease.leaseExpiredAtCompletion=true
durableExpiredLease.succeedUpdatedRows=1
durableExpiredLease.finalState=SUCCEEDED
source restored after probe: yes
@@ -0,0 +1,503 @@
package dev.caskeleton.adapter.outbound.persistence.operation;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import dev.caskeleton.adapter.outbound.persistence.platform.JpaPlatformContractSupport;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
/**
* The durable operation store against a real PostgreSQL.
*
* <p>Every claim in this store is about concurrency or about a constraint, and neither survives
* being tested against a fake. {@code FOR UPDATE SKIP LOCKED} does not exist in H2's PostgreSQL
* mode with the semantics that matter, a {@code CHECK} constraint a fake does not enforce is a
* comment, and a lease race has no meaning where only one connection exists.
*
* <p>A dead worker is written the only way that is faithful here: the row a crash leaves behind — a
* lease nobody will renew — and then recovery runs against it. That is precisely what a killed
* process is from the database's side.
*/
@Tag("jpa-contract")
class DurableOperationStoreContractTest {
private static final Instant NOW = Instant.parse("2026-08-25T09:00:00Z");
private static final Duration LEASE = Duration.ofMinutes(2);
private static JpaPlatformContractSupport support;
@BeforeAll
static void startServer() {
support = JpaPlatformContractSupport.start();
}
@AfterAll
static void stopServer() {
if (support != null) {
support.close();
}
}
@BeforeEach
void migrate() throws SQLException, IOException {
try (Connection connection = support.connection();
Statement statement = connection.createStatement()) {
statement.execute("DROP SCHEMA public CASCADE");
statement.execute("CREATE SCHEMA public");
statement.execute(migration());
}
}
@Test
@DisplayName("a lease left behind by a dead worker is reclaimed by another")
void expiredLeaseCanBeReclaimedAfterWorkerCrash() throws SQLException {
submit("op-1", "hash-1");
claim("worker-a", NOW);
// The worker is gone. Nothing renews the lease, and nothing else may touch the operation until
// it lapses — that exclusivity is the point of the lease.
assertThat(claim("worker-b", NOW.plusSeconds(30))).isEmpty();
assertThat(claim("worker-b", NOW.plus(LEASE).plusSeconds(1))).contains("op-1");
assertThat(state("op-1")).isEqualTo("RUNNING");
assertThat(leaseOwner("op-1")).isEqualTo("worker-b");
}
@Test
@DisplayName("a reclaimed operation keeps the moment it first started")
void reclaimKeepsTheOriginalStartTime() throws SQLException {
submit("op-1", "hash-1");
claim("worker-a", NOW);
claim("worker-b", NOW.plus(LEASE).plusSeconds(1));
// COALESCE, not an unconditional assignment. Overwriting it would make every retry look like a
// fresh start and hide how long the operation has really been running.
assertThat(startedAt("op-1")).isEqualTo(NOW);
}
@Test
@DisplayName("two workers claiming at once take different operations")
void concurrentWorkersDoNotTakeTheSameOperation() throws Exception {
submit("op-1", "hash-1");
submit("op-2", "hash-2");
List<String> claimed = new ArrayList<>();
try (Connection first = support.connection();
Connection second = support.connection()) {
first.setAutoCommit(false);
second.setAutoCommit(false);
// Interleaved on purpose and inside open transactions: with plain FOR UPDATE the second
// claim blocks here until the first commits, and with no locking at all both take op-1.
claimed.add(claimOn(first, "worker-a", NOW).orElseThrow());
claimed.add(claimOn(second, "worker-b", NOW).orElseThrow());
first.commit();
second.commit();
}
assertThat(claimed).containsExactlyInAnyOrder("op-1", "op-2");
}
@Test
@DisplayName("a worker whose lease lapsed cannot record a result over its successor")
void aStaleWorkerCannotOverwriteTheNewOwner() throws SQLException {
submit("op-1", "hash-1");
claim("worker-a", NOW);
claim("worker-b", NOW.plus(LEASE).plusSeconds(1));
// worker-a is alive and still finishing. Letting it report here is how one operation ends up
// with two answers, and which one survives would come down to timing.
assertThat(succeed("op-1", "worker-a", "/results/a")).isZero();
assertThat(succeed("op-1", "worker-b", "/results/b")).isOne();
assertThat(resultReference("op-1")).isEqualTo("/results/b");
}
@Test
@DisplayName("a heartbeat from a worker that lost the lease fails rather than extending it")
void heartbeatFromAStaleWorkerFails() throws SQLException {
submit("op-1", "hash-1");
claim("worker-a", NOW);
claim("worker-b", NOW.plus(LEASE).plusSeconds(1));
assertThat(heartbeat("op-1", "worker-a", NOW.plus(LEASE).plusSeconds(2))).isZero();
assertThat(heartbeat("op-1", "worker-b", NOW.plus(LEASE).plusSeconds(2))).isOne();
}
@Test
@DisplayName("cancel works once and never reverses a finished operation")
void cancelIsIdempotentAndNeverReversesTerminal() throws SQLException {
submit("op-1", "hash-1");
claim("worker-a", NOW);
succeed("op-1", "worker-a", "/results/a");
// The work happened. Recording it as CANCELED would replace the only record of a real outcome
// with a claim that it never occurred.
assertThat(cancel("op-1", "alice")).isZero();
assertThat(state("op-1")).isEqualTo("SUCCEEDED");
submit("op-2", "hash-2");
assertThat(cancel("op-2", "alice")).isOne();
assertThat(cancel("op-2", "alice")).isZero();
assertThat(state("op-2")).isEqualTo("CANCELED");
}
@Test
@DisplayName("another principal cannot cancel an operation they did not submit")
void cancelIsScopedToThePrincipal() throws SQLException {
submit("op-1", "hash-1");
assertThat(cancel("op-1", "mallory")).isZero();
assertThat(state("op-1")).isEqualTo("PENDING");
}
@Test
@DisplayName("a cancelled operation is not handed to a worker")
void cancelledOperationsAreNotClaimable() throws SQLException {
submit("op-1", "hash-1");
cancel("op-1", "alice");
assertThat(claim("worker-a", NOW)).isEmpty();
}
@Test
@DisplayName("an identical resubmission is refused by the unique constraint")
void resubmissionCollidesOnScope() throws SQLException {
submit("op-1", "hash-1");
// The constraint is the arbiter rather than a prior read: two identical submissions can arrive
// at the same instant, and a read-then-insert lets both through.
assertThatThrownBy(() -> submit("op-2", "hash-1")).isInstanceOf(SQLException.class);
}
@Test
@DisplayName("a succeeded row with no result is refused by the database")
void succeededRowRequiresAResult() throws SQLException {
submit("op-1", "hash-1");
assertThatThrownBy(
() ->
execute(
"UPDATE durable_operation SET state = 'SUCCEEDED', completed_at = now(),"
+ " lease_owner = NULL WHERE operation_id = 'op-1'"))
.isInstanceOf(SQLException.class)
.hasMessageContaining("ck_durable_operation_succeeded_has_result");
}
@Test
@DisplayName("a running row with no lease is refused by the database")
void runningRowRequiresALease() {
assertThatThrownBy(
() -> {
submit("op-1", "hash-1");
execute("UPDATE durable_operation SET state = 'RUNNING' WHERE operation_id = 'op-1'");
})
.isInstanceOf(SQLException.class)
.hasMessageContaining("ck_durable_operation_running_has_lease");
}
@Test
@DisplayName("a state outside the published vocabulary is refused")
void stateVocabularyIsClosed() {
// A worker that could write its own state would extend the published contract by writing a
// row, and a client branching on status would meet a value that is in no version of the API.
assertThatThrownBy(
() -> {
submit("op-1", "hash-1");
execute(
"UPDATE durable_operation SET state = 'ALMOST_DONE' WHERE operation_id = 'op-1'");
})
.isInstanceOf(SQLException.class)
.hasMessageContaining("ck_durable_operation_state");
}
@Test
@DisplayName("an aged-out operation is expired, and a finished one is left alone")
void expirySweepLeavesRealOutcomesAlone() throws SQLException {
submit("op-1", "hash-1");
submit("op-2", "hash-2");
claim("worker-a", NOW);
List<String> expired = expireStale(NOW.plus(Duration.ofDays(2)));
assertThat(expired).containsExactlyInAnyOrder("op-1", "op-2");
submit("op-3", "hash-3");
claim("worker-b", NOW);
succeed("op-3", "worker-b", "/results/c");
assertThat(expireStale(NOW.plus(Duration.ofDays(2)))).isEmpty();
assertThat(state("op-3")).isEqualTo("SUCCEEDED");
}
@Test
@DisplayName("reclaim returns lapsed leases to the queue")
void reclaimReturnsLapsedLeases() throws SQLException {
submit("op-1", "hash-1");
claim("worker-a", NOW);
assertThat(reclaim(NOW.plusSeconds(30))).isEmpty();
assertThat(reclaim(NOW.plus(LEASE).plusSeconds(1))).containsExactly("op-1");
assertThat(state("op-1")).isEqualTo("PENDING");
assertThat(leaseOwner("op-1")).isNull();
}
private void submit(String operationId, String hash) throws SQLException {
try (Connection connection = support.connection();
PreparedStatement statement =
connection.prepareStatement(
"""
INSERT INTO durable_operation
(operation_id, tenant, principal, operation_name, request_hash, payload,
state, submitted_at, expires_at)
VALUES (?, '', 'alice', 'transfers.create', ?, '{}', 'PENDING', ?, ?)
""")) {
statement.setString(1, operationId);
statement.setString(2, hash);
statement.setTimestamp(3, Timestamp.from(NOW));
statement.setTimestamp(4, Timestamp.from(NOW.plus(Duration.ofDays(1))));
statement.executeUpdate();
}
}
private Optional<String> claim(String workerId, Instant now) throws SQLException {
try (Connection connection = support.connection()) {
return claimOn(connection, workerId, now);
}
}
private Optional<String> claimOn(Connection connection, String workerId, Instant now)
throws SQLException {
try (PreparedStatement statement =
connection.prepareStatement(
"""
UPDATE durable_operation
SET state = 'RUNNING',
lease_owner = ?,
lease_acquired_at = ?,
lease_expires_at = ?,
started_at = COALESCE(started_at, ?),
row_version = row_version + 1
WHERE operation_id = (
SELECT operation_id
FROM durable_operation
WHERE (state = 'PENDING'
OR (state = 'RUNNING' AND lease_expires_at <= ?))
AND expires_at > ?
ORDER BY submitted_at
FOR UPDATE SKIP LOCKED
LIMIT 1)
RETURNING operation_id
""")) {
statement.setString(1, workerId);
statement.setTimestamp(2, Timestamp.from(now));
statement.setTimestamp(3, Timestamp.from(now.plus(LEASE)));
statement.setTimestamp(4, Timestamp.from(now));
statement.setTimestamp(5, Timestamp.from(now));
statement.setTimestamp(6, Timestamp.from(now));
try (ResultSet rows = statement.executeQuery()) {
return rows.next() ? Optional.of(rows.getString(1)) : Optional.empty();
}
}
}
@Test
void analysisProbeExpiredLeaseCanStillSucceedBeforeTakeover() throws SQLException {
submit("op-expired", "hash-expired");
claim("worker-a", NOW);
Instant afterLease = NOW.plus(LEASE).plusSeconds(1);
int updated = succeedAt("op-expired", "worker-a", "/results/stale", afterLease);
System.out.println("durableExpiredLease.completionAt=" + afterLease);
System.out.println("durableExpiredLease.leaseExpiredAtCompletion=true");
System.out.println("durableExpiredLease.succeedUpdatedRows=" + updated);
System.out.println("durableExpiredLease.finalState=" + state("op-expired"));
assertThat(updated).isOne();
}
private int succeedAt(String operationId, String workerId, String resultReference, Instant completedAt)
throws SQLException {
try (Connection connection = support.connection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE durable_operation SET state = 'SUCCEEDED', result_reference = ?, completed_at = ?, "
+ "lease_owner = NULL, lease_acquired_at = NULL, lease_expires_at = NULL, "
+ "row_version = row_version + 1 "
+ "WHERE operation_id = ? AND state = 'RUNNING' AND lease_owner = ?")) {
statement.setString(1, resultReference);
statement.setTimestamp(2, Timestamp.from(completedAt));
statement.setString(3, operationId);
statement.setString(4, workerId);
return statement.executeUpdate();
}
}
private int succeed(String operationId, String workerId, String resultReference)
throws SQLException {
try (Connection connection = support.connection();
PreparedStatement statement =
connection.prepareStatement(
"""
UPDATE durable_operation
SET state = 'SUCCEEDED', result_reference = ?, completed_at = ?,
lease_owner = NULL, lease_acquired_at = NULL, lease_expires_at = NULL,
row_version = row_version + 1
WHERE operation_id = ? AND state = 'RUNNING' AND lease_owner = ?
""")) {
statement.setString(1, resultReference);
statement.setTimestamp(2, Timestamp.from(NOW.plusSeconds(10)));
statement.setString(3, operationId);
statement.setString(4, workerId);
return statement.executeUpdate();
}
}
private int heartbeat(String operationId, String workerId, Instant now) throws SQLException {
try (Connection connection = support.connection();
PreparedStatement statement =
connection.prepareStatement(
"""
UPDATE durable_operation
SET lease_expires_at = ?, row_version = row_version + 1
WHERE operation_id = ? AND state = 'RUNNING' AND lease_owner = ?
AND lease_expires_at > ?
""")) {
statement.setTimestamp(1, Timestamp.from(now.plus(LEASE)));
statement.setString(2, operationId);
statement.setString(3, workerId);
statement.setTimestamp(4, Timestamp.from(now));
return statement.executeUpdate();
}
}
private int cancel(String operationId, String principal) throws SQLException {
try (Connection connection = support.connection();
PreparedStatement statement =
connection.prepareStatement(
"""
UPDATE durable_operation
SET state = 'CANCELED', completed_at = ?, lease_owner = NULL,
lease_acquired_at = NULL, lease_expires_at = NULL,
row_version = row_version + 1
WHERE operation_id = ? AND principal = ? AND state IN ('PENDING', 'RUNNING')
""")) {
statement.setTimestamp(1, Timestamp.from(NOW.plusSeconds(5)));
statement.setString(2, operationId);
statement.setString(3, principal);
return statement.executeUpdate();
}
}
private List<String> reclaim(Instant now) throws SQLException {
return idsFrom(
"""
UPDATE durable_operation
SET state = 'PENDING', lease_owner = NULL, lease_acquired_at = NULL,
lease_expires_at = NULL, row_version = row_version + 1
WHERE state = 'RUNNING' AND lease_expires_at <= ?
RETURNING operation_id
""",
now);
}
private List<String> expireStale(Instant now) throws SQLException {
return idsFrom(
"""
UPDATE durable_operation
SET state = 'EXPIRED', completed_at = ?, lease_owner = NULL,
lease_acquired_at = NULL, lease_expires_at = NULL,
row_version = row_version + 1
WHERE state IN ('PENDING', 'RUNNING') AND expires_at <= ?
RETURNING operation_id
""",
now,
now);
}
private List<String> idsFrom(String sql, Instant... arguments) throws SQLException {
try (Connection connection = support.connection();
PreparedStatement statement = connection.prepareStatement(sql)) {
for (int index = 0; index < arguments.length; index++) {
statement.setTimestamp(index + 1, Timestamp.from(arguments[index]));
}
List<String> ids = new ArrayList<>();
try (ResultSet rows = statement.executeQuery()) {
while (rows.next()) {
ids.add(rows.getString(1));
}
}
return ids;
}
}
private String state(String operationId) throws SQLException {
return column(operationId, "state");
}
private String leaseOwner(String operationId) throws SQLException {
return column(operationId, "lease_owner");
}
private String resultReference(String operationId) throws SQLException {
return column(operationId, "result_reference");
}
private Instant startedAt(String operationId) throws SQLException {
try (Connection connection = support.connection();
PreparedStatement statement =
connection.prepareStatement(
"SELECT started_at FROM durable_operation WHERE operation_id = ?")) {
statement.setString(1, operationId);
try (ResultSet rows = statement.executeQuery()) {
rows.next();
return rows.getTimestamp(1).toInstant();
}
}
}
private String column(String operationId, String name) throws SQLException {
try (Connection connection = support.connection();
PreparedStatement statement =
connection.prepareStatement(
"SELECT " + name + " FROM durable_operation WHERE operation_id = ?")) {
statement.setString(1, operationId);
try (ResultSet rows = statement.executeQuery()) {
rows.next();
return rows.getString(1);
}
}
}
private void execute(String sql) throws SQLException {
try (Connection connection = support.connection();
Statement statement = connection.createStatement()) {
statement.execute(sql);
}
}
private static String migration() throws IOException {
try (InputStream resource =
DurableOperationStoreContractTest.class
.getClassLoader()
.getResourceAsStream("db/migration/postgresql/V11__durable_operation.sql")) {
if (resource == null) {
throw new IOException("the durable operation migration is missing from the classpath");
}
return new String(resource.readAllBytes(), StandardCharsets.UTF_8);
}
}
}
@@ -0,0 +1,41 @@
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
command=./gradlew :adapter:outbound:persistence-jpa:test --tests dev.caskeleton.adapter.outbound.persistence.outbox.OutboxStoreAdapterTest.analysisProbeStaleWorkerCanRegressPublishedRowToFailed --rerun-tasks --console=plain --no-daemon
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
Daemon will be stopped at the end of the build
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :shared-contract:compileJava
> Task :shared-contract:processResources
> Task :shared-contract:classes
> Task :shared-contract:jar
> Task :application-core:compileJava
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes
> Task :application-core:jar
> Task :adapter:outbound:persistence-jpa:compileJava
> Task :adapter:outbound:persistence-jpa:processResources
> Task :adapter:outbound:persistence-jpa:classes
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses
> Task :adapter:outbound:persistence-jpa:compileTestJava
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testClasses
> Task :adapter:outbound:persistence-jpa:test
BUILD SUCCESSFUL in 37s
18 actionable tasks: 18 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
=== ASSERTED OBSERVATION (from Gradle test XML) ===
outboxStaleWorker.before=PUBLISHED
outboxStaleWorker.after=FAILED
outboxStaleWorker.retryAt=2026-06-11T10:00:30Z
source restored after probe: yes
@@ -0,0 +1,300 @@
package dev.caskeleton.adapter.outbound.persistence.outbox;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentCaptor.forClass;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import dev.caskeleton.adapter.outbound.persistence.outbox.entity.OutboxEventEntity;
import dev.caskeleton.application.outbox.NewOutboxEvent;
import dev.caskeleton.application.outbox.OutboxEvent;
import dev.caskeleton.application.outbox.OutboxEventStatus;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
/**
* Unit tests for {@link OutboxStoreAdapter} — covers append mapping, claim transition, mark
* operations, countByStatus, and oldestUnpublishedAgeSecondsByEventType.
*
* <h2>FIFO gate location contract</h2>
*
* <p>The per-aggregate FIFO gate (I4) lives entirely in the {@link
* OutboxClaimRepository#claimEligible} native SQL query via a {@code NOT EXISTS} correlated
* subquery. The adapter itself performs NO in-memory filtering: every row returned by the claim
* repository passes through to the caller. Verifying that the SQL gate enforces FIFO is the
* responsibility of the Testcontainers contract test in {@code app-bootstrap}.
*
* <p>Real SKIP LOCKED / FIFO gate behaviour is covered by the Task E PG contract tests.
*/
class OutboxStoreAdapterTest {
private static final Instant NOW = Instant.parse("2026-06-11T10:00:00Z");
private static final Duration IN_FLIGHT_TIMEOUT = Duration.ofMinutes(5);
private final OutboxEventJpaRepository repo = mock(OutboxEventJpaRepository.class);
private final OutboxClaimRepository claimRepo = mock(OutboxClaimRepository.class);
private final OutboxStoreAdapter adapter = new OutboxStoreAdapter(repo, claimRepo);
// ---- helper ----
private static NewOutboxEvent newEvent(String eventId) {
return new NewOutboxEvent(eventId, "UserCreated", "agg-1", "{}", NOW, "corr-1", "idem-1");
}
private static OutboxEventEntity pendingEntity(String eventId) {
OutboxEventEntity e = new OutboxEventEntity();
e.setEventId(eventId);
e.setAggregateId("agg-1");
e.setEventType("UserCreated");
e.setPayload("{}");
e.setOccurredAt(NOW.minusSeconds(60));
e.setStatus("PENDING");
e.setAttemptCount(0);
e.setNextAttemptAt(NOW.minusSeconds(60));
e.setCorrelationId("corr-1");
e.setIdempotencyKey("idem-1");
return e;
}
// ---- append ----
@Test
void appendPersistsEntityWithCorrectFieldMapping() {
NewOutboxEvent event = newEvent("evt-001");
adapter.append(event);
ArgumentCaptor<OutboxEventEntity> saved = forClass(OutboxEventEntity.class);
verify(repo).save(saved.capture());
OutboxEventEntity entity = saved.getValue();
assertThat(entity.getEventId()).isEqualTo("evt-001");
assertThat(entity.getEventType()).isEqualTo("UserCreated");
assertThat(entity.getAggregateId()).isEqualTo("agg-1");
assertThat(entity.getPayload()).isEqualTo("{}");
assertThat(entity.getOccurredAt()).isEqualTo(NOW);
assertThat(entity.getStatus()).isEqualTo("PENDING");
assertThat(entity.getAttemptCount()).isEqualTo(0);
assertThat(entity.getNextAttemptAt()).isEqualTo(NOW); // nextAttemptAt = occurredAt on PENDING
assertThat(entity.getCorrelationId()).isEqualTo("corr-1");
assertThat(entity.getIdempotencyKey()).isEqualTo("idem-1");
}
// ---- claimBatch ----
@Test
void claimBatchTransitionsEntityToInFlightAndReturnsMappedEvent() {
OutboxEventEntity entity = pendingEntity("evt-002");
when(claimRepo.claimEligible(eq(NOW), eq(2))).thenReturn(List.of(entity));
List<OutboxEvent> claimed = adapter.claimBatch(2, NOW, IN_FLIGHT_TIMEOUT);
// status and attemptCount updated on entity
assertThat(entity.getStatus()).isEqualTo("IN_FLIGHT");
assertThat(entity.getAttemptCount()).isEqualTo(1);
assertThat(entity.getNextAttemptAt()).isEqualTo(NOW.plus(IN_FLIGHT_TIMEOUT));
// returned OutboxEvent reflects post-transition state
assertThat(claimed).hasSize(1);
OutboxEvent result = claimed.get(0);
assertThat(result.eventId()).isEqualTo("evt-002");
assertThat(result.status()).isEqualTo(OutboxEventStatus.IN_FLIGHT);
assertThat(result.attemptCount()).isEqualTo(1);
}
@Test
void claimBatchReturnsEmptyWhenRepoReturnsNothing() {
when(claimRepo.claimEligible(any(), anyInt())).thenReturn(List.of());
assertThat(adapter.claimBatch(5, NOW, IN_FLIGHT_TIMEOUT)).isEmpty();
}
/**
* Regression guard: the per-aggregate FIFO gate lives in the SQL query (I4). The adapter must NOT
* apply any in-memory filtering — all rows returned by the repository must appear in the result,
* including multiple rows for the same aggregate.
*
* <p>If this test fails it means in-memory filtering was re-introduced in the adapter; the fix is
* to move that logic back to the {@code claimEligible} SQL query.
*/
@Test
void claimBatchPassesAllRepoResultsThroughWithoutInMemoryFifoFiltering() {
// Two rows for the same aggregate — repo returns both (SQL gate already enforces FIFO)
OutboxEventEntity head = pendingEntity("evt-head");
OutboxEventEntity tail = new OutboxEventEntity();
tail.setEventId("evt-tail");
tail.setAggregateId("agg-1"); // same aggregate as head
tail.setEventType("UserCreated");
tail.setPayload("{}");
tail.setOccurredAt(NOW.minusSeconds(30)); // later than head (head is at -60s)
tail.setStatus("PENDING");
tail.setAttemptCount(0);
tail.setNextAttemptAt(NOW.minusSeconds(30));
tail.setCorrelationId("corr-2");
tail.setIdempotencyKey("idem-2");
when(claimRepo.claimEligible(eq(NOW), eq(10))).thenReturn(List.of(head, tail));
List<OutboxEvent> claimed = adapter.claimBatch(10, NOW, IN_FLIGHT_TIMEOUT);
// Adapter must return both rows — no in-memory FIFO filtering
assertThat(claimed)
.hasSize(2)
.extracting(OutboxEvent::eventId)
.containsExactly("evt-head", "evt-tail");
}
// ---- markPublished ----
@Test
void markPublishedUpdatesStatusToPublished() {
OutboxEventEntity entity = pendingEntity("evt-003");
entity.setStatus("IN_FLIGHT");
when(repo.findById("evt-003")).thenReturn(Optional.of(entity));
adapter.markPublished("evt-003");
assertThat(entity.getStatus()).isEqualTo("PUBLISHED");
}
// ---- markFailed ----
@Test
void markFailedSetsFailedStatusAndNextAttemptAt() {
OutboxEventEntity entity = pendingEntity("evt-004");
entity.setStatus("IN_FLIGHT");
when(repo.findById("evt-004")).thenReturn(Optional.of(entity));
Instant retryAt = NOW.plus(Duration.ofSeconds(30));
adapter.markFailed("evt-004", retryAt);
assertThat(entity.getStatus()).isEqualTo("FAILED");
assertThat(entity.getNextAttemptAt()).isEqualTo(retryAt);
}
// ---- markDead ----
@Test
void markDeadSetsDeadStatus() {
OutboxEventEntity entity = pendingEntity("evt-005");
entity.setStatus("IN_FLIGHT");
when(repo.findById("evt-005")).thenReturn(Optional.of(entity));
adapter.markDead("evt-005");
assertThat(entity.getStatus()).isEqualTo("DEAD");
}
// ---- countByStatus ----
@Test
void countByStatusMapsProjectionResultsToStatusEnumMap() {
List<Object[]> rows =
List.of(
new Object[] {"PENDING", 5L},
new Object[] {"IN_FLIGHT", 2L},
new Object[] {"PUBLISHED", 10L});
when(repo.countGroupedByStatus()).thenReturn(rows);
Map<OutboxEventStatus, Long> counts = adapter.countByStatus();
assertThat(counts).containsEntry(OutboxEventStatus.PENDING, 5L);
assertThat(counts).containsEntry(OutboxEventStatus.IN_FLIGHT, 2L);
assertThat(counts).containsEntry(OutboxEventStatus.PUBLISHED, 10L);
assertThat(counts).doesNotContainKey(OutboxEventStatus.FAILED);
assertThat(counts).doesNotContainKey(OutboxEventStatus.DEAD);
}
// ---- oldestUnpublishedAgeSecondsByEventType ----
@Test
void oldestUnpublishedAgeComputesSecondsFromOldestRowOccurredAt() {
// oldest row for "UserCreated" occurred 120 seconds before NOW
Instant oldestAt = NOW.minusSeconds(120);
List<Object[]> rows = List.<Object[]>of(new Object[] {"UserCreated", oldestAt});
when(repo.findOldestUnpublishedOccurredAtByEventType()).thenReturn(rows);
Map<String, Long> ages = adapter.oldestUnpublishedAgeSecondsByEventType(NOW);
assertThat(ages).containsEntry("UserCreated", 120L);
}
@Test
void oldestUnpublishedAgeReturnsEmptyMapWhenNoUnpublishedRows() {
when(repo.findOldestUnpublishedOccurredAtByEventType()).thenReturn(List.of());
assertThat(adapter.oldestUnpublishedAgeSecondsByEventType(NOW)).isEmpty();
}
// ---- missing-entity guard (fail-closed consistency) ----
/**
* When the outbox row is not found during markPublished, the adapter must throw {@link
* IllegalStateException} with the eventId in the message instead of silently doing nothing. A
* silent no-op leaves the row IN_FLIGHT forever, blocking the aggregate's FIFO queue with no
* error observable by the relay.
*/
@Test
void markPublishedThrowsWhenEventIdNotFound() {
when(repo.findById("missing-evt")).thenReturn(Optional.empty());
assertThatThrownBy(() -> adapter.markPublished("missing-evt"))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("missing-evt");
}
/**
* When the outbox row is not found during markFailed, the adapter must throw {@link
* IllegalStateException} with the eventId in the message. A silent no-op would leave the relay
* believing a retry is scheduled while the row remains IN_FLIGHT, blocking the aggregate queue
* and producing no error log.
*/
@Test
void markFailedThrowsWhenEventIdNotFound() {
when(repo.findById("missing-evt")).thenReturn(Optional.empty());
Instant retryAt = NOW.plus(Duration.ofSeconds(30));
assertThatThrownBy(() -> adapter.markFailed("missing-evt", retryAt))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("missing-evt");
}
/**
* When the outbox row is not found during markDead, the adapter must throw {@link
* IllegalStateException} with the eventId in the message. A silent no-op would leave the row
* IN_FLIGHT instead of DEAD, preventing runbook-level visibility and manual resolution.
*/
@Test
void markDeadThrowsWhenEventIdNotFound() {
when(repo.findById("missing-evt")).thenReturn(Optional.empty());
assertThatThrownBy(() -> adapter.markDead("missing-evt"))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("missing-evt");
}
@Test
void analysisProbeStaleWorkerCanRegressPublishedRowToFailed() {
OutboxEventEntity entity = pendingEntity("evt-stale");
entity.setStatus("PUBLISHED");
when(repo.findById("evt-stale")).thenReturn(Optional.of(entity));
Instant retryAt = NOW.plus(Duration.ofSeconds(30));
adapter.markFailed("evt-stale", retryAt);
System.out.println("outboxStaleWorker.before=PUBLISHED");
System.out.println("outboxStaleWorker.after=" + entity.getStatus());
System.out.println("outboxStaleWorker.retryAt=" + entity.getNextAttemptAt());
assertThat(entity.getStatus()).isEqualTo("FAILED");
}
}
@@ -0,0 +1,31 @@
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :shared-contract:compileJava
> Task :shared-contract:processResources
> Task :shared-contract:classes
> Task :shared-contract:jar
> Task :application-core:compileJava
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes
> Task :application-core:jar
> Task :adapter:outbound:persistence-jpa:compileJava
> Task :adapter:outbound:persistence-jpa:processResources
> Task :adapter:outbound:persistence-jpa:classes
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses
> Task :adapter:outbound:persistence-jpa:compileTestJava
> Task :adapter:outbound:persistence-jpa:processTestResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testClasses
> Task :adapter:outbound:persistence-jpa:test
BUILD SUCCESSFUL in 38s
18 actionable tasks: 18 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
@@ -0,0 +1,37 @@
Sub-scope 07 Fileserver persistence manifest
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
generatedAt=2026-08-29T11:59:28Z
production=25 tests=0 resources=4 total=29
=== PRODUCTION ===
ad874e26e83fce74dc666dfc7278570ef8f5840f src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/FileEntityMapper.java
4508a945e2f6339371ae414143428826c96701f6 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/FileserverJpaPersistenceConfig.java
136bd818df67c33998c87f7c317745154e0b6516 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/FileserverSchemaActivation.java
019fb271540fab8d62b3b2efb372a71158605e82 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaCleanupQueue.java
a07bcb2fae88ff4cf924c195be0db9aa682c2ea0 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaContentReferenceLedger.java
980aa08cbabcb7104f5dac7f0f1185a6456d1562 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaFileMetadataStore.java
8309ecb7e2affa32a299ce355a3fda95cde6da3f src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaFileQuotaService.java
764dbe19792cf44f31ca8807329b056747799a40 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaQuotaCommitGateway.java
59ed25ded31efec4b491bc2bdb2c800c41879823 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaQuotaReclaimGateway.java
12c252024161f166854c7229ce6136e0720e7106 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaRecoveryQueue.java
56b8424d4c0456fdd7415cd10f8ac85a1ccaeda5 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaStagingUploadLocator.java
4639a54e5bed3ea0117df2c51f939e34d1e84126 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaUploadSessionStore.java
7278c4707be3b9a214b1fc644b727fb49eeaf991 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/CleanupItemEntity.java
5d285dd9d192ee6ac63abc6872f8d0ff0c6addb5 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/FileEntity.java
aade605e847a7d38e08c26e418de7765760a5f41 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/QuotaReservationEntity.java
83b8324709117a6f7b667f22286e26f72c59e5fc src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/RecoveryItemEntity.java
26f6be44cd09220b02b3299fe0e3ca0bc6149be9 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/UploadSessionEntity.java
be32b59a228ee3eb72ff5f205dfbd3cdd7e7aca0 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/VerificationResultEntity.java
60abdfd28809d998da203415392f3a4c06ed3029 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/FileTransitionRepository.java
e4ff72f4effe8104c8d0f896ab4e099357eebdd6 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/FileserverCleanupRepository.java
cac64f32e6548d25a4e30fa1f18daa1b7a8774fe src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/FileserverQuotaRepository.java
fcb7475ac206e8fa9de296f9ee0f3f117ed70707 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/FileserverRecoveryRepository.java
46270df09eab0003f8223a67287c9d1ee507e342 src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/JpaFileRepository.java
61f265216b6aa0382aa090c8fe270709c27f289f src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/JpaUploadSessionRepository.java
ac9bdec31a8899ecea1e6e166e560fb47125843a src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/UploadLeaseRepository.java
=== RESOURCE ===
414f9ce715410e111d36794f4fa435ddee7c2b1f src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V1__create_fileserver_metadata.sql
ae76061212a359ca569927e5d5ed61c35c85a1df src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V2__fileserver_recovery_and_staging_cleanup.sql
f98200909e0045b71a644d458c71742c9c71f928 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V3__fileserver_fenced_cleanup_lease.sql
d928c244ca0767498099221c43e68680fbc2ed27 src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V4__fileserver_upload_terminal_state.sql
@@ -0,0 +1,43 @@
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
command=./gradlew :adapter:outbound:persistence-jpa:postgresqlFileserverReclamationIntegrationTest --rerun-tasks --console=plain --no-daemon
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
Daemon will be stopped at the end of the build
> Task :build-logic:extractPluginRequests
> Task :build-logic:generatePluginAdapters
> Task :build-logic:compileJava
> Task :build-logic:compileGroovy
> Task :build-logic:compileGroovyPlugins
> Task :build-logic:pluginDescriptors
> Task :build-logic:processResources
> Task :build-logic:classes
> Task :build-logic:jar
> Task :shared-contract:compileJava
> Task :shared-contract:processResources
> Task :shared-contract:classes
> Task :shared-contract:jar
> Task :application-core:compileJava
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes
> Task :application-core:jar
> Task :adapter:outbound:persistence-jpa:compileJava
> Task :adapter:outbound:persistence-jpa:processResources
> Task :adapter:outbound:persistence-jpa:classes
> Task :adapter:outbound:persistence-jpa:compileTestkitJava
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses
> Task :adapter:outbound:persistence-jpa:postgresqlFileserverReclamationIntegrationTest
BUILD SUCCESSFUL in 45s
19 actionable tasks: 19 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
=== ASSERTED OBSERVATION (from Gradle test XML) ===
fileserverExpiredQuota.status=COMMITTED
fileserverExpiredQuota.committedBytes=600
fileserverQuotaReclaim.before=65
fileserverQuotaReclaim.requested=65
fileserverQuotaReclaim.after=1
source restored after probe: yes
@@ -0,0 +1,549 @@
package dev.caskeleton.adapter.outbound.persistence.readiness;
import static org.assertj.core.api.Assertions.assertThat;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaCleanupQueue;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaContentReferenceLedger;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaFileMetadataStore;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaFileQuotaService;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaQuotaCommitGateway;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaQuotaReclaimGateway;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaRecoveryQueue;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaStagingUploadLocator;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaUploadSessionStore;
import dev.caskeleton.application.fileserver.api.ContentKey;
import dev.caskeleton.application.fileserver.api.FileId;
import dev.caskeleton.application.fileserver.api.FileState;
import dev.caskeleton.application.fileserver.api.StorageNamespace;
import dev.caskeleton.application.fileserver.api.UploadId;
import dev.caskeleton.application.fileserver.api.metadata.FileRecord;
import dev.caskeleton.application.fileserver.api.metadata.FileRecordDraft;
import dev.caskeleton.application.fileserver.api.metadata.FileRecordMutation;
import dev.caskeleton.application.fileserver.api.metadata.QuotaScope;
import dev.caskeleton.application.fileserver.api.metadata.UploadSession;
import dev.caskeleton.application.fileserver.api.metadata.UploadSessionDraft;
import dev.caskeleton.application.fileserver.api.transfer.UploadProtocol;
import dev.caskeleton.application.fileserver.cleanup.CleanupItem;
import dev.caskeleton.application.fileserver.cleanup.CleanupRequest;
import dev.caskeleton.application.fileserver.cleanup.CleanupType;
import dev.caskeleton.application.fileserver.recovery.ReconciliationStatus;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.UUID;
import java.util.function.Supplier;
import org.flywaydb.core.Flyway;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.support.TransactionTemplate;
/**
* Real-PostgreSQL proof of the reclamation side of the Fileserver.
*
* <p>The cleanup queue, the recovery queue, and the quota ledger are the components that decide
* when physical bytes may be destroyed and how much space a tenant is charged for. All three are
* conditional-update designs whose correctness lives in SQL, so an in-memory fake would prove
* nothing about them: the partial unique index, the {@code IN ('PENDING','FAILED')} claim guard,
* and the {@code committedBytes >= :amount} floor only exist in the database.
*/
class PostgreSqlFileserverReclamationIntegrationTest {
private static PostgreSqlReadinessSupport postgres;
private static FileserverJpaTestContext context;
private static JdbcTemplate jdbc;
private static TransactionTemplate transactions;
private static JpaFileMetadataStore files;
private static JpaUploadSessionStore uploads;
private static JpaFileQuotaService quota;
private static JpaCleanupQueue cleanupQueue;
private static JpaRecoveryQueue recoveryQueue;
private static JpaQuotaCommitGateway quotaCommit;
private static JpaQuotaReclaimGateway quotaReclaim;
private static JpaContentReferenceLedger ledger;
private static JpaStagingUploadLocator stagingLocator;
@BeforeAll
static void startAndMigratePostgreSql() {
PostgreSqlReadinessSupport.assertDockerAvailable();
postgres = PostgreSqlReadinessSupport.start(8, 2_000);
migrate("classpath:db/migration/postgresql", "flyway_schema_history");
migrateIndependent(
"classpath:db/migration/jpa/core", "flyway_jpa_core_history", "explicit-jpa-core-adoption");
migrateIndependent(
"classpath:db/migration/jpa/fileserver",
"flyway_jpa_fileserver_history",
"explicit-jpa-fileserver-adoption");
jdbc = new JdbcTemplate(postgres.dataSource());
context = new FileserverJpaTestContext(postgres.dataSource(), Clock.systemUTC());
transactions = context.transactions();
files = context.bean(JpaFileMetadataStore.class);
uploads = context.bean(JpaUploadSessionStore.class);
quota = context.bean(JpaFileQuotaService.class);
cleanupQueue = context.bean(JpaCleanupQueue.class);
recoveryQueue = context.bean(JpaRecoveryQueue.class);
quotaCommit = context.bean(JpaQuotaCommitGateway.class);
quotaReclaim = context.bean(JpaQuotaReclaimGateway.class);
ledger = context.bean(JpaContentReferenceLedger.class);
stagingLocator = context.bean(JpaStagingUploadLocator.class);
}
@AfterAll
static void stopPostgreSql() {
if (context != null) {
context.close();
}
if (postgres != null) {
postgres.close();
}
}
@BeforeEach
void truncate() {
jdbc.execute(
"TRUNCATE fs_recovery_item, fs_cleanup_item, fs_quota_reservation,"
+ " fs_verification_result, fs_upload_session, fs_file CASCADE");
}
@Test
void aStagingCleanupSurvivesTheRoundTripWithItsUploadIdentity() {
FileRecord record = insertRecord();
UploadId uploadId = UploadId.of(UUID.randomUUID());
inTransaction(
() -> {
cleanupQueue.enqueue(
CleanupRequest.forStaging(CleanupType.CANCELLED_STAGING, record.fileId(), uploadId));
return null;
});
List<CleanupItem> claimed = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10));
assertThat(claimed)
.singleElement()
.satisfies(
item -> {
assertThat(item.request().uploadId()).contains(uploadId);
assertThat(item.request().contentKey()).isEmpty();
assertThat(item.type()).isEqualTo(CleanupType.CANCELLED_STAGING);
});
}
@Test
void aClaimedItemIsNotHandedToASecondWorker() {
FileRecord record = insertRecord();
inTransaction(
() -> {
cleanupQueue.enqueue(
CleanupRequest.forContent(
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
return null;
});
List<CleanupItem> first = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10));
List<CleanupItem> second = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10));
assertThat(first).hasSize(1);
assertThat(second).isEmpty();
}
@Test
void aFailedItemBecomesDueAgainOnlyAfterItsBackoff() {
FileRecord record = insertRecord();
inTransaction(
() -> {
cleanupQueue.enqueue(
CleanupRequest.forContent(
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
return null;
});
CleanupItem item = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10)).get(0);
Instant retryAt = Instant.now().plus(Duration.ofMinutes(5));
inTransaction(
() -> {
cleanupQueue.markFailed(item, "STORAGE_UNAVAILABLE", retryAt);
return null;
});
assertThat(inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10))).isEmpty();
assertThat(inTransaction(() -> cleanupQueue.claimDue(retryAt.plusSeconds(1), 10))).hasSize(1);
}
@Test
void anItemThatKeepsFailingIsAbandonedRatherThanRetriedForever() {
FileRecord record = insertRecord();
inTransaction(
() -> {
cleanupQueue.enqueue(
CleanupRequest.forContent(
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
return null;
});
Instant due = Instant.now();
for (int attempt = 0; attempt < JpaCleanupQueue.MAXIMUM_ATTEMPTS; attempt++) {
List<CleanupItem> claimed = inTransaction(() -> cleanupQueue.claimDue(due, 10));
if (claimed.isEmpty()) {
break;
}
inTransaction(
() -> {
cleanupQueue.markFailed(claimed.get(0), "STILL_FAILING", due);
return null;
});
}
assertThat(inTransaction(() -> cleanupQueue.claimDue(due.plusSeconds(1), 10))).isEmpty();
assertThat(statusCounts("fs_cleanup_item", "ABANDONED")).isEqualTo(1);
}
@Test
void aDoneItemIsNeverClaimedAgain() {
FileRecord record = insertRecord();
inTransaction(
() -> {
cleanupQueue.enqueue(
CleanupRequest.forContent(
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
return null;
});
CleanupItem item = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10)).get(0);
inTransaction(
() -> {
cleanupQueue.markDone(item);
return null;
});
assertThat(inTransaction(() -> cleanupQueue.claimDue(Instant.now().plusSeconds(600), 10)))
.isEmpty();
}
@Test
void thesameFileReportedTwiceHoldsOneOpenRecoveryItem() {
FileRecord record = insertRecord();
inTransaction(
() -> {
recoveryQueue.enqueue(record.fileId(), "READY_DIGEST_MISMATCH");
recoveryQueue.enqueue(record.fileId(), "READY_SIZE_MISMATCH");
return null;
});
assertThat(inTransaction(() -> recoveryQueue.pending(10))).containsExactly(record.fileId());
assertThat(rowCount("fs_recovery_item")).isEqualTo(1);
assertThat(jdbc.queryForObject("SELECT reason_code FROM fs_recovery_item", String.class))
.isEqualTo("READY_SIZE_MISMATCH");
}
@Test
void aResolvedRecoveryItemLeavesThePendingListButKeepsItsOutcome() {
FileRecord record = insertRecord();
inTransaction(
() -> {
recoveryQueue.enqueue(record.fileId(), "PUBLISH_EVIDENCE_INCOMPLETE");
return null;
});
inTransaction(
() -> {
recoveryQueue.resolve(record.fileId(), ReconciliationStatus.QUARANTINE_REQUIRED);
return null;
});
assertThat(inTransaction(() -> recoveryQueue.pending(10))).isEmpty();
assertThat(statusCounts("fs_recovery_item", "QUARANTINE_REQUIRED")).isEqualTo(1);
}
@Test
void aResolvedFileCanBeRaisedAgainLater() {
FileRecord record = insertRecord();
inTransaction(
() -> {
recoveryQueue.enqueue(record.fileId(), "FIRST");
recoveryQueue.resolve(record.fileId(), ReconciliationStatus.UNRESOLVED);
recoveryQueue.enqueue(record.fileId(), "SECOND");
return null;
});
assertThat(inTransaction(() -> recoveryQueue.pending(10))).containsExactly(record.fileId());
assertThat(rowCount("fs_recovery_item")).isEqualTo(2);
}
@Test
void committingAnUploadMovesReservedBytesToCommitted() {
FileRecord record = insertRecord();
UploadSession session = insertSession(record.fileId());
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
inTransaction(() -> quota.reserve(scope, 1_000, Duration.ofHours(1)));
inTransaction(
() -> {
quotaCommit.commit(session, 600);
return null;
});
assertThat(inTransaction(() -> quota.reservedBytes(scope))).isZero();
assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(600);
}
@Test
void committingWithoutALiveReservationStillRecordsTheDurableUsage() {
FileRecord record = insertRecord();
UploadSession session = insertSession(record.fileId());
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
inTransaction(
() -> {
quotaCommit.commit(session, 450);
return null;
});
assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(450);
}
@Test
void releasingAnUploadGivesTheReservedCapacityBack() {
FileRecord record = insertRecord();
UploadSession session = insertSession(record.fileId());
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
inTransaction(() -> quota.reserve(scope, 2_000, Duration.ofHours(1)));
inTransaction(
() -> {
quotaCommit.release(session);
return null;
});
assertThat(inTransaction(() -> quota.reservedBytes(scope))).isZero();
assertThat(inTransaction(() -> quota.committedBytes(scope))).isZero();
}
@Test
void reclaimingDrawsCommittedBytesDownAcrossRows() {
FileRecord record = insertRecord();
UploadSession session = insertSession(record.fileId());
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
inTransaction(
() -> {
quotaCommit.commit(session, 300);
quotaCommit.commit(session, 700);
return null;
});
inTransaction(
() -> {
quotaReclaim.reclaim(scope, 800);
return null;
});
assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(200);
}
@Test
void reclaimingMoreThanIsRecordedStopsAtZeroRatherThanGoingNegative() {
FileRecord record = insertRecord();
UploadSession session = insertSession(record.fileId());
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
inTransaction(
() -> {
quotaCommit.commit(session, 100);
return null;
});
inTransaction(
() -> {
quotaReclaim.reclaim(scope, 5_000);
return null;
});
assertThat(inTransaction(() -> quota.committedBytes(scope))).isZero();
}
@Test
void theLedgerReportsAKeyAsReferencedOnlyWhileARecordNamesIt() {
ContentKey key = contentKey();
assertThat(inTransaction(() -> ledger.isReferenced(key))).isFalse();
FileRecord record = insertRecord();
inTransaction(() -> publish(record, key));
assertThat(inTransaction(() -> ledger.isReferenced(key))).isTrue();
}
@Test
void theStagingLocatorReturnsTheNewestSessionForAFile() {
FileRecord record = insertRecord();
UploadSession older = insertSession(record.fileId());
UploadSession newer = insertSession(record.fileId());
Optional<UploadId> located = inTransaction(() -> stagingLocator.locate(record.fileId()));
assertThat(located).isPresent();
assertThat(located.get()).isIn(older.uploadId(), newer.uploadId());
assertThat(inTransaction(() -> stagingLocator.locate(FileId.of(UUID.randomUUID())))).isEmpty();
}
private FileRecord insertRecord() {
return inTransaction(
() ->
files.insert(
new FileRecordDraft(
FileId.of(UUID.randomUUID()),
StorageNamespace.of("tenant-a"),
"report.bin",
Optional.of("application/octet-stream"),
OptionalLong.of(1_000))));
}
private FileRecord publish(FileRecord record, ContentKey key) {
FileRecord uploading =
files.transition(
record.fileId(),
record.version(),
FileState.CREATED,
FileState.UPLOADING,
FileRecordMutation.none());
FileRecord uploaded =
files.transition(
uploading.fileId(),
uploading.version(),
FileState.UPLOADING,
FileState.UPLOADED,
FileRecordMutation.uploaded(10, "b".repeat(64)));
FileRecord verifying =
files.transition(
uploaded.fileId(),
uploaded.version(),
FileState.UPLOADED,
FileState.VERIFYING,
FileRecordMutation.none());
return files.transition(
verifying.fileId(),
verifying.version(),
FileState.VERIFYING,
FileState.READY,
FileRecordMutation.publishAt(
key, 10, "b".repeat(64), "\"" + "b".repeat(64) + "\"", Instant.now()));
}
private UploadSession insertSession(FileId fileId) {
return inTransaction(
() ->
uploads.create(
new UploadSessionDraft(
UploadId.of(UUID.randomUUID()),
fileId,
UploadProtocol.RAW,
OptionalLong.of(1_000),
Instant.now().plus(Duration.ofHours(1)))));
}
private static ContentKey contentKey() {
String flat = UUID.randomUUID().toString().replace("-", "");
return ContentKey.of(flat.substring(0, 2) + '/' + flat.substring(2, 4) + '/' + flat);
}
private static <T> T inTransaction(Supplier<T> action) {
return transactions.execute(status -> action.get());
}
private static int rowCount(String table) {
Integer count = jdbc.queryForObject("SELECT count(*) FROM " + table, Integer.class);
return count == null ? 0 : count;
}
private static int statusCounts(String table, String status) {
Integer count =
jdbc.queryForObject(
"SELECT count(*) FROM " + table + " WHERE status = ?", Integer.class, status);
return count == null ? 0 : count;
}
private static void migrate(String location, String historyTable) {
Flyway.configure()
.dataSource(postgres.dataSource())
.locations(location)
.table(historyTable)
.baselineOnMigrate(false)
.outOfOrder(false)
.load()
.migrate();
}
private static void migrateIndependent(
String location, String historyTable, String baselineDescription) {
Flyway flyway =
Flyway.configure()
.dataSource(postgres.dataSource())
.locations(location)
.table(historyTable)
.baselineVersion("0")
.baselineDescription(baselineDescription)
.baselineOnMigrate(false)
.outOfOrder(false)
.load();
flyway.baseline();
flyway.migrate();
}
@Test
void analysisProbeReclaimStopsAfterSixtyFourCommittedRows() {
FileRecord record = insertRecord();
UploadSession session = insertSession(record.fileId());
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
for (int i = 0; i < 65; i++) {
inTransaction(
() -> {
quotaCommit.commit(session, 1);
return null;
});
}
long before = inTransaction(() -> quota.committedBytes(scope));
inTransaction(
() -> {
quotaReclaim.reclaim(scope, 65);
return null;
});
long after = inTransaction(() -> quota.committedBytes(scope));
System.out.println("fileserverQuotaReclaim.before=" + before);
System.out.println("fileserverQuotaReclaim.requested=65");
System.out.println("fileserverQuotaReclaim.after=" + after);
assertThat(before).isEqualTo(65);
assertThat(after).isEqualTo(1);
}
@Test
void analysisProbeExpiredDirectReservationStillCommits() {
QuotaScope scope = QuotaScope.ofTenant("analysis-expired");
var reservation = inTransaction(() -> quota.reserve(scope, 1000, Duration.ofHours(1)));
jdbc.update(
"update fs_quota_reservation set expires_at = now() - interval '1 minute' where reservation_id = ?",
reservation.reservationId());
inTransaction(
() -> {
quota.commit(reservation, 600);
return null;
});
String status = jdbc.queryForObject(
"select status from fs_quota_reservation where reservation_id = ?",
String.class,
reservation.reservationId());
long committed = inTransaction(() -> quota.committedBytes(scope));
System.out.println("fileserverExpiredQuota.status=" + status);
System.out.println("fileserverExpiredQuota.committedBytes=" + committed);
assertThat(status).isEqualTo("COMMITTED");
assertThat(committed).isEqualTo(600);
}
}
@@ -0,0 +1,40 @@
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
Daemon will be stopped at the end of the build
> Task :build-logic:extractPluginRequests UP-TO-DATE
> Task :build-logic:generatePluginAdapters UP-TO-DATE
> Task :build-logic:compileJava UP-TO-DATE
> Task :build-logic:compileGroovy UP-TO-DATE
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
> Task :build-logic:pluginDescriptors UP-TO-DATE
> Task :build-logic:processResources UP-TO-DATE
> Task :build-logic:classes UP-TO-DATE
> Task :build-logic:jar UP-TO-DATE
> Task :shared-contract:compileJava UP-TO-DATE
> Task :shared-contract:processResources UP-TO-DATE
> Task :shared-contract:classes UP-TO-DATE
> Task :shared-contract:jar UP-TO-DATE
> Task :application-core:compileJava UP-TO-DATE
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes UP-TO-DATE
> Task :application-core:jar UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses
> Task :adapter:outbound:persistence-jpa:postgresqlFileserverReclamationIntegrationTest
BUILD SUCCESSFUL in 18s
19 actionable tasks: 2 executed, 17 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
=== SYSTEM-OUT ===
fileserverCleanupCrash.maxAttempts=8
fileserverCleanupCrash.actualAttempt=9
fileserverCleanupCrash.status=FAILED
fileserverCleanupCrash.lastError=CLAIM_LEASE_EXPIRED
exitCode=0
@@ -0,0 +1,723 @@
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
=== production quota aggregate readers ===
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaFileQuotaService.java:85: return reservations.sumReservedBytes(scope.type(), scope.value(), clock.instant());
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaFileQuotaService.java:90: return reservations.sumCommittedBytes(scope.type(), scope.value());
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/FileserverQuotaRepository.java:75: long sumReservedBytes(
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/FileserverQuotaRepository.java:87: long sumCommittedBytes(
=== quota ceiling config/symbol search ===
src/application-core/src/main/java/dev/caskeleton/application/fileserver/api/error/QuotaExceededException.java:3:/** A quota scope reservation, commit, or concurrency limit rejected the request. */
=== DefaultTransferAdmissionController ===
1 package dev.caskeleton.application.fileserver.quota;
2
3 import dev.caskeleton.application.fileserver.api.error.FileTooLargeException;
4 import dev.caskeleton.application.fileserver.api.error.FileserverErrorCode;
5 import dev.caskeleton.application.fileserver.api.error.FileserverFailureContext;
6 import dev.caskeleton.application.fileserver.api.error.QuotaExceededException;
7 import dev.caskeleton.application.fileserver.api.error.StorageFullException;
8 import dev.caskeleton.application.fileserver.api.error.TransferAdmissionRejectedException;
9 import dev.caskeleton.application.fileserver.api.metadata.QuotaScope;
10 import java.util.Map;
11 import java.util.OptionalDouble;
12 import java.util.concurrent.ConcurrentHashMap;
13 import java.util.concurrent.Semaphore;
14 import java.util.concurrent.atomic.AtomicBoolean;
15
16 /**
17 * In-process admission control backed by bounded semaphores.
18 *
19 * <p>The failure vocabulary is deliberately distinct so a client can tell a permanent policy denial
20 * from a transient one: an exhausted pool is {@code STORAGE_FULL}, a scope over its ceiling is
21 * {@code QUOTA_EXCEEDED}, and momentary permit exhaustion is a retryable admission rejection.
22 */
23 public final class DefaultTransferAdmissionController implements TransferAdmissionController {
24
25 private final TransferAdmissionProperties properties;
26 private final StorageUsageProbe usageProbe;
27 private final long maximumFileSize;
28 private final Semaphore instanceUploads;
29 private final Semaphore directDownloads;
30 private final Map<String, Semaphore> scopeUploads = new ConcurrentHashMap<>();
31
32 public DefaultTransferAdmissionController(
33 TransferAdmissionProperties properties, StorageUsageProbe usageProbe, long maximumFileSize) {
34 if (maximumFileSize <= 0) {
35 throw new IllegalArgumentException("maximumFileSize must be positive");
36 }
37 this.properties = properties;
38 this.usageProbe = usageProbe;
39 this.maximumFileSize = maximumFileSize;
40 this.instanceUploads = new Semaphore(properties.instanceUploadPermits());
41 this.directDownloads = new Semaphore(properties.directDownloadPermits());
42 }
43
44 @Override
45 public TransferPermit acquireUpload(QuotaScope scope, long requestedBytes) {
46 requireWithinFileSizePolicy(requestedBytes);
47 requireBelowHardHighWater();
48 Semaphore scopePermits = scopePermits(scope);
49 if (!scopePermits.tryAcquire()) {
50 throw new QuotaExceededException(
51 "scope upload concurrency is exhausted",
52 FileserverFailureContext.of(FileserverErrorCode.QUOTA_EXCEEDED, true));
53 }
54 if (!instanceUploads.tryAcquire()) {
55 scopePermits.release();
56 throw new TransferAdmissionRejectedException(
57 "instance upload permits are exhausted",
58 FileserverFailureContext.of(FileserverErrorCode.TRANSFER_ADMISSION_REJECTED, true));
59 }
60 return new SemaphorePermit(scopePermits, instanceUploads);
61 }
62
63 @Override
64 public TransferPermit acquireDirectDownload(QuotaScope scope) {
65 if (!directDownloads.tryAcquire()) {
66 throw new TransferAdmissionRejectedException(
67 "instance direct download permits are exhausted",
68 FileserverFailureContext.of(FileserverErrorCode.TRANSFER_ADMISSION_REJECTED, true));
69 }
70 return new SemaphorePermit(directDownloads);
71 }
72
73 /**
74 * True when the pool crossed the soft mark.
75 *
76 * <p>Callers use this to throttle or defer large uploads while still accepting small ones.
77 */
78 public boolean isAboveSoftHighWater() {
79 OptionalDouble used = usageProbe.usedFraction();
80 return used.isPresent() && used.getAsDouble() >= properties.softHighWater();
81 }
82
83 private void requireWithinFileSizePolicy(long requestedBytes) {
84 if (requestedBytes > maximumFileSize) {
85 throw new FileTooLargeException(
86 "requested upload exceeds the configured maximum file size",
87 FileserverFailureContext.of(FileserverErrorCode.FILE_TOO_LARGE, false));
88 }
89 }
90
91 private void requireBelowHardHighWater() {
92 OptionalDouble used = usageProbe.usedFraction();
93 if (used.isPresent() && used.getAsDouble() >= properties.hardHighWater()) {
94 throw new StorageFullException(
95 "storage pool crossed its hard high-water mark",
96 FileserverFailureContext.of(FileserverErrorCode.STORAGE_FULL, false));
97 }
98 }
99
100 private Semaphore scopePermits(QuotaScope scope) {
101 return scopeUploads.computeIfAbsent(
102 scope.canonicalKey(), ignored -> new Semaphore(properties.scopeUploadPermits()));
103 }
104
105 /** Releases each held semaphore exactly once, however many times {@code close} is called. */
106 private static final class SemaphorePermit implements TransferPermit {
107
108 private final Semaphore[] held;
109 private final AtomicBoolean released = new AtomicBoolean();
110
111 private SemaphorePermit(Semaphore... held) {
112 this.held = held.clone();
113 }
114
115 @Override
116 public void close() {
117 if (released.compareAndSet(false, true)) {
118 for (Semaphore semaphore : held) {
119 semaphore.release();
120 }
121 }
122 }
123
124 @Override
125 public boolean isHeld() {
126 return !released.get();
127 }
128 }
129 }
=== FileserverPlatformSettings quota ===
/** Transfer admission control and storage high-water marks. */
public record Quota(
@DefaultValue("16") int instanceUploadPermits,
@DefaultValue("4") int scopeUploadPermits,
@DefaultValue("64") int directDownloadPermits,
@DefaultValue("0.70") double softHighWater,
@DefaultValue("0.85") double hardHighWater) {
public Quota {
requirePositive(instanceUploadPermits, "quota.instance-upload-permits");
requirePositive(scopeUploadPermits, "quota.scope-upload-permits");
requirePositive(directDownloadPermits, "quota.direct-download-permits");
requireFraction(softHighWater, "quota.soft-high-water");
requireFraction(hardHighWater, "quota.hard-high-water");
if (softHighWater >= hardHighWater) {
throw new IllegalStateException(
PREFIX + ".quota.soft-high-water must be below quota.hard-high-water");
}
if (scopeUploadPermits > instanceUploadPermits) {
throw new IllegalStateException(
PREFIX + ".quota.scope-upload-permits must not exceed quota.instance-upload-permits");
}
}
}
=== JpaFileQuotaService ===
1 package dev.caskeleton.adapter.outbound.persistence.fileserver;
2
3 import dev.caskeleton.adapter.outbound.persistence.fileserver.entity.QuotaReservationEntity;
4 import dev.caskeleton.adapter.outbound.persistence.fileserver.repository.FileserverQuotaRepository;
5 import dev.caskeleton.application.fileserver.api.error.FileserverErrorCode;
6 import dev.caskeleton.application.fileserver.api.error.FileserverFailureContext;
7 import dev.caskeleton.application.fileserver.api.error.QuotaExceededException;
8 import dev.caskeleton.application.fileserver.api.metadata.FileQuotaService;
9 import dev.caskeleton.application.fileserver.api.metadata.QuotaReservation;
10 import dev.caskeleton.application.fileserver.api.metadata.QuotaReservationStatus;
11 import dev.caskeleton.application.fileserver.api.metadata.QuotaScope;
12 import java.time.Clock;
13 import java.time.Duration;
14 import java.time.Instant;
15 import java.util.UUID;
16 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
17 import org.springframework.stereotype.Repository;
18
19 /**
20 * JPA-backed {@link FileQuotaService}.
21 *
22 * <p>Reservation, extension, commit, and release are conditional statements, so a reservation that
23 * already expired or was released can never be extended or committed.
24 */
25 @Repository
26 @ConditionalOnProperty(prefix = "app.fileserver-platform", name = "enabled", havingValue = "true")
27 public class JpaFileQuotaService implements FileQuotaService {
28
29 private final FileserverQuotaRepository reservations;
30 private final Clock clock;
31
32 public JpaFileQuotaService(FileserverQuotaRepository reservations, Clock clock) {
33 this.reservations = reservations;
34 this.clock = clock;
35 }
36
37 @Override
38 public QuotaReservation reserve(QuotaScope scope, long expectedBytes, Duration ttl) {
39 if (expectedBytes < 0) {
40 throw new IllegalArgumentException("expectedBytes must not be negative");
41 }
42 Instant now = clock.instant();
43 QuotaReservationEntity entity =
44 new QuotaReservationEntity(
45 UUID.randomUUID(),
46 scope.type(),
47 scope.value(),
48 expectedBytes,
49 now.plus(ttl),
50 QuotaReservationStatus.RESERVED.name(),
51 now);
52 return FileEntityMapper.toReservation(reservations.save(entity));
53 }
54
55 @Override
56 public void extend(QuotaReservation reservation, long additionalBytes) {
57 if (additionalBytes < 0) {
58 throw new IllegalArgumentException("additionalBytes must not be negative");
59 }
60 int updated =
61 reservations.extend(reservation.reservationId(), additionalBytes, clock.instant());
62 if (updated == 0) {
63 throw quotaConflict("reservation is no longer extendable");
64 }
65 }
66
67 @Override
68 public void commit(QuotaReservation reservation, long actualBytes) {
69 if (actualBytes < 0) {
70 throw new IllegalArgumentException("actualBytes must not be negative");
71 }
72 int updated = reservations.commit(reservation.reservationId(), actualBytes, clock.instant());
73 if (updated == 0) {
74 throw quotaConflict("reservation is no longer committable");
75 }
76 }
77
78 @Override
79 public void release(QuotaReservation reservation) {
80 reservations.release(reservation.reservationId(), clock.instant());
81 }
82
83 /** Bytes currently reserved but not yet committed for a scope. */
84 public long reservedBytes(QuotaScope scope) {
85 return reservations.sumReservedBytes(scope.type(), scope.value(), clock.instant());
86 }
87
88 /** Bytes durably committed for a scope. */
89 public long committedBytes(QuotaScope scope) {
90 return reservations.sumCommittedBytes(scope.type(), scope.value());
91 }
92
93 private QuotaExceededException quotaConflict(String message) {
94 return new QuotaExceededException(
95 message, FileserverFailureContext.of(FileserverErrorCode.QUOTA_EXCEEDED, false));
96 }
97 }
=== quota repository ===
1 package dev.caskeleton.adapter.outbound.persistence.fileserver.repository;
2
3 import dev.caskeleton.adapter.outbound.persistence.fileserver.entity.QuotaReservationEntity;
4 import java.time.Instant;
5 import java.util.List;
6 import java.util.UUID;
7 import org.springframework.data.domain.Limit;
8 import org.springframework.data.jpa.repository.JpaRepository;
9 import org.springframework.data.jpa.repository.Modifying;
10 import org.springframework.data.jpa.repository.Query;
11 import org.springframework.data.repository.query.Param;
12
13 /**
14 * Conditional quota reservation statements.
15 *
16 * <p>Extend, commit, and release all require the reservation to still be {@code RESERVED} at the
17 * expected version, so a reservation reclaimed by expiry cannot be resurrected.
18 */
19 public interface FileserverQuotaRepository extends JpaRepository<QuotaReservationEntity, UUID> {
20
21 @Modifying(clearAutomatically = true, flushAutomatically = true)
22 @Query(
23 """
24 update QuotaReservationEntity q
25 set q.reservedBytes = q.reservedBytes + :additionalBytes,
26 q.version = q.version + 1,
27 q.updatedAt = :now
28 where q.reservationId = :reservationId
29 and q.status = 'RESERVED'
30 and q.expiresAt > :now
31 """)
32 int extend(
33 @Param("reservationId") UUID reservationId,
34 @Param("additionalBytes") long additionalBytes,
35 @Param("now") Instant now);
36
37 @Modifying(clearAutomatically = true, flushAutomatically = true)
38 @Query(
39 """
40 update QuotaReservationEntity q
41 set q.status = 'COMMITTED',
42 q.committedBytes = :actualBytes,
43 q.reservedBytes = 0,
44 q.version = q.version + 1,
45 q.updatedAt = :now
46 where q.reservationId = :reservationId
47 and q.status = 'RESERVED'
48 """)
49 int commit(
50 @Param("reservationId") UUID reservationId,
51 @Param("actualBytes") long actualBytes,
52 @Param("now") Instant now);
53
54 @Modifying(clearAutomatically = true, flushAutomatically = true)
55 @Query(
56 """
57 update QuotaReservationEntity q
58 set q.status = 'RELEASED',
59 q.reservedBytes = 0,
60 q.version = q.version + 1,
61 q.updatedAt = :now
62 where q.reservationId = :reservationId
63 and q.status = 'RESERVED'
64 """)
65 int release(@Param("reservationId") UUID reservationId, @Param("now") Instant now);
66
67 @Query(
68 """
69 select coalesce(sum(q.reservedBytes), 0) from QuotaReservationEntity q
70 where q.scopeType = :scopeType
71 and q.scopeValue = :scopeValue
72 and q.status = 'RESERVED'
73 and q.expiresAt > :now
74 """)
75 long sumReservedBytes(
76 @Param("scopeType") String scopeType,
77 @Param("scopeValue") String scopeValue,
78 @Param("now") Instant now);
79
80 @Query(
81 """
82 select coalesce(sum(q.committedBytes), 0) from QuotaReservationEntity q
83 where q.scopeType = :scopeType
84 and q.scopeValue = :scopeValue
85 and q.status = 'COMMITTED'
86 """)
87 long sumCommittedBytes(
88 @Param("scopeType") String scopeType, @Param("scopeValue") String scopeValue);
89
90 /** Live reservations for a scope, oldest first. */
91 @Query(
92 """
93 select q from QuotaReservationEntity q
94 where q.scopeType = :scopeType
95 and q.scopeValue = :scopeValue
96 and q.status = 'RESERVED'
97 and q.expiresAt > :now
98 order by q.createdAt asc
99 """)
100 List<QuotaReservationEntity> findActiveReservations(
101 @Param("scopeType") String scopeType,
102 @Param("scopeValue") String scopeValue,
103 @Param("now") Instant now,
104 Limit limit);
105
106 /** Committed rows for a scope that still carry bytes, newest first. */
107 @Query(
108 """
109 select q from QuotaReservationEntity q
110 where q.scopeType = :scopeType
111 and q.scopeValue = :scopeValue
112 and q.status = 'COMMITTED'
113 and q.committedBytes > 0
114 order by q.updatedAt desc
115 """)
116 List<QuotaReservationEntity> findCommittedWithBytes(
117 @Param("scopeType") String scopeType, @Param("scopeValue") String scopeValue, Limit limit);
118
119 /**
120 * Gives back part of a committed row.
121 *
122 * <p>The guard is what makes concurrent reclaims safe: a row that another reclaim already drew
123 * down below {@code amount} updates zero rows, and the caller moves to the next row instead of
124 * driving the ledger negative.
125 */
126 @Modifying(clearAutomatically = true, flushAutomatically = true)
127 @Query(
128 """
129 update QuotaReservationEntity q
130 set q.committedBytes = q.committedBytes - :amount,
131 q.version = q.version + 1,
132 q.updatedAt = :now
133 where q.reservationId = :reservationId
134 and q.committedBytes >= :amount
135 """)
136 int reduceCommitted(
137 @Param("reservationId") UUID reservationId,
138 @Param("amount") long amount,
139 @Param("now") Instant now);
140 }
=== quota reclaim gateway ===
1 package dev.caskeleton.adapter.outbound.persistence.fileserver;
2
3 import dev.caskeleton.adapter.outbound.persistence.fileserver.entity.QuotaReservationEntity;
4 import dev.caskeleton.adapter.outbound.persistence.fileserver.repository.FileserverQuotaRepository;
5 import dev.caskeleton.application.fileserver.api.metadata.QuotaScope;
6 import dev.caskeleton.application.fileserver.cleanup.QuotaReclaimGateway;
7 import java.time.Clock;
8 import java.time.Instant;
9 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
10 import org.springframework.data.domain.Limit;
11 import org.springframework.stereotype.Repository;
12
13 /**
14 * Returns reclaimed bytes to a scope's committed total.
15 *
16 * <p>Committed usage is spread over many rows, so a reclaim is drawn down newest-first across them
17 * until the amount is satisfied. Newest-first matters: the most recently committed rows are the
18 * ones a delete is most likely to correspond to, and drawing from them keeps historical rows from
19 * being hollowed out by unrelated deletions.
20 *
21 * <p>Each draw-down is conditional on the row still holding at least that many bytes, so two
22 * cleanup workers reclaiming at once cannot push the ledger negative — the loser simply moves to
23 * the next row.
24 *
25 * <p>A remainder that no row can absorb is dropped rather than carried. The ledger's floor is zero:
26 * a scope cannot owe negative bytes, and a reclaim that outruns the recorded total means the total
27 * was already understated, which a negative balance would not fix.
28 */
29 @Repository
30 @ConditionalOnProperty(prefix = "app.fileserver-platform", name = "enabled", havingValue = "true")
31 public class JpaQuotaReclaimGateway implements QuotaReclaimGateway {
32
33 private static final Limit RECLAIM_PAGE = Limit.of(64);
34
35 private final FileserverQuotaRepository reservations;
36 private final Clock clock;
37
38 public JpaQuotaReclaimGateway(FileserverQuotaRepository reservations, Clock clock) {
39 this.reservations = reservations;
40 this.clock = clock;
41 }
42
43 @Override
44 public void reclaim(QuotaScope scope, long bytes) {
45 if (bytes < 0) {
46 throw new IllegalArgumentException("bytes must not be negative");
47 }
48 if (bytes == 0) {
49 return;
50 }
51 Instant now = clock.instant();
52 long outstanding = bytes;
53 for (QuotaReservationEntity committed :
54 reservations.findCommittedWithBytes(scope.type(), scope.value(), RECLAIM_PAGE)) {
55 if (outstanding == 0) {
56 return;
57 }
58 long draw = Math.min(outstanding, committed.getCommittedBytes());
59 if (reservations.reduceCommitted(committed.getReservationId(), draw, now) == 1) {
60 outstanding -= draw;
61 }
62 }
63 }
64 }
=== recovery upsert ===
1 package dev.caskeleton.adapter.outbound.persistence.fileserver;
2
3 import dev.caskeleton.adapter.outbound.persistence.fileserver.entity.RecoveryItemEntity;
4 import dev.caskeleton.adapter.outbound.persistence.fileserver.repository.FileserverRecoveryRepository;
5 import dev.caskeleton.application.fileserver.api.FileId;
6 import dev.caskeleton.application.fileserver.recovery.ReconciliationStatus;
7 import dev.caskeleton.application.fileserver.recovery.RecoveryQueue;
8 import java.time.Clock;
9 import java.time.Instant;
10 import java.util.List;
11 import java.util.UUID;
12 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
13 import org.springframework.data.domain.Limit;
14 import org.springframework.stereotype.Repository;
15
16 /**
17 * Durable recovery queue over {@code fs_recovery_item}.
18 *
19 * <p>An enqueue is an upsert: the same file reported twice updates the open item rather than adding
20 * a second one. Reconciliation runs on a schedule and re-raises whatever it still cannot settle, so
21 * an append-only queue would grow one row per sweep per unresolved file and bury the distinct
22 * problems under repetitions of the same one.
23 *
24 * <p>Resolution keeps the outcome rather than deleting the row. {@code UNRESOLVED} and {@code
25 * QUARANTINE_REQUIRED} are the two answers a human has to act on, and both are worthless if the
26 * record of what the system concluded disappears with the item.
27 */
28 @Repository
29 @ConditionalOnProperty(prefix = "app.fileserver-platform", name = "enabled", havingValue = "true")
30 public class JpaRecoveryQueue implements RecoveryQueue {
31
32 private static final String STATUS_PENDING = "PENDING";
33
34 private final FileserverRecoveryRepository items;
35 private final Clock clock;
36
37 public JpaRecoveryQueue(FileserverRecoveryRepository items, Clock clock) {
38 this.items = items;
39 this.clock = clock;
40 }
41
42 @Override
43 public void enqueue(FileId fileId, String reasonCode) {
44 Instant now = clock.instant();
45 if (items.refreshPending(fileId.value(), reasonCode, now) > 0) {
46 return;
47 }
48 items.save(
49 new RecoveryItemEntity(UUID.randomUUID(), fileId.value(), reasonCode, STATUS_PENDING, now));
50 }
51
52 @Override
53 public List<FileId> pending(int limit) {
54 if (limit < 1) {
55 throw new IllegalArgumentException("limit must be positive");
56 }
57 return items.findPending(Limit.of(limit)).stream()
58 .map(RecoveryItemEntity::getFileId)
59 .map(FileId::of)
60 .toList();
61 }
62
63 @Override
64 public void resolve(FileId fileId, ReconciliationStatus status) {
65 items.resolvePending(fileId.value(), status.name(), clock.instant());
66 }
67 }
1 package dev.caskeleton.adapter.outbound.persistence.fileserver.repository;
2
3 import dev.caskeleton.adapter.outbound.persistence.fileserver.entity.RecoveryItemEntity;
4 import java.time.Instant;
5 import java.util.List;
6 import java.util.UUID;
7 import org.springframework.data.domain.Limit;
8 import org.springframework.data.jpa.repository.JpaRepository;
9 import org.springframework.data.jpa.repository.Modifying;
10 import org.springframework.data.jpa.repository.Query;
11 import org.springframework.data.repository.query.Param;
12
13 /** Durable list of files whose physical and logical state could not be reconciled automatically. */
14 public interface FileserverRecoveryRepository extends JpaRepository<RecoveryItemEntity, UUID> {
15
16 @Query(
17 """
18 select r from RecoveryItemEntity r
19 where r.status = 'PENDING'
20 order by r.createdAt asc
21 """)
22 List<RecoveryItemEntity> findPending(Limit limit);
23
24 /**
25 * Re-raises an open item instead of adding a second one.
26 *
27 * <p>Reconciliation is retried on a schedule, so the same file reaches the queue repeatedly. One
28 * open item per file keeps the queue a worklist rather than a failure log; the newest reason wins
29 * because it describes the most recent evidence.
30 */
31 @Modifying(clearAutomatically = true, flushAutomatically = true)
32 @Query(
33 """
34 update RecoveryItemEntity r
35 set r.reasonCode = :reasonCode,
36 r.attempt = r.attempt + 1,
37 r.updatedAt = :now
38 where r.fileId = :fileId
39 and r.status = 'PENDING'
40 """)
41 int refreshPending(
42 @Param("fileId") UUID fileId,
43 @Param("reasonCode") String reasonCode,
44 @Param("now") Instant now);
45
46 @Modifying(clearAutomatically = true, flushAutomatically = true)
47 @Query(
48 """
49 update RecoveryItemEntity r
50 set r.status = :status,
51 r.updatedAt = :now
52 where r.fileId = :fileId
53 and r.status = 'PENDING'
54 """)
55 int resolvePending(
56 @Param("fileId") UUID fileId, @Param("status") String status, @Param("now") Instant now);
57 }
=== schema revision and activation ===
1 package dev.caskeleton.adapter.outbound.persistence.fileserver;
2
3 import org.springframework.jdbc.core.JdbcOperations;
4
5 /**
6 * Proves the Fileserver schema stream was applied and promoted before the capability serves a
7 * request.
8 *
9 * <p>The stream is operator-applied, like every other optional capability stream: the application
10 * migrates {@code db/migration/postgresql} only, and {@code db/migration/jpa/fileserver} is applied
11 * and promoted to {@code ACTIVE} deliberately. Until that happens the {@code fs_*} tables either do
12 * not exist or are not sanctioned for use.
13 *
14 * <p>The check runs once, at startup, rather than per operation. The sibling capabilities verify on
15 * every call because they are low-frequency; a file download is not, and a registry round trip on
16 * the metadata read path would be paid by every byte served. Startup is also the honest place for
17 * it — an unpromoted stream is a deployment state, not a per-request condition.
18 *
19 * <p>Failing here rather than at the first upload is the point. The alternative is a raw "relation
20 * fs_file does not exist" surfacing as a 500 to whoever happened to upload first.
21 */
22 public final class FileserverSchemaActivation {
23
24 static final String CAPABILITY_ID = "jpa-fileserver-metadata-v1";
25
26 private static final String ACTIVE_CAPABILITY_SQL =
27 """
28 select count(*)
29 from capability_schema_registry
30 where capability_id = 'jpa-fileserver-metadata-v1'
31 and core_epoch = 1
32 and feature_revision >= 2
33 and lifecycle_state = 'ACTIVE'
34 """;
35
36 private final JdbcOperations jdbc;
37
38 public FileserverSchemaActivation(JdbcOperations jdbc) {
39 this.jdbc = jdbc;
40 }
41
42 /**
43 * Fails closed unless the stream is applied and promoted.
44 *
45 * <p>An unreadable registry is treated as "not promoted" rather than "assume fine": the registry
46 * table itself is created by the core stream, so its absence means the prerequisite chain was
47 * never established.
48 */
49 public void requireActive() {
50 Integer active;
51 try {
52 active = jdbc.queryForObject(ACTIVE_CAPABILITY_SQL, Integer.class);
53 } catch (RuntimeException unreadable) {
54 throw new IllegalStateException(
55 CAPABILITY_ID
56 + " could not be verified: the capability schema registry is unreadable, so the "
57 + "Fileserver schema stream cannot be confirmed as applied",
58 unreadable);
59 }
60 if (active == null || active != 1) {
61 throw new IllegalStateException(
62 CAPABILITY_ID
63 + " is not ACTIVE at core epoch 1 revision 2. Apply db/migration/jpa/fileserver "
64 + "against history table flyway_jpa_fileserver_history and promote the capability "
65 + "before enabling app.fileserver-platform.enabled");
66 }
67 }
68 }
--- src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V1__create_fileserver_metadata.sql
13: AND lifecycle_state = 'ACTIVE'
187: feature_revision,
188: lifecycle_state
--- src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V2__fileserver_recovery_and_staging_cleanup.sql
16: AND feature_revision >= 1
18: RAISE EXCEPTION 'fileserver recovery schema requires fileserver metadata revision 1';
61: SET feature_revision = 2
63: AND feature_revision < 2;
--- src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V3__fileserver_fenced_cleanup_lease.sql
19: ADD COLUMN IF NOT EXISTS claim_token uuid,
23:COMMENT ON COLUMN fs_cleanup_item.claim_token IS
--- src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V4__fileserver_upload_terminal_state.sql
24: AND feature_revision >= 2
26: RAISE EXCEPTION 'fileserver upload terminal state requires fileserver metadata revision 2';
32: ADD COLUMN IF NOT EXISTS lifecycle_state varchar(16) NOT NULL DEFAULT 'ACTIVE';
35: DROP CONSTRAINT IF EXISTS ck_fs_upload_lifecycle_state;
38: ADD CONSTRAINT ck_fs_upload_lifecycle_state
39: CHECK (lifecycle_state IN ('ACTIVE', 'TERMINAL'));
41:COMMENT ON COLUMN fs_upload_session.lifecycle_state IS
49: WHERE lifecycle_state = 'TERMINAL';
=== quota contract docs ===
### 9. Quota settlement is FIFO within a scope
Nothing links a reservation row to the upload that took it, and the design deliberately reclaims
stragglers by TTL and the `STALE_QUOTA_RESERVATION` cleanup type rather than threading a reservation
id through the upload session. `QuotaCommitGateway` therefore settles the oldest live reservation in
the file's namespace.
Which row closes does not change any quota decision: enforcement sums reserved and committed bytes
per scope and never reads an individual row. Concurrent uploads of different sizes can leave the
reserved total transiently high or low, and it converges as each settles. Durable usage with no live
reservation behind it — an upload that outlived its TTL — is still recorded, because a ledger that
silently under-counts is worse than one that is briefly imprecise.
Pinned by `PostgreSqlFileserverReclamationIntegrationTest`.
Expected: FAIL because admission control is not implemented.
- [ ] **Step 3: Implement reservation and bounded permits**
Use DB conditional updates for quota bytes and JVM semaphores for per-instance transfer concurrency. A create request with unknown length reserves the configured initial chunk; append extends the reservation before writing additional bytes. On cancellation or failure, release the reservation in `finally` or cleanup recovery.
```java
public interface TransferAdmissionController {
TransferPermit acquireUpload(QuotaScope scope, long requestedBytes);
TransferPermit acquireDirectDownload(QuotaScope scope);
}
```
A hard storage high-water condition maps to `StorageFullException`; scope limit maps to `QuotaExceededException`; temporary permit exhaustion maps to `TransferAdmissionRejectedException` with `retryable=true`.
- [ ] **Step 4: Run quota and concurrency tests**
@@ -0,0 +1,530 @@
package dev.caskeleton.adapter.outbound.persistence.readiness;
import static org.assertj.core.api.Assertions.assertThat;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaCleanupQueue;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaContentReferenceLedger;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaFileMetadataStore;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaFileQuotaService;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaQuotaCommitGateway;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaQuotaReclaimGateway;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaRecoveryQueue;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaStagingUploadLocator;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaUploadSessionStore;
import dev.caskeleton.application.fileserver.api.ContentKey;
import dev.caskeleton.application.fileserver.api.FileId;
import dev.caskeleton.application.fileserver.api.FileState;
import dev.caskeleton.application.fileserver.api.StorageNamespace;
import dev.caskeleton.application.fileserver.api.UploadId;
import dev.caskeleton.application.fileserver.api.metadata.FileRecord;
import dev.caskeleton.application.fileserver.api.metadata.FileRecordDraft;
import dev.caskeleton.application.fileserver.api.metadata.FileRecordMutation;
import dev.caskeleton.application.fileserver.api.metadata.QuotaScope;
import dev.caskeleton.application.fileserver.api.metadata.UploadSession;
import dev.caskeleton.application.fileserver.api.metadata.UploadSessionDraft;
import dev.caskeleton.application.fileserver.api.transfer.UploadProtocol;
import dev.caskeleton.application.fileserver.cleanup.CleanupItem;
import dev.caskeleton.application.fileserver.cleanup.CleanupRequest;
import dev.caskeleton.application.fileserver.cleanup.CleanupType;
import dev.caskeleton.application.fileserver.recovery.ReconciliationStatus;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.UUID;
import java.util.function.Supplier;
import org.flywaydb.core.Flyway;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.support.TransactionTemplate;
/**
* Real-PostgreSQL proof of the reclamation side of the Fileserver.
*
* <p>The cleanup queue, the recovery queue, and the quota ledger are the components that decide
* when physical bytes may be destroyed and how much space a tenant is charged for. All three are
* conditional-update designs whose correctness lives in SQL, so an in-memory fake would prove
* nothing about them: the partial unique index, the {@code IN ('PENDING','FAILED')} claim guard,
* and the {@code committedBytes >= :amount} floor only exist in the database.
*/
class PostgreSqlFileserverReclamationIntegrationTest {
private static PostgreSqlReadinessSupport postgres;
private static FileserverJpaTestContext context;
private static JdbcTemplate jdbc;
private static TransactionTemplate transactions;
private static JpaFileMetadataStore files;
private static JpaUploadSessionStore uploads;
private static JpaFileQuotaService quota;
private static JpaCleanupQueue cleanupQueue;
private static JpaRecoveryQueue recoveryQueue;
private static JpaQuotaCommitGateway quotaCommit;
private static JpaQuotaReclaimGateway quotaReclaim;
private static JpaContentReferenceLedger ledger;
private static JpaStagingUploadLocator stagingLocator;
@BeforeAll
static void startAndMigratePostgreSql() {
PostgreSqlReadinessSupport.assertDockerAvailable();
postgres = PostgreSqlReadinessSupport.start(8, 2_000);
migrate("classpath:db/migration/postgresql", "flyway_schema_history");
migrateIndependent(
"classpath:db/migration/jpa/core", "flyway_jpa_core_history", "explicit-jpa-core-adoption");
migrateIndependent(
"classpath:db/migration/jpa/fileserver",
"flyway_jpa_fileserver_history",
"explicit-jpa-fileserver-adoption");
jdbc = new JdbcTemplate(postgres.dataSource());
context = new FileserverJpaTestContext(postgres.dataSource(), Clock.systemUTC());
transactions = context.transactions();
files = context.bean(JpaFileMetadataStore.class);
uploads = context.bean(JpaUploadSessionStore.class);
quota = context.bean(JpaFileQuotaService.class);
cleanupQueue = context.bean(JpaCleanupQueue.class);
recoveryQueue = context.bean(JpaRecoveryQueue.class);
quotaCommit = context.bean(JpaQuotaCommitGateway.class);
quotaReclaim = context.bean(JpaQuotaReclaimGateway.class);
ledger = context.bean(JpaContentReferenceLedger.class);
stagingLocator = context.bean(JpaStagingUploadLocator.class);
}
@AfterAll
static void stopPostgreSql() {
if (context != null) {
context.close();
}
if (postgres != null) {
postgres.close();
}
}
@BeforeEach
void truncate() {
jdbc.execute(
"TRUNCATE fs_recovery_item, fs_cleanup_item, fs_quota_reservation,"
+ " fs_verification_result, fs_upload_session, fs_file CASCADE");
}
@Test
void aStagingCleanupSurvivesTheRoundTripWithItsUploadIdentity() {
FileRecord record = insertRecord();
UploadId uploadId = UploadId.of(UUID.randomUUID());
inTransaction(
() -> {
cleanupQueue.enqueue(
CleanupRequest.forStaging(CleanupType.CANCELLED_STAGING, record.fileId(), uploadId));
return null;
});
List<CleanupItem> claimed = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10));
assertThat(claimed)
.singleElement()
.satisfies(
item -> {
assertThat(item.request().uploadId()).contains(uploadId);
assertThat(item.request().contentKey()).isEmpty();
assertThat(item.type()).isEqualTo(CleanupType.CANCELLED_STAGING);
});
}
@Test
void aClaimedItemIsNotHandedToASecondWorker() {
FileRecord record = insertRecord();
inTransaction(
() -> {
cleanupQueue.enqueue(
CleanupRequest.forContent(
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
return null;
});
List<CleanupItem> first = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10));
List<CleanupItem> second = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10));
assertThat(first).hasSize(1);
assertThat(second).isEmpty();
}
@Test
void aFailedItemBecomesDueAgainOnlyAfterItsBackoff() {
FileRecord record = insertRecord();
inTransaction(
() -> {
cleanupQueue.enqueue(
CleanupRequest.forContent(
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
return null;
});
CleanupItem item = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10)).get(0);
Instant retryAt = Instant.now().plus(Duration.ofMinutes(5));
inTransaction(
() -> {
cleanupQueue.markFailed(item, "STORAGE_UNAVAILABLE", retryAt);
return null;
});
assertThat(inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10))).isEmpty();
assertThat(inTransaction(() -> cleanupQueue.claimDue(retryAt.plusSeconds(1), 10))).hasSize(1);
}
@Test
void anItemThatKeepsFailingIsAbandonedRatherThanRetriedForever() {
FileRecord record = insertRecord();
inTransaction(
() -> {
cleanupQueue.enqueue(
CleanupRequest.forContent(
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
return null;
});
Instant due = Instant.now();
for (int attempt = 0; attempt < JpaCleanupQueue.MAXIMUM_ATTEMPTS; attempt++) {
List<CleanupItem> claimed = inTransaction(() -> cleanupQueue.claimDue(due, 10));
if (claimed.isEmpty()) {
break;
}
inTransaction(
() -> {
cleanupQueue.markFailed(claimed.get(0), "STILL_FAILING", due);
return null;
});
}
assertThat(inTransaction(() -> cleanupQueue.claimDue(due.plusSeconds(1), 10))).isEmpty();
assertThat(statusCounts("fs_cleanup_item", "ABANDONED")).isEqualTo(1);
}
@Test
void aDoneItemIsNeverClaimedAgain() {
FileRecord record = insertRecord();
inTransaction(
() -> {
cleanupQueue.enqueue(
CleanupRequest.forContent(
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
return null;
});
CleanupItem item = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10)).get(0);
inTransaction(
() -> {
cleanupQueue.markDone(item);
return null;
});
assertThat(inTransaction(() -> cleanupQueue.claimDue(Instant.now().plusSeconds(600), 10)))
.isEmpty();
}
@Test
void thesameFileReportedTwiceHoldsOneOpenRecoveryItem() {
FileRecord record = insertRecord();
inTransaction(
() -> {
recoveryQueue.enqueue(record.fileId(), "READY_DIGEST_MISMATCH");
recoveryQueue.enqueue(record.fileId(), "READY_SIZE_MISMATCH");
return null;
});
assertThat(inTransaction(() -> recoveryQueue.pending(10))).containsExactly(record.fileId());
assertThat(rowCount("fs_recovery_item")).isEqualTo(1);
assertThat(jdbc.queryForObject("SELECT reason_code FROM fs_recovery_item", String.class))
.isEqualTo("READY_SIZE_MISMATCH");
}
@Test
void aResolvedRecoveryItemLeavesThePendingListButKeepsItsOutcome() {
FileRecord record = insertRecord();
inTransaction(
() -> {
recoveryQueue.enqueue(record.fileId(), "PUBLISH_EVIDENCE_INCOMPLETE");
return null;
});
inTransaction(
() -> {
recoveryQueue.resolve(record.fileId(), ReconciliationStatus.QUARANTINE_REQUIRED);
return null;
});
assertThat(inTransaction(() -> recoveryQueue.pending(10))).isEmpty();
assertThat(statusCounts("fs_recovery_item", "QUARANTINE_REQUIRED")).isEqualTo(1);
}
@Test
void aResolvedFileCanBeRaisedAgainLater() {
FileRecord record = insertRecord();
inTransaction(
() -> {
recoveryQueue.enqueue(record.fileId(), "FIRST");
recoveryQueue.resolve(record.fileId(), ReconciliationStatus.UNRESOLVED);
recoveryQueue.enqueue(record.fileId(), "SECOND");
return null;
});
assertThat(inTransaction(() -> recoveryQueue.pending(10))).containsExactly(record.fileId());
assertThat(rowCount("fs_recovery_item")).isEqualTo(2);
}
@Test
void committingAnUploadMovesReservedBytesToCommitted() {
FileRecord record = insertRecord();
UploadSession session = insertSession(record.fileId());
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
inTransaction(() -> quota.reserve(scope, 1_000, Duration.ofHours(1)));
inTransaction(
() -> {
quotaCommit.commit(session, 600);
return null;
});
assertThat(inTransaction(() -> quota.reservedBytes(scope))).isZero();
assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(600);
}
@Test
void committingWithoutALiveReservationStillRecordsTheDurableUsage() {
FileRecord record = insertRecord();
UploadSession session = insertSession(record.fileId());
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
inTransaction(
() -> {
quotaCommit.commit(session, 450);
return null;
});
assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(450);
}
@Test
void releasingAnUploadGivesTheReservedCapacityBack() {
FileRecord record = insertRecord();
UploadSession session = insertSession(record.fileId());
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
inTransaction(() -> quota.reserve(scope, 2_000, Duration.ofHours(1)));
inTransaction(
() -> {
quotaCommit.release(session);
return null;
});
assertThat(inTransaction(() -> quota.reservedBytes(scope))).isZero();
assertThat(inTransaction(() -> quota.committedBytes(scope))).isZero();
}
@Test
void reclaimingDrawsCommittedBytesDownAcrossRows() {
FileRecord record = insertRecord();
UploadSession session = insertSession(record.fileId());
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
inTransaction(
() -> {
quotaCommit.commit(session, 300);
quotaCommit.commit(session, 700);
return null;
});
inTransaction(
() -> {
quotaReclaim.reclaim(scope, 800);
return null;
});
assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(200);
}
@Test
void reclaimingMoreThanIsRecordedStopsAtZeroRatherThanGoingNegative() {
FileRecord record = insertRecord();
UploadSession session = insertSession(record.fileId());
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
inTransaction(
() -> {
quotaCommit.commit(session, 100);
return null;
});
inTransaction(
() -> {
quotaReclaim.reclaim(scope, 5_000);
return null;
});
assertThat(inTransaction(() -> quota.committedBytes(scope))).isZero();
}
@Test
void theLedgerReportsAKeyAsReferencedOnlyWhileARecordNamesIt() {
ContentKey key = contentKey();
assertThat(inTransaction(() -> ledger.isReferenced(key))).isFalse();
FileRecord record = insertRecord();
inTransaction(() -> publish(record, key));
assertThat(inTransaction(() -> ledger.isReferenced(key))).isTrue();
}
@Test
void theStagingLocatorReturnsTheNewestSessionForAFile() {
FileRecord record = insertRecord();
UploadSession older = insertSession(record.fileId());
UploadSession newer = insertSession(record.fileId());
Optional<UploadId> located = inTransaction(() -> stagingLocator.locate(record.fileId()));
assertThat(located).isPresent();
assertThat(located.get()).isIn(older.uploadId(), newer.uploadId());
assertThat(inTransaction(() -> stagingLocator.locate(FileId.of(UUID.randomUUID())))).isEmpty();
}
private FileRecord insertRecord() {
return inTransaction(
() ->
files.insert(
new FileRecordDraft(
FileId.of(UUID.randomUUID()),
StorageNamespace.of("tenant-a"),
"report.bin",
Optional.of("application/octet-stream"),
OptionalLong.of(1_000))));
}
private FileRecord publish(FileRecord record, ContentKey key) {
FileRecord uploading =
files.transition(
record.fileId(),
record.version(),
FileState.CREATED,
FileState.UPLOADING,
FileRecordMutation.none());
FileRecord uploaded =
files.transition(
uploading.fileId(),
uploading.version(),
FileState.UPLOADING,
FileState.UPLOADED,
FileRecordMutation.uploaded(10, "b".repeat(64)));
FileRecord verifying =
files.transition(
uploaded.fileId(),
uploaded.version(),
FileState.UPLOADED,
FileState.VERIFYING,
FileRecordMutation.none());
return files.transition(
verifying.fileId(),
verifying.version(),
FileState.VERIFYING,
FileState.READY,
FileRecordMutation.publishAt(
key, 10, "b".repeat(64), "\"" + "b".repeat(64) + "\"", Instant.now()));
}
private UploadSession insertSession(FileId fileId) {
return inTransaction(
() ->
uploads.create(
new UploadSessionDraft(
UploadId.of(UUID.randomUUID()),
fileId,
UploadProtocol.RAW,
OptionalLong.of(1_000),
Instant.now().plus(Duration.ofHours(1)))));
}
private static ContentKey contentKey() {
String flat = UUID.randomUUID().toString().replace("-", "");
return ContentKey.of(flat.substring(0, 2) + '/' + flat.substring(2, 4) + '/' + flat);
}
private static <T> T inTransaction(Supplier<T> action) {
return transactions.execute(status -> action.get());
}
private static int rowCount(String table) {
Integer count = jdbc.queryForObject("SELECT count(*) FROM " + table, Integer.class);
return count == null ? 0 : count;
}
private static int statusCounts(String table, String status) {
Integer count =
jdbc.queryForObject(
"SELECT count(*) FROM " + table + " WHERE status = ?", Integer.class, status);
return count == null ? 0 : count;
}
private static void migrate(String location, String historyTable) {
Flyway.configure()
.dataSource(postgres.dataSource())
.locations(location)
.table(historyTable)
.baselineOnMigrate(false)
.outOfOrder(false)
.load()
.migrate();
}
private static void migrateIndependent(
String location, String historyTable, String baselineDescription) {
Flyway flyway =
Flyway.configure()
.dataSource(postgres.dataSource())
.locations(location)
.table(historyTable)
.baselineVersion("0")
.baselineDescription(baselineDescription)
.baselineOnMigrate(false)
.outOfOrder(false)
.load();
flyway.baseline();
flyway.migrate();
}
@Test
void analysisProbeCrashReclaimsBypassMaximumAttempts() {
FileRecord record = insertRecord();
inTransaction(
() -> {
cleanupQueue.enqueue(
CleanupRequest.forContent(
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
return null;
});
Instant base = Instant.now();
for (int crash = 0; crash < JpaCleanupQueue.MAXIMUM_ATTEMPTS + 1; crash++) {
Instant claimAt = base.plus(Duration.ofHours(crash));
List<CleanupItem> claimed = inTransaction(() -> cleanupQueue.claimDue(claimAt, 10));
assertThat(claimed).hasSize(1);
int reclaimed =
inTransaction(() -> cleanupQueue.reclaimExpiredClaims(claimAt.plus(Duration.ofMinutes(11)), 10));
assertThat(reclaimed).isEqualTo(1);
}
Integer attempt =
jdbc.queryForObject("SELECT attempt FROM fs_cleanup_item", Integer.class);
String status =
jdbc.queryForObject("SELECT status FROM fs_cleanup_item", String.class);
String error =
jdbc.queryForObject("SELECT last_error_code FROM fs_cleanup_item", String.class);
System.out.println("fileserverCleanupCrash.maxAttempts=" + JpaCleanupQueue.MAXIMUM_ATTEMPTS);
System.out.println("fileserverCleanupCrash.actualAttempt=" + attempt);
System.out.println("fileserverCleanupCrash.status=" + status);
System.out.println("fileserverCleanupCrash.lastError=" + error);
assertThat(attempt).isEqualTo(JpaCleanupQueue.MAXIMUM_ATTEMPTS + 1);
assertThat(status).isEqualTo("FAILED");
}
}
@@ -0,0 +1,42 @@
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
command=./gradlew :adapter:outbound:persistence-jpa:postgresqlFileserverReclamationIntegrationTest --tests dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlFileserverReclamationIntegrationTest.analysisProbeConcurrentRecoveryEnqueueIsNotAnAtomicUpsert --console=plain --no-daemon
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
Daemon will be stopped at the end of the build
> Task :build-logic:extractPluginRequests UP-TO-DATE
> Task :build-logic:generatePluginAdapters UP-TO-DATE
> Task :build-logic:compileJava UP-TO-DATE
> Task :build-logic:compileGroovy UP-TO-DATE
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
> Task :build-logic:pluginDescriptors UP-TO-DATE
> Task :build-logic:processResources UP-TO-DATE
> Task :build-logic:classes UP-TO-DATE
> Task :build-logic:jar UP-TO-DATE
> Task :shared-contract:compileJava UP-TO-DATE
> Task :shared-contract:processResources UP-TO-DATE
> Task :shared-contract:classes UP-TO-DATE
> Task :shared-contract:jar UP-TO-DATE
> Task :application-core:compileJava UP-TO-DATE
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes UP-TO-DATE
> Task :application-core:jar UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses
> Task :adapter:outbound:persistence-jpa:postgresqlFileserverReclamationIntegrationTest
BUILD SUCCESSFUL in 17s
19 actionable tasks: 2 executed, 17 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
=== ASSERTED OBSERVATION (from Gradle test XML) ===
fileserverRecovery.concurrentFailures=1
fileserverRecovery.first=SUCCESS
fileserverRecovery.second=org.springframework.dao.DataIntegrityViolationException
fileserverRecovery.rowCount=1
source restored after probe: yes
@@ -0,0 +1,532 @@
package dev.caskeleton.adapter.outbound.persistence.readiness;
import static org.assertj.core.api.Assertions.assertThat;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaCleanupQueue;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaContentReferenceLedger;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaFileMetadataStore;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaFileQuotaService;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaQuotaCommitGateway;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaQuotaReclaimGateway;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaRecoveryQueue;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaStagingUploadLocator;
import dev.caskeleton.adapter.outbound.persistence.fileserver.JpaUploadSessionStore;
import dev.caskeleton.application.fileserver.api.ContentKey;
import dev.caskeleton.application.fileserver.api.FileId;
import dev.caskeleton.application.fileserver.api.FileState;
import dev.caskeleton.application.fileserver.api.StorageNamespace;
import dev.caskeleton.application.fileserver.api.UploadId;
import dev.caskeleton.application.fileserver.api.metadata.FileRecord;
import dev.caskeleton.application.fileserver.api.metadata.FileRecordDraft;
import dev.caskeleton.application.fileserver.api.metadata.FileRecordMutation;
import dev.caskeleton.application.fileserver.api.metadata.QuotaScope;
import dev.caskeleton.application.fileserver.api.metadata.UploadSession;
import dev.caskeleton.application.fileserver.api.metadata.UploadSessionDraft;
import dev.caskeleton.application.fileserver.api.transfer.UploadProtocol;
import dev.caskeleton.application.fileserver.cleanup.CleanupItem;
import dev.caskeleton.application.fileserver.cleanup.CleanupRequest;
import dev.caskeleton.application.fileserver.cleanup.CleanupType;
import dev.caskeleton.application.fileserver.recovery.ReconciliationStatus;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.UUID;
import java.util.function.Supplier;
import org.flywaydb.core.Flyway;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.support.TransactionTemplate;
/**
* Real-PostgreSQL proof of the reclamation side of the Fileserver.
*
* <p>The cleanup queue, the recovery queue, and the quota ledger are the components that decide
* when physical bytes may be destroyed and how much space a tenant is charged for. All three are
* conditional-update designs whose correctness lives in SQL, so an in-memory fake would prove
* nothing about them: the partial unique index, the {@code IN ('PENDING','FAILED')} claim guard,
* and the {@code committedBytes >= :amount} floor only exist in the database.
*/
class PostgreSqlFileserverReclamationIntegrationTest {
private static PostgreSqlReadinessSupport postgres;
private static FileserverJpaTestContext context;
private static JdbcTemplate jdbc;
private static TransactionTemplate transactions;
private static JpaFileMetadataStore files;
private static JpaUploadSessionStore uploads;
private static JpaFileQuotaService quota;
private static JpaCleanupQueue cleanupQueue;
private static JpaRecoveryQueue recoveryQueue;
private static JpaQuotaCommitGateway quotaCommit;
private static JpaQuotaReclaimGateway quotaReclaim;
private static JpaContentReferenceLedger ledger;
private static JpaStagingUploadLocator stagingLocator;
@BeforeAll
static void startAndMigratePostgreSql() {
PostgreSqlReadinessSupport.assertDockerAvailable();
postgres = PostgreSqlReadinessSupport.start(8, 2_000);
migrate("classpath:db/migration/postgresql", "flyway_schema_history");
migrateIndependent(
"classpath:db/migration/jpa/core", "flyway_jpa_core_history", "explicit-jpa-core-adoption");
migrateIndependent(
"classpath:db/migration/jpa/fileserver",
"flyway_jpa_fileserver_history",
"explicit-jpa-fileserver-adoption");
jdbc = new JdbcTemplate(postgres.dataSource());
context = new FileserverJpaTestContext(postgres.dataSource(), Clock.systemUTC());
transactions = context.transactions();
files = context.bean(JpaFileMetadataStore.class);
uploads = context.bean(JpaUploadSessionStore.class);
quota = context.bean(JpaFileQuotaService.class);
cleanupQueue = context.bean(JpaCleanupQueue.class);
recoveryQueue = context.bean(JpaRecoveryQueue.class);
quotaCommit = context.bean(JpaQuotaCommitGateway.class);
quotaReclaim = context.bean(JpaQuotaReclaimGateway.class);
ledger = context.bean(JpaContentReferenceLedger.class);
stagingLocator = context.bean(JpaStagingUploadLocator.class);
}
@AfterAll
static void stopPostgreSql() {
if (context != null) {
context.close();
}
if (postgres != null) {
postgres.close();
}
}
@BeforeEach
void truncate() {
jdbc.execute(
"TRUNCATE fs_recovery_item, fs_cleanup_item, fs_quota_reservation,"
+ " fs_verification_result, fs_upload_session, fs_file CASCADE");
}
@Test
void aStagingCleanupSurvivesTheRoundTripWithItsUploadIdentity() {
FileRecord record = insertRecord();
UploadId uploadId = UploadId.of(UUID.randomUUID());
inTransaction(
() -> {
cleanupQueue.enqueue(
CleanupRequest.forStaging(CleanupType.CANCELLED_STAGING, record.fileId(), uploadId));
return null;
});
List<CleanupItem> claimed = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10));
assertThat(claimed)
.singleElement()
.satisfies(
item -> {
assertThat(item.request().uploadId()).contains(uploadId);
assertThat(item.request().contentKey()).isEmpty();
assertThat(item.type()).isEqualTo(CleanupType.CANCELLED_STAGING);
});
}
@Test
void aClaimedItemIsNotHandedToASecondWorker() {
FileRecord record = insertRecord();
inTransaction(
() -> {
cleanupQueue.enqueue(
CleanupRequest.forContent(
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
return null;
});
List<CleanupItem> first = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10));
List<CleanupItem> second = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10));
assertThat(first).hasSize(1);
assertThat(second).isEmpty();
}
@Test
void aFailedItemBecomesDueAgainOnlyAfterItsBackoff() {
FileRecord record = insertRecord();
inTransaction(
() -> {
cleanupQueue.enqueue(
CleanupRequest.forContent(
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
return null;
});
CleanupItem item = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10)).get(0);
Instant retryAt = Instant.now().plus(Duration.ofMinutes(5));
inTransaction(
() -> {
cleanupQueue.markFailed(item, "STORAGE_UNAVAILABLE", retryAt);
return null;
});
assertThat(inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10))).isEmpty();
assertThat(inTransaction(() -> cleanupQueue.claimDue(retryAt.plusSeconds(1), 10))).hasSize(1);
}
@Test
void anItemThatKeepsFailingIsAbandonedRatherThanRetriedForever() {
FileRecord record = insertRecord();
inTransaction(
() -> {
cleanupQueue.enqueue(
CleanupRequest.forContent(
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
return null;
});
Instant due = Instant.now();
for (int attempt = 0; attempt < JpaCleanupQueue.MAXIMUM_ATTEMPTS; attempt++) {
List<CleanupItem> claimed = inTransaction(() -> cleanupQueue.claimDue(due, 10));
if (claimed.isEmpty()) {
break;
}
inTransaction(
() -> {
cleanupQueue.markFailed(claimed.get(0), "STILL_FAILING", due);
return null;
});
}
assertThat(inTransaction(() -> cleanupQueue.claimDue(due.plusSeconds(1), 10))).isEmpty();
assertThat(statusCounts("fs_cleanup_item", "ABANDONED")).isEqualTo(1);
}
@Test
void aDoneItemIsNeverClaimedAgain() {
FileRecord record = insertRecord();
inTransaction(
() -> {
cleanupQueue.enqueue(
CleanupRequest.forContent(
CleanupType.DELETED_READY_CONTENT, record.fileId(), contentKey()));
return null;
});
CleanupItem item = inTransaction(() -> cleanupQueue.claimDue(Instant.now(), 10)).get(0);
inTransaction(
() -> {
cleanupQueue.markDone(item);
return null;
});
assertThat(inTransaction(() -> cleanupQueue.claimDue(Instant.now().plusSeconds(600), 10)))
.isEmpty();
}
@Test
void thesameFileReportedTwiceHoldsOneOpenRecoveryItem() {
FileRecord record = insertRecord();
inTransaction(
() -> {
recoveryQueue.enqueue(record.fileId(), "READY_DIGEST_MISMATCH");
recoveryQueue.enqueue(record.fileId(), "READY_SIZE_MISMATCH");
return null;
});
assertThat(inTransaction(() -> recoveryQueue.pending(10))).containsExactly(record.fileId());
assertThat(rowCount("fs_recovery_item")).isEqualTo(1);
assertThat(jdbc.queryForObject("SELECT reason_code FROM fs_recovery_item", String.class))
.isEqualTo("READY_SIZE_MISMATCH");
}
@Test
void aResolvedRecoveryItemLeavesThePendingListButKeepsItsOutcome() {
FileRecord record = insertRecord();
inTransaction(
() -> {
recoveryQueue.enqueue(record.fileId(), "PUBLISH_EVIDENCE_INCOMPLETE");
return null;
});
inTransaction(
() -> {
recoveryQueue.resolve(record.fileId(), ReconciliationStatus.QUARANTINE_REQUIRED);
return null;
});
assertThat(inTransaction(() -> recoveryQueue.pending(10))).isEmpty();
assertThat(statusCounts("fs_recovery_item", "QUARANTINE_REQUIRED")).isEqualTo(1);
}
@Test
void aResolvedFileCanBeRaisedAgainLater() {
FileRecord record = insertRecord();
inTransaction(
() -> {
recoveryQueue.enqueue(record.fileId(), "FIRST");
recoveryQueue.resolve(record.fileId(), ReconciliationStatus.UNRESOLVED);
recoveryQueue.enqueue(record.fileId(), "SECOND");
return null;
});
assertThat(inTransaction(() -> recoveryQueue.pending(10))).containsExactly(record.fileId());
assertThat(rowCount("fs_recovery_item")).isEqualTo(2);
}
@Test
void committingAnUploadMovesReservedBytesToCommitted() {
FileRecord record = insertRecord();
UploadSession session = insertSession(record.fileId());
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
inTransaction(() -> quota.reserve(scope, 1_000, Duration.ofHours(1)));
inTransaction(
() -> {
quotaCommit.commit(session, 600);
return null;
});
assertThat(inTransaction(() -> quota.reservedBytes(scope))).isZero();
assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(600);
}
@Test
void committingWithoutALiveReservationStillRecordsTheDurableUsage() {
FileRecord record = insertRecord();
UploadSession session = insertSession(record.fileId());
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
inTransaction(
() -> {
quotaCommit.commit(session, 450);
return null;
});
assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(450);
}
@Test
void releasingAnUploadGivesTheReservedCapacityBack() {
FileRecord record = insertRecord();
UploadSession session = insertSession(record.fileId());
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
inTransaction(() -> quota.reserve(scope, 2_000, Duration.ofHours(1)));
inTransaction(
() -> {
quotaCommit.release(session);
return null;
});
assertThat(inTransaction(() -> quota.reservedBytes(scope))).isZero();
assertThat(inTransaction(() -> quota.committedBytes(scope))).isZero();
}
@Test
void reclaimingDrawsCommittedBytesDownAcrossRows() {
FileRecord record = insertRecord();
UploadSession session = insertSession(record.fileId());
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
inTransaction(
() -> {
quotaCommit.commit(session, 300);
quotaCommit.commit(session, 700);
return null;
});
inTransaction(
() -> {
quotaReclaim.reclaim(scope, 800);
return null;
});
assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(200);
}
@Test
void reclaimingMoreThanIsRecordedStopsAtZeroRatherThanGoingNegative() {
FileRecord record = insertRecord();
UploadSession session = insertSession(record.fileId());
QuotaScope scope = QuotaScope.ofNamespace(record.namespace().value());
inTransaction(
() -> {
quotaCommit.commit(session, 100);
return null;
});
inTransaction(
() -> {
quotaReclaim.reclaim(scope, 5_000);
return null;
});
assertThat(inTransaction(() -> quota.committedBytes(scope))).isZero();
}
@Test
void theLedgerReportsAKeyAsReferencedOnlyWhileARecordNamesIt() {
ContentKey key = contentKey();
assertThat(inTransaction(() -> ledger.isReferenced(key))).isFalse();
FileRecord record = insertRecord();
inTransaction(() -> publish(record, key));
assertThat(inTransaction(() -> ledger.isReferenced(key))).isTrue();
}
@Test
void theStagingLocatorReturnsTheNewestSessionForAFile() {
FileRecord record = insertRecord();
UploadSession older = insertSession(record.fileId());
UploadSession newer = insertSession(record.fileId());
Optional<UploadId> located = inTransaction(() -> stagingLocator.locate(record.fileId()));
assertThat(located).isPresent();
assertThat(located.get()).isIn(older.uploadId(), newer.uploadId());
assertThat(inTransaction(() -> stagingLocator.locate(FileId.of(UUID.randomUUID())))).isEmpty();
}
private FileRecord insertRecord() {
return inTransaction(
() ->
files.insert(
new FileRecordDraft(
FileId.of(UUID.randomUUID()),
StorageNamespace.of("tenant-a"),
"report.bin",
Optional.of("application/octet-stream"),
OptionalLong.of(1_000))));
}
private FileRecord publish(FileRecord record, ContentKey key) {
FileRecord uploading =
files.transition(
record.fileId(),
record.version(),
FileState.CREATED,
FileState.UPLOADING,
FileRecordMutation.none());
FileRecord uploaded =
files.transition(
uploading.fileId(),
uploading.version(),
FileState.UPLOADING,
FileState.UPLOADED,
FileRecordMutation.uploaded(10, "b".repeat(64)));
FileRecord verifying =
files.transition(
uploaded.fileId(),
uploaded.version(),
FileState.UPLOADED,
FileState.VERIFYING,
FileRecordMutation.none());
return files.transition(
verifying.fileId(),
verifying.version(),
FileState.VERIFYING,
FileState.READY,
FileRecordMutation.publishAt(
key, 10, "b".repeat(64), "\"" + "b".repeat(64) + "\"", Instant.now()));
}
private UploadSession insertSession(FileId fileId) {
return inTransaction(
() ->
uploads.create(
new UploadSessionDraft(
UploadId.of(UUID.randomUUID()),
fileId,
UploadProtocol.RAW,
OptionalLong.of(1_000),
Instant.now().plus(Duration.ofHours(1)))));
}
private static ContentKey contentKey() {
String flat = UUID.randomUUID().toString().replace("-", "");
return ContentKey.of(flat.substring(0, 2) + '/' + flat.substring(2, 4) + '/' + flat);
}
private static <T> T inTransaction(Supplier<T> action) {
return transactions.execute(status -> action.get());
}
private static int rowCount(String table) {
Integer count = jdbc.queryForObject("SELECT count(*) FROM " + table, Integer.class);
return count == null ? 0 : count;
}
private static int statusCounts(String table, String status) {
Integer count =
jdbc.queryForObject(
"SELECT count(*) FROM " + table + " WHERE status = ?", Integer.class, status);
return count == null ? 0 : count;
}
private static void migrate(String location, String historyTable) {
Flyway.configure()
.dataSource(postgres.dataSource())
.locations(location)
.table(historyTable)
.baselineOnMigrate(false)
.outOfOrder(false)
.load()
.migrate();
}
private static void migrateIndependent(
String location, String historyTable, String baselineDescription) {
Flyway flyway =
Flyway.configure()
.dataSource(postgres.dataSource())
.locations(location)
.table(historyTable)
.baselineVersion("0")
.baselineDescription(baselineDescription)
.baselineOnMigrate(false)
.outOfOrder(false)
.load();
flyway.baseline();
flyway.migrate();
}
@Test
void analysisProbeConcurrentRecoveryEnqueueIsNotAnAtomicUpsert() throws Exception {
FileRecord record = insertRecord();
var pool = java.util.concurrent.Executors.newFixedThreadPool(2);
try {
var start = new java.util.concurrent.CountDownLatch(1);
java.util.concurrent.Callable<Throwable> action =
() -> {
start.await();
try {
inTransaction(
() -> {
recoveryQueue.enqueue(record.fileId(), "CONCURRENT");
return null;
});
return null;
} catch (Throwable t) {
return t;
}
};
var first = pool.submit(action);
var second = pool.submit(action);
start.countDown();
Throwable a = first.get();
Throwable b = second.get();
long failures = java.util.stream.Stream.of(a, b).filter(java.util.Objects::nonNull).count();
System.out.println("fileserverRecovery.concurrentFailures=" + failures);
System.out.println("fileserverRecovery.first=" + (a == null ? "SUCCESS" : a.getClass().getName()));
System.out.println("fileserverRecovery.second=" + (b == null ? "SUCCESS" : b.getClass().getName()));
System.out.println("fileserverRecovery.rowCount=" + rowCount("fs_recovery_item"));
assertThat(failures).isEqualTo(1);
assertThat(rowCount("fs_recovery_item")).isEqualTo(1);
} finally {
pool.shutdownNow();
}
}
}
@@ -0,0 +1,42 @@
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
command=./gradlew :adapter:outbound:persistence-jpa:postgresqlFileserverMigrationIntegrationTest --tests dev.caskeleton.adapter.outbound.persistence.readiness.PostgreSqlFileserverMigrationIntegrationTest.analysisProbeRevisionTwoIsAcceptedWithoutRevisionThreeAndFourColumns --console=plain --no-daemon
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.0.0/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
Daemon will be stopped at the end of the build
> Task :build-logic:extractPluginRequests UP-TO-DATE
> Task :build-logic:generatePluginAdapters UP-TO-DATE
> Task :build-logic:compileJava UP-TO-DATE
> Task :build-logic:compileGroovy UP-TO-DATE
> Task :build-logic:compileGroovyPlugins UP-TO-DATE
> Task :build-logic:pluginDescriptors UP-TO-DATE
> Task :build-logic:processResources UP-TO-DATE
> Task :build-logic:classes UP-TO-DATE
> Task :build-logic:jar UP-TO-DATE
> Task :shared-contract:compileJava UP-TO-DATE
> Task :shared-contract:processResources UP-TO-DATE
> Task :shared-contract:classes UP-TO-DATE
> Task :shared-contract:jar UP-TO-DATE
> Task :application-core:compileJava UP-TO-DATE
> Task :application-core:processResources NO-SOURCE
> Task :application-core:classes UP-TO-DATE
> Task :application-core:jar UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:compileJava UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:processResources UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:classes UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:compileTestkitJava UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:processTestkitResources NO-SOURCE
> Task :adapter:outbound:persistence-jpa:testkitClasses UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:compilePostgresqlIntegrationTestJava
> Task :adapter:outbound:persistence-jpa:processPostgresqlIntegrationTestResources UP-TO-DATE
> Task :adapter:outbound:persistence-jpa:postgresqlIntegrationTestClasses
> Task :adapter:outbound:persistence-jpa:postgresqlFileserverMigrationIntegrationTest
BUILD SUCCESSFUL in 16s
19 actionable tasks: 2 executed, 17 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
=== ASSERTED OBSERVATION (from Gradle test XML) ===
fileserverSchemaV2.featureRevision=2
fileserverSchemaV2.activationAccepted=true
fileserverSchemaV2.cleanupClaimToken=false
fileserverSchemaV2.uploadLifecycleState=false
source restored after probe: yes
@@ -0,0 +1,290 @@
package dev.caskeleton.adapter.outbound.persistence.readiness;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.UUID;
import org.flywaydb.core.Flyway;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.jdbc.core.JdbcTemplate;
/**
* Real-PostgreSQL proof that the Fileserver metadata schema exists with the optimistic-locking and
* READY-completeness guarantees the design requires.
*/
class PostgreSqlFileserverMigrationIntegrationTest {
private static PostgreSqlReadinessSupport postgres;
private static JdbcTemplate jdbc;
@BeforeAll
static void startAndMigratePostgreSql() {
PostgreSqlReadinessSupport.assertDockerAvailable();
postgres = PostgreSqlReadinessSupport.start();
migrate("classpath:db/migration/postgresql", "flyway_schema_history");
migrateIndependent(
"classpath:db/migration/jpa/core", "flyway_jpa_core_history", "explicit-jpa-core-adoption");
migrateIndependent(
"classpath:db/migration/jpa/fileserver",
"flyway_jpa_fileserver_history",
"explicit-jpa-fileserver-adoption");
jdbc = new JdbcTemplate(postgres.dataSource());
}
@AfterAll
static void stopPostgreSql() {
if (postgres != null) {
postgres.close();
}
}
@BeforeEach
void clearRows() {
jdbc.update("delete from fs_cleanup_item");
jdbc.update("delete from fs_quota_reservation");
jdbc.update("delete from fs_verification_result");
jdbc.update("delete from fs_upload_session");
jdbc.update("delete from fs_file");
}
@Test
void createsFileserverTablesAndVersionColumns() throws Exception {
try (Connection connection = postgres.dataSource().getConnection()) {
assertThat(columnExists(connection, "fs_file", "version")).isTrue();
assertThat(columnExists(connection, "fs_file", "content_key")).isTrue();
assertThat(columnExists(connection, "fs_file", "strong_etag")).isTrue();
assertThat(columnExists(connection, "fs_upload_session", "lease_until")).isTrue();
assertThat(columnExists(connection, "fs_upload_session", "lease_token")).isTrue();
assertThat(columnExists(connection, "fs_upload_session", "committed_offset")).isTrue();
assertThat(columnExists(connection, "fs_quota_reservation", "reserved_bytes")).isTrue();
assertThat(columnExists(connection, "fs_verification_result", "verdict")).isTrue();
assertThat(columnExists(connection, "fs_cleanup_item", "next_attempt_at")).isTrue();
}
}
@Test
void registersTheCapabilityStreamAsInstalledButInactive() {
String lifecycle =
jdbc.queryForObject(
"select lifecycle_state from capability_schema_registry where capability_id = ?",
String.class,
"jpa-fileserver-metadata-v1");
assertThat(lifecycle).isEqualTo("INSTALLED_INACTIVE");
}
@Test
void readyRowsMustCarryCompletePublishedIdentity() {
UUID fileId = UUID.randomUUID();
assertThatThrownBy(
() ->
jdbc.update(
"""
insert into fs_file(
file_id, namespace, state, original_name, version, created_at, updated_at)
values (?, ?, 'READY', ?, 0, ?, ?)
""",
fileId,
"tenant-a",
"report.bin",
now(),
now()))
.isInstanceOf(DataIntegrityViolationException.class);
}
@Test
void contentKeyIsUniqueAcrossFilesButManyRowsMayHaveNone() {
insertCreated(UUID.randomUUID());
insertCreated(UUID.randomUUID());
UUID first = UUID.randomUUID();
UUID second = UUID.randomUUID();
insertCreated(first);
insertCreated(second);
jdbc.update(
"update fs_file set content_key = ? where file_id = ?", "ab/cd/key0000000001", first);
assertThatThrownBy(
() ->
jdbc.update(
"update fs_file set content_key = ? where file_id = ?",
"ab/cd/key0000000001",
second))
.isInstanceOf(DataIntegrityViolationException.class);
}
@Test
void aLeaseIsAllOrNothing() {
UUID fileId = UUID.randomUUID();
UUID uploadId = UUID.randomUUID();
insertCreated(fileId);
insertUpload(uploadId, fileId);
assertThatThrownBy(
() ->
jdbc.update(
"update fs_upload_session set lease_owner = ? where upload_id = ?",
"node-a",
uploadId))
.isInstanceOf(DataIntegrityViolationException.class);
}
@Test
void committedOffsetNeverExceedsTheDeclaredLength() {
UUID fileId = UUID.randomUUID();
UUID uploadId = UUID.randomUUID();
insertCreated(fileId);
insertUpload(uploadId, fileId);
jdbc.update("update fs_upload_session set expected_length = 10 where upload_id = ?", uploadId);
assertThatThrownBy(
() ->
jdbc.update(
"update fs_upload_session set committed_offset = 11 where upload_id = ?",
uploadId))
.isInstanceOf(DataIntegrityViolationException.class);
}
private void insertCreated(UUID fileId) {
jdbc.update(
"""
insert into fs_file(
file_id, namespace, state, original_name, version, created_at, updated_at)
values (?, ?, 'CREATED', ?, 0, ?, ?)
""",
fileId,
"tenant-a",
"report.bin",
now(),
now());
}
private void insertUpload(UUID uploadId, UUID fileId) {
jdbc.update(
"""
insert into fs_upload_session(
upload_id, file_id, protocol, committed_offset, expires_at, version,
created_at, updated_at)
values (?, ?, 'RAW', 0, ?, 0, ?, ?)
""",
uploadId,
fileId,
now().plusSeconds(3600),
now(),
now());
}
private static OffsetDateTime now() {
return OffsetDateTime.now(ZoneOffset.UTC);
}
private static boolean columnExists(Connection connection, String table, String column)
throws SQLException {
try (ResultSet columns = connection.getMetaData().getColumns(null, null, table, column)) {
return columns.next();
}
}
private static void migrate(String location, String historyTable) {
Flyway.configure()
.dataSource(postgres.dataSource())
.locations(location)
.table(historyTable)
.baselineOnMigrate(false)
.outOfOrder(false)
.load()
.migrate();
}
private static void migrateIndependent(
String location, String historyTable, String baselineDescription) {
Flyway flyway =
Flyway.configure()
.dataSource(postgres.dataSource())
.locations(location)
.table(historyTable)
.baselineVersion("0")
.baselineDescription(baselineDescription)
.baselineOnMigrate(false)
.outOfOrder(false)
.load();
flyway.baseline();
flyway.migrate();
}
@Test
void analysisProbeRevisionTwoIsAcceptedWithoutRevisionThreeAndFourColumns() throws Exception {
try (PostgreSqlReadinessSupport v2 = PostgreSqlReadinessSupport.start()) {
Flyway.configure()
.dataSource(v2.dataSource())
.locations("classpath:db/migration/postgresql")
.table("flyway_schema_history")
.baselineOnMigrate(false)
.outOfOrder(false)
.load()
.migrate();
Flyway core =
Flyway.configure()
.dataSource(v2.dataSource())
.locations("classpath:db/migration/jpa/core")
.table("flyway_jpa_core_history")
.baselineVersion("0")
.baselineDescription("analysis-core")
.baselineOnMigrate(false)
.outOfOrder(false)
.load();
core.baseline();
core.migrate();
Flyway fileserver =
Flyway.configure()
.dataSource(v2.dataSource())
.locations("classpath:db/migration/jpa/fileserver")
.table("flyway_jpa_fileserver_history")
.baselineVersion("0")
.baselineDescription("analysis-fileserver-v2")
.baselineOnMigrate(false)
.outOfOrder(false)
.target("2")
.load();
fileserver.baseline();
fileserver.migrate();
JdbcTemplate v2Jdbc = new JdbcTemplate(v2.dataSource());
v2Jdbc.update(
"update capability_schema_registry set lifecycle_state = 'ACTIVE' where capability_id = 'jpa-fileserver-metadata-v1'");
new dev.caskeleton.adapter.outbound.persistence.fileserver.FileserverSchemaActivation(v2Jdbc)
.requireActive();
int revision =
v2Jdbc.queryForObject(
"select feature_revision from capability_schema_registry where capability_id = 'jpa-fileserver-metadata-v1'",
Integer.class);
boolean hasCleanupClaimToken;
boolean hasUploadLifecycle;
try (Connection connection = v2.dataSource().getConnection()) {
hasCleanupClaimToken = columnExists(connection, "fs_cleanup_item", "claim_token");
hasUploadLifecycle = columnExists(connection, "fs_upload_session", "lifecycle_state");
}
System.out.println("fileserverSchemaV2.featureRevision=" + revision);
System.out.println("fileserverSchemaV2.activationAccepted=true");
System.out.println("fileserverSchemaV2.cleanupClaimToken=" + hasCleanupClaimToken);
System.out.println("fileserverSchemaV2.uploadLifecycleState=" + hasUploadLifecycle);
assertThat(revision).isEqualTo(2);
assertThat(hasCleanupClaimToken).isFalse();
assertThat(hasUploadLifecycle).isFalse();
}
}
}

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