merge: integrate JPA production capability

# Conflicts:
#	.github/ci-gate-matrix.yml
#	.github/scripts/verify-gate-matrix.sh
#	.github/workflows/ci-quality-gates.yml
#	src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java
This commit is contained in:
donghyeon-ka
2026-07-31 23:57:29 +09:00
142 changed files with 13206 additions and 325 deletions
+609
View File
@@ -648,6 +648,615 @@ tasks.register('verifyCleanArchitectureDependencies') {
}
}
Set<String> expectedJpaReadinessCardIds = [
'jpa-observability-lifecycle',
'jpa-security-baseline',
'jpa-flyway-migration',
'jpa-transaction-runtime',
'jpa-aggregate-store',
'jpa-query-model',
'jpa-primary-foundation',
'jpa-idempotency-owner-safe-v2',
'jpa-outbox-storage-v2',
'jpa-outbox-polling-delivery-v2',
'jpa-outbox-cdc-retention-v1',
'jpa-inbox-same-store-v1',
'jpa-primary-replica',
'jpa-tenant-discriminator-rls',
'jpa-jdbc-efficiency-coordination'
] as Set
Set<String> expectedJpaOwnedMigrationCardIds = [
'jpa-flyway-migration',
'jpa-idempotency-owner-safe-v2',
'jpa-outbox-storage-v2',
'jpa-outbox-polling-delivery-v2',
'jpa-inbox-same-store-v1',
'jpa-tenant-discriminator-rls',
'jpa-jdbc-efficiency-coordination'
] as Set
Closure<List<String>> validateJpaReadinessRegistry = {
Map<String, Object> registry,
String rawRegistry,
Closure<Boolean> taskExists ->
List<String> violations = []
Set<String> rootKeys = registry.keySet().collect { it as String }.toSet()
Set<String> expectedRootKeys = ['schema-version', 'legacy-adoption', 'cards'] as Set
if (rootKeys != expectedRootKeys) {
violations << "root keys must be exactly ${expectedRootKeys}; got ${rootKeys}"
}
if (registry['schema-version'] != 1) {
violations << "schema-version must be integer 1; got ${registry['schema-version']}"
}
Map<String, Object> legacy = registry['legacy-adoption'] instanceof Map
? registry['legacy-adoption'] as Map<String, Object>
: [:]
Set<String> expectedLegacyKeys = [
'state',
'location',
'history-table',
'immutable-applied-versions',
'allowed-origin'
] as Set
if (legacy.keySet().collect { it as String }.toSet() != expectedLegacyKeys) {
violations << "legacy-adoption keys must be exactly ${expectedLegacyKeys}"
}
if (legacy.state != 'transition-only') {
violations << "legacy-adoption.state must be transition-only"
}
if (legacy.location != 'db/migration/postgresql') {
violations << "legacy-adoption.location must be db/migration/postgresql"
}
if (legacy['history-table'] != 'flyway_schema_history') {
violations << "legacy-adoption.history-table must be flyway_schema_history"
}
if (legacy['immutable-applied-versions'] != [1, 3, 4, 5]) {
violations << "legacy-adoption immutable versions must be exactly [1, 3, 4, 5]"
}
if (legacy['allowed-origin'] != 'LEGACY_ADOPTED') {
violations << "legacy-adoption.allowed-origin must be LEGACY_ADOPTED"
}
Map<String, Object> cards = registry.cards instanceof Map
? registry.cards as Map<String, Object>
: [:]
Set<String> actualCardIds = cards.keySet().collect { it as String }.toSet()
Set<String> missingCards = expectedJpaReadinessCardIds - actualCardIds
Set<String> unknownCards = actualCardIds - expectedJpaReadinessCardIds
if (!missingCards.isEmpty()) {
violations << "missing card ids ${missingCards.toSorted()}"
}
if (!unknownCards.isEmpty()) {
violations << "unknown card ids ${unknownCards.toSorted()}"
}
List<String> rawCardKeys = []
def rawCardKeyMatcher = rawRegistry =~ /"(?<card>jpa-[a-z0-9.-]+)"\s*:/
while (rawCardKeyMatcher.find()) {
rawCardKeys << rawCardKeyMatcher.group('card')
}
Set<String> duplicateRawCardKeys = rawCardKeys.countBy { it }.findAll {
String ignored, Integer count -> count > 1
}.keySet()
if (!duplicateRawCardKeys.isEmpty()) {
violations << "duplicate raw card keys ${duplicateRawCardKeys.toSorted()}"
}
Set<String> allowedCardKeys = [
'state',
'schema-stream',
'prerequisites',
'external-prerequisites',
'readiness-task',
'support-tasks',
'required-evidence',
'evidence',
'dispatch-modes',
'migration'
] as Set
Set<String> allowedStates = ['selected', 'implemented-candidate', 'not-implemented'] as Set
Set<String> allowedSchemaStreams = ['none', 'owned', 'contributes-to-core'] as Set
Map<String, String> taskOwners = [:]
Map<String, String> migrationLocationOwners = [:]
Map<String, String> migrationHistoryOwners = [:]
Map<String, String> evidenceSelectorOwners = [:]
Set<String> actualOwnedMigrationCards = []
cards.each { String cardId, Object rawCard ->
if (!(rawCard instanceof Map)) {
violations << "${cardId}: card value must be an object"
return
}
Map<String, Object> card = rawCard as Map<String, Object>
Set<String> unknownKeys = card.keySet().collect { it as String }.toSet() - allowedCardKeys
if (!unknownKeys.isEmpty()) {
violations << "${cardId}: unknown keys ${unknownKeys.toSorted()}"
}
String state = card.state as String
String schemaStream = card['schema-stream'] as String
if (!allowedStates.contains(state)) {
violations << "${cardId}: invalid state '${state}'"
}
if (!allowedSchemaStreams.contains(schemaStream)) {
violations << "${cardId}: invalid schema-stream '${schemaStream}'"
}
if (!(card.prerequisites instanceof List)) {
violations << "${cardId}: prerequisites must be a list"
}
List<String> prerequisites = card.prerequisites instanceof List
? (card.prerequisites as List).collect { it as String }
: []
if (prerequisites.toSet().size() != prerequisites.size()) {
violations << "${cardId}: duplicate prerequisites ${prerequisites}"
}
prerequisites.each { String prerequisite ->
if (!cards.containsKey(prerequisite)) {
violations << "${cardId}: unknown prerequisite '${prerequisite}'"
} else if (state == 'selected' &&
((cards[prerequisite] as Map).state as String) != 'selected') {
violations << "${cardId}: selected card requires non-selected '${prerequisite}'"
}
}
String readinessTask = card['readiness-task'] as String
if (readinessTask == null || !readinessTask.startsWith(':')) {
violations << "${cardId}: readiness-task must be an absolute Gradle task path"
}
List<String> supportTasks = card['support-tasks'] instanceof List
? (card['support-tasks'] as List).collect { it as String }
: []
if (supportTasks.toSet().size() != supportTasks.size()) {
violations << "${cardId}: duplicate support-tasks ${supportTasks}"
}
([readinessTask] + supportTasks).findAll { it != null }.each { String taskPath ->
if (!taskPath.startsWith(':')) {
violations << "${cardId}: task '${taskPath}' must be an absolute Gradle task path"
return
}
String previousOwner = taskOwners.putIfAbsent(taskPath, cardId)
if (previousOwner != null) {
violations << "duplicate task '${taskPath}' owned by ${previousOwner} and ${cardId}"
}
if (state == 'selected' && !taskExists(taskPath)) {
violations << "${cardId}: selected task does not exist '${taskPath}'"
}
}
List<String> requiredEvidence = card['required-evidence'] instanceof List
? (card['required-evidence'] as List).collect { it as String }
: []
if (requiredEvidence.isEmpty()) {
violations << "${cardId}: required-evidence must be a non-empty list"
} else {
if (requiredEvidence.toSet().size() != requiredEvidence.size()) {
violations << "${cardId}: duplicate required-evidence ${requiredEvidence}"
}
if (!requiredEvidence.contains('no-skip')) {
violations << "${cardId}: required-evidence must include no-skip"
}
}
Object migrationNode = card.migration
Set<String> allowedEvidenceClaims = requiredEvidence
.findAll { String requirement -> requirement != 'no-skip' }
.toSet()
Map<String, Object> migrationForEvidence = migrationNode instanceof Map
? migrationNode as Map<String, Object>
: [:]
Object lifecycleEvidenceNode = migrationForEvidence['lifecycle-evidence']
if (lifecycleEvidenceNode instanceof List) {
(lifecycleEvidenceNode as List).each {
Object lifecycle ->
allowedEvidenceClaims <<
"migration-lifecycle:${lifecycle as String}".toString()
}
}
Object evidenceNode = card.evidence
if (state == 'not-implemented') {
if (evidenceNode != null) {
violations << "${cardId}: not-implemented card forbids evidence"
}
} else if (!(evidenceNode instanceof Map)) {
violations << "${cardId}: active card requires evidence"
} else {
Map<String, Object> evidence = evidenceNode as Map<String, Object>
Set<String> evidenceKeys = evidence.keySet().collect { it as String }.toSet()
Set<String> expectedEvidenceKeys = ['scenarios', 'task-claims'] as Set
if (evidenceKeys != expectedEvidenceKeys) {
violations << "${cardId}: evidence keys must be exactly ${expectedEvidenceKeys}"
}
List<Object> scenarios = evidence.scenarios instanceof List
? evidence.scenarios as List<Object>
: []
if (!(evidence.scenarios instanceof List)) {
violations << "${cardId}: evidence scenarios must be a list"
}
List<Object> taskClaims = evidence['task-claims'] instanceof List
? evidence['task-claims'] as List<Object>
: []
if (!(evidence['task-claims'] instanceof List)) {
violations << "${cardId}: evidence task-claims must be a list"
}
if (scenarios.isEmpty() && taskClaims.isEmpty()) {
violations << "${cardId}: evidence must contain a scenario or task claim"
}
scenarios.eachWithIndex { Object rawScenario, int index ->
if (!(rawScenario instanceof Map)) {
violations << "${cardId}: evidence scenario ${index} must be an object"
return
}
Map<String, Object> scenario = rawScenario as Map<String, Object>
Set<String> scenarioKeys =
scenario.keySet().collect { it as String }.toSet()
if (scenarioKeys != ['selector', 'covers'] as Set) {
violations << "${cardId}: evidence scenario ${index} has invalid keys ${scenarioKeys}"
}
String selector = scenario.selector as String
if (selector == null ||
!(selector ==~ /dev\.caskeleton\.[A-Za-z0-9_.]+\#[A-Za-z][A-Za-z0-9_]*/)) {
violations << "${cardId}: invalid evidence selector '${selector}'"
} else {
String previousOwner = evidenceSelectorOwners.putIfAbsent(selector, cardId)
if (previousOwner != null) {
violations << "duplicate evidence selector '${selector}' owned by " +
"${previousOwner} and ${cardId}"
}
}
List<String> covers = scenario.covers instanceof List
? (scenario.covers as List).collect { it as String }
: []
if (covers.isEmpty()) {
violations << "${cardId}: evidence scenario ${index} covers must be non-empty"
}
if (covers.toSet().size() != covers.size()) {
violations << "${cardId}: evidence scenario ${index} has duplicate covers ${covers}"
}
covers.each { String claim ->
if (!allowedEvidenceClaims.contains(claim)) {
violations << "${cardId}: evidence covers unknown requirement '${claim}'"
}
}
}
Set<String> ownedTasks = ([readinessTask] + supportTasks)
.findAll { it != null }
.toSet()
taskClaims.eachWithIndex { Object rawClaim, int index ->
if (!(rawClaim instanceof Map)) {
violations << "${cardId}: evidence task claim ${index} must be an object"
return
}
Map<String, Object> claim = rawClaim as Map<String, Object>
Set<String> claimKeys = claim.keySet().collect { it as String }.toSet()
if (claimKeys != ['task', 'covers'] as Set) {
violations << "${cardId}: evidence task claim ${index} has invalid keys ${claimKeys}"
}
String taskPath = claim.task as String
if (!ownedTasks.contains(taskPath)) {
violations << "${cardId}: evidence task claim is not owned by card '${taskPath}'"
}
List<String> covers = claim.covers instanceof List
? (claim.covers as List).collect { it as String }
: []
if (covers.isEmpty()) {
violations << "${cardId}: evidence task claim ${index} covers must be non-empty"
}
if (covers.toSet().size() != covers.size()) {
violations << "${cardId}: evidence task claim ${index} has duplicate covers ${covers}"
}
covers.each { String evidenceClaim ->
if (!allowedEvidenceClaims.contains(evidenceClaim)) {
violations << "${cardId}: evidence covers unknown requirement '${evidenceClaim}'"
}
}
}
}
if (schemaStream == 'owned') {
actualOwnedMigrationCards << cardId
if (!(migrationNode instanceof Map)) {
violations << "${cardId}: owned schema-stream requires migration"
}
} else if (migrationNode != null) {
violations << "${cardId}: schema-stream ${schemaStream} forbids migration"
}
if (migrationNode instanceof Map) {
Map<String, Object> migration = migrationNode as Map<String, Object>
Set<String> expectedMigrationKeys = [
'location',
'history-table',
'required-core-epoch',
'feature-revision',
'lifecycle-evidence'
] as Set
Set<String> migrationKeys = migration.keySet().collect { it as String }.toSet()
if (migrationKeys != expectedMigrationKeys) {
violations << "${cardId}: migration keys must be exactly ${expectedMigrationKeys}"
}
String location = migration.location as String
String historyTable = migration['history-table'] as String
if (location == null || !(location ==~ /db\/migration\/jpa\/[a-z0-9-]+/)) {
violations << "${cardId}: invalid migration location '${location}'"
} else {
String previousOwner = migrationLocationOwners.putIfAbsent(location, cardId)
if (previousOwner != null) {
violations << "duplicate migration location '${location}' for ${previousOwner} and ${cardId}"
}
}
if (historyTable == null || !(historyTable ==~ /flyway_jpa_[a-z0-9_]+_history/)) {
violations << "${cardId}: invalid migration history-table '${historyTable}'"
} else {
String previousOwner = migrationHistoryOwners.putIfAbsent(historyTable, cardId)
if (previousOwner != null) {
violations << "duplicate migration history-table '${historyTable}' for ${previousOwner} and ${cardId}"
}
}
Object coreEpoch = migration['required-core-epoch']
Object featureRevision = migration['feature-revision']
if (!(coreEpoch instanceof Integer) || (coreEpoch as Integer) < 0) {
violations << "${cardId}: required-core-epoch must be a non-negative integer"
}
if (!(featureRevision instanceof Integer) || (featureRevision as Integer) <= 0) {
violations << "${cardId}: feature-revision must be a positive integer"
}
List<String> lifecycleEvidence = migration['lifecycle-evidence'] instanceof List
? (migration['lifecycle-evidence'] as List).collect { it as String }
: []
if (lifecycleEvidence.isEmpty()) {
violations << "${cardId}: lifecycle-evidence must be a non-empty list"
} else if (lifecycleEvidence.toSet().size() != lifecycleEvidence.size()) {
violations << "${cardId}: duplicate lifecycle-evidence ${lifecycleEvidence}"
}
}
if (card['external-prerequisites'] != null) {
if (!(card['external-prerequisites'] instanceof List)) {
violations << "${cardId}: external-prerequisites must be a list"
} else {
(card['external-prerequisites'] as List).eachWithIndex {
Object rawExternal, int index ->
if (!(rawExternal instanceof Map)) {
violations << "${cardId}: external prerequisite ${index} must be an object"
return
}
Map<String, Object> external = rawExternal as Map<String, Object>
Set<String> externalKeys = external.keySet()
.collect { it as String }
.toSet()
if (externalKeys != ['registry', 'card-id', 'minimum-readiness'] as Set) {
violations << "${cardId}: external prerequisite ${index} has invalid keys ${externalKeys}"
}
if (!((external.registry as String)?.startsWith('src/config/'))) {
violations << "${cardId}: external prerequisite ${index} has invalid registry"
}
if (!((external['card-id'] as String) ==~ /[a-z0-9.-]+/)) {
violations << "${cardId}: external prerequisite ${index} has invalid card-id"
}
if (!((external['minimum-readiness'] as String) ==~ /R[0-3]/)) {
violations << "${cardId}: external prerequisite ${index} has invalid minimum-readiness"
}
}
}
}
}
if (actualOwnedMigrationCards != expectedJpaOwnedMigrationCardIds) {
violations << "owned migration cards must be exactly ${expectedJpaOwnedMigrationCardIds}; " +
"got ${actualOwnedMigrationCards}"
}
Map<String, Integer> visitState = [:].withDefault { 0 }
Closure<Void> visitCard
visitCard = { String cardId ->
if (visitState[cardId] == 1) {
violations << "readiness prerequisite cycle includes '${cardId}'"
return
}
if (visitState[cardId] == 2 || !cards.containsKey(cardId)) {
return
}
visitState[cardId] = 1
Map<String, Object> card = cards[cardId] as Map<String, Object>
if (card.prerequisites instanceof List) {
(card.prerequisites as List).each { Object prerequisite ->
visitCard(prerequisite as String)
}
}
visitState[cardId] = 2
}
cards.keySet().each { Object cardId -> visitCard(cardId as String) }
boolean pollingSelected =
((cards['jpa-outbox-polling-delivery-v2'] as Map)?.state as String) == 'selected'
boolean cdcSelected =
((cards['jpa-outbox-cdc-retention-v1'] as Map)?.state as String) == 'selected'
if (pollingSelected && cdcSelected) {
violations << 'polling and CDC outbox delivery cards cannot both be selected'
}
violations
}
Closure<Boolean> jpaTaskExists = { String absoluteTaskPath ->
int separator = absoluteTaskPath.lastIndexOf(':')
if (separator < 0 || separator == absoluteTaskPath.length() - 1) {
return false
}
String projectPath = separator == 0 ? ':' : absoluteTaskPath.substring(0, separator)
String taskName = absoluteTaskPath.substring(separator + 1)
Project targetProject = rootProject.findProject(projectPath)
targetProject != null && targetProject.tasks.findByName(taskName) != null
}
def verifyJpaReadinessRegistryContract = tasks.register('verifyJpaReadinessRegistryContract') {
group = 'verification'
description = 'Mutation-tests the fail-closed JPA readiness registry validator.'
File registryFile = file("${rootProject.projectDir}/config/jpa/readiness-cards.yaml")
inputs.file(registryFile)
doLast {
String raw = registryFile.getText('UTF-8')
Map<String, Object> baseline = new JsonSlurper().parseText(raw) as Map<String, Object>
Closure<Map<String, Object>> copyRegistry = {
new JsonSlurper().parseText(JsonOutput.toJson(baseline)) as Map<String, Object>
}
Closure<Void> expectViolation = {
String scenario,
String expectedText,
Closure<Void> mutation,
Closure<Boolean> taskExists = { String ignored -> true } ->
Map<String, Object> candidate = copyRegistry()
mutation(candidate)
List<String> candidateViolations = validateJpaReadinessRegistry(
candidate,
JsonOutput.toJson(candidate),
taskExists)
if (!candidateViolations.any { String violation ->
violation.contains(expectedText)
}) {
throw new GradleException(
"verifyJpaReadinessRegistryContract: scenario '${scenario}' did not " +
"produce '${expectedText}'; got ${candidateViolations}")
}
}
expectViolation('unknown-card', 'unknown card ids', { Map<String, Object> candidate ->
(candidate.cards as Map)['jpa-primary-foundation-alias'] =
(candidate.cards as Map)['jpa-primary-foundation']
})
expectViolation('duplicate-task', 'duplicate task', { Map<String, Object> candidate ->
((candidate.cards as Map)['jpa-security-baseline'] as Map)['readiness-task'] =
((candidate.cards as Map)['jpa-observability-lifecycle'] as Map)['readiness-task']
})
expectViolation('missing-prerequisite', 'unknown prerequisite', {
Map<String, Object> candidate ->
((candidate.cards as Map)['jpa-security-baseline'] as Map).prerequisites =
['jpa-does-not-exist']
})
expectViolation('cycle', 'prerequisite cycle', { Map<String, Object> candidate ->
((candidate.cards as Map)['jpa-observability-lifecycle'] as Map).prerequisites =
['jpa-security-baseline']
})
expectViolation('duplicate-location', 'duplicate migration location', {
Map<String, Object> candidate ->
(((candidate.cards as Map)['jpa-idempotency-owner-safe-v2'] as Map).migration
as Map).location = 'db/migration/jpa/core'
})
expectViolation(
'missing-selected-task',
'selected task does not exist',
{ Map<String, Object> ignored -> },
{ String taskPath ->
taskPath !=
':adapter:outbound:persistence-jpa:postgresqlLifecycleIntegrationTest'
})
expectViolation('missing-active-evidence', 'active card requires evidence', {
Map<String, Object> candidate ->
((candidate.cards as Map)['jpa-observability-lifecycle'] as Map)
.remove('evidence')
})
expectViolation('unknown-evidence-requirement', 'evidence covers unknown requirement', {
Map<String, Object> candidate ->
((candidate.cards as Map)['jpa-observability-lifecycle'] as Map).evidence = [
scenarios: [[
selector: 'dev.caskeleton.ReadinessTest#startsPostgreSql',
covers: ['not-a-card-requirement']
]],
'task-claims': []
]
})
expectViolation('duplicate-evidence-selector', 'duplicate evidence selector', {
Map<String, Object> candidate ->
Map<String, Object> card =
(candidate.cards as Map)['jpa-observability-lifecycle'] as Map<String, Object>
card.evidence = [
scenarios: [
[
selector: 'dev.caskeleton.ReadinessTest#startsPostgreSql',
covers: ['real-postgresql']
],
[
selector: 'dev.caskeleton.ReadinessTest#startsPostgreSql',
covers: ['lifecycle']
]
],
'task-claims': []
]
})
expectViolation('unknown-evidence-task', 'evidence task claim is not owned by card', {
Map<String, Object> candidate ->
((candidate.cards as Map)['jpa-primary-foundation'] as Map).evidence = [
scenarios: [],
'task-claims': [[
task: ':test',
covers: ['architecture']
]]
]
})
logger.lifecycle(
'verifyJpaReadinessRegistryContract: OK — unknown card, duplicate task, ' +
'missing prerequisite, cycle, duplicate migration ownership, missing ' +
'selected task, and malformed evidence ownership all fail closed.')
}
}
def verifyJpaReadinessRegistry = tasks.register('verifyJpaReadinessRegistry') {
group = 'verification'
description = 'Validates the JPA readiness card, prerequisite, task, and migration registry.'
dependsOn verifyJpaReadinessRegistryContract
File registryFile = file("${rootProject.projectDir}/config/jpa/readiness-cards.yaml")
inputs.file(registryFile)
doLast {
if (!registryFile.isFile()) {
throw new GradleException(
"verifyJpaReadinessRegistry: missing registry ${registryFile}")
}
String raw = registryFile.getText('UTF-8')
Map<String, Object> registry
try {
registry = new JsonSlurper().parseText(raw) as Map<String, Object>
} catch (RuntimeException ex) {
throw new GradleException(
"verifyJpaReadinessRegistry: registry is not valid JSON-compatible YAML",
ex)
}
List<String> violations =
validateJpaReadinessRegistry(registry, raw, jpaTaskExists)
if (!violations.isEmpty()) {
throw new GradleException(
"verifyJpaReadinessRegistry: ${violations.size()} violation(s):\n " +
violations.toSorted().join('\n '))
}
logger.lifecycle(
"verifyJpaReadinessRegistry: OK — ${expectedJpaReadinessCardIds.size()} exact " +
"cards, ${expectedJpaOwnedMigrationCardIds.size()} owned migration " +
'streams, acyclic prerequisites, unique tasks/locations/history tables, ' +
'and selected task existence verified.')
}
}
configure(subprojects.findAll { it.childProjects.isEmpty() }) {
tasks.named('check') {
dependsOn verifyJpaReadinessRegistry
}
}
def verifyApplicationCoreDependencyPurity = tasks.register('verifyApplicationCoreDependencyPurity') {
group = 'verification'
description = 'Verifies application-core has only project production dependencies and no diagnostic frameworks on application classpaths.'