import groovy.json.JsonOutput import groovy.json.JsonSlurper import groovy.xml.XmlSlurper import java.nio.charset.StandardCharsets import java.security.MessageDigest import java.time.Instant import org.gradle.api.artifacts.component.ModuleComponentIdentifier import org.gradle.api.tasks.testing.Test /* * JPA readiness evidence producer. * * The registry owns the card/task/scenario mapping. This script only accepts evidence emitted by * tasks in that registry, reads their JUnit XML, and writes one content-addressed manifest per * active card. The candidate verifier deliberately permits incomplete R2 dimensions while the * canonical primary-foundation task requires a clean CI R2 profile and a complete prerequisite * manifest DAG. */ File jpaEvidenceRegistryFile = rootProject.file('config/jpa/readiness-cards.yaml') def jpaEvidenceOutputDirectory = layout.buildDirectory.dir('jpa-evidence/manifests') String jpaEvidenceImage = project.ext.jpaPostgreSqlEvidenceImage as String Closure canonicalizeJpaEvidence canonicalizeJpaEvidence = { Object value -> if (value instanceof Map) { Map sorted = new TreeMap<>() (value as Map).each { Object key, Object child -> sorted[key as String] = canonicalizeJpaEvidence(child) } return sorted } if (value instanceof List) { return (value as List).collect { Object child -> canonicalizeJpaEvidence(child) } } value } Closure canonicalJpaEvidenceJson = { Object value -> JsonOutput.toJson(canonicalizeJpaEvidence(value)) } Closure sha256JpaEvidence = { String value -> MessageDigest digest = MessageDigest.getInstance('SHA-256') digest.digest(value.getBytes(StandardCharsets.UTF_8)).encodeHex().toString() } Closure jpaEvidenceTaskAtPath = { String absoluteTaskPath -> int separator = absoluteTaskPath.lastIndexOf(':') if (separator < 0 || separator == absoluteTaskPath.length() - 1) { throw new GradleException("Invalid absolute Gradle task path '${absoluteTaskPath}'") } String projectPath = separator == 0 ? ':' : absoluteTaskPath.substring(0, separator) String taskName = absoluteTaskPath.substring(separator + 1) Project owner = rootProject.findProject(projectPath) if (owner == null) { throw new GradleException("Unknown project for JPA evidence task '${absoluteTaskPath}'") } Task task = owner.tasks.findByName(taskName) if (task == null) { throw new GradleException("Missing JPA evidence task '${absoluteTaskPath}'") } task } Closure runJpaEvidenceCommand = { List command -> providers.exec { commandLine command ignoreExitValue = true }.standardOutput.asText.get().trim() } Closure> readJpaJUnitResult = { Test testTask -> File resultDirectory = testTask.reports.junitXml.outputLocation.get().asFile List resultFiles = resultDirectory.isDirectory() ? (resultDirectory.listFiles() ?: [] as File[]) .findAll { File result -> result.name.startsWith('TEST-') && result.name.endsWith('.xml') } .toSorted { File left, File right -> left.name <=> right.name } : [] if (resultFiles.isEmpty()) { throw new GradleException( "${testTask.path}: JUnit XML evidence is missing from ${resultDirectory}") } int executed = 0 int skipped = 0 int failures = 0 int errors = 0 Set selectors = new TreeSet<>() resultFiles.each { File resultFile -> def suite = new XmlSlurper(false, false).parse(resultFile) executed += (suite.@tests.text() ?: '0') as int skipped += (suite.@skipped.text() ?: '0') as int failures += (suite.@failures.text() ?: '0') as int errors += (suite.@errors.text() ?: '0') as int suite.testcase.each { Object rawCase -> String className = rawCase.@classname.text() String methodName = rawCase.@name.text().replaceFirst(/\([^)]*\)$/, '') selectors << "${className}#${methodName}".toString() } } [ tasks: [testTask.path], resultDirectories: [rootProject.relativePath(resultDirectory)], executedTestCount: executed, skippedOrAbortedCount: skipped, failureCount: failures, errorCount: errors, noSkipResult: executed > 0 && skipped == 0 && failures == 0 && errors == 0, executedSelectors: selectors.toList() ] as Map } Closure> requiredJpaEvidence = { Map card -> List required = (card['required-evidence'] as List) .collect { Object item -> item as String } if (card.migration instanceof Map) { Object rawLifecycle = (card.migration as Map)['lifecycle-evidence'] if (rawLifecycle instanceof List) { (rawLifecycle as List).each { Object lifecycle -> required << "migration-lifecycle:${lifecycle as String}".toString() } } } required.toSet().toSorted() } Closure> resolvedJpaEvidenceVersions = { Map versions = [:] configurations.postgresqlIntegrationTestRuntimeClasspath .incoming .resolutionResult .allComponents .each { component -> if (component.id instanceof ModuleComponentIdentifier) { ModuleComponentIdentifier id = component.id as ModuleComponentIdentifier versions["${id.group}:${id.module}".toString()] = id.version } } [ pgjdbc: versions['org.postgresql:postgresql'] ?: '', hibernate: versions['org.hibernate.orm:hibernate-core'] ?: '', flyway: versions['org.flywaydb:flyway-core'] ?: '' ] as Map } Set expectedJpaEvidenceManifestKeys = [ 'schemaVersion', 'cardId', 'cardVersion', 'declaredState', 'attainedReadiness', 'evidenceGrade', 'profile', 'prerequisites', 'source', 'producer', 'testResult', 'requiredEvidence', 'coveredEvidence', 'missingEvidence', 'readinessBlockers', 'postgresql', 'dependencies', 'generatedAt', 'date', 'topology', 'artifactLocation', 'migration', 'dispatchModes' ] as Set Closure> validateJpaEvidenceManifest = { Map card, Map manifest -> List violations = [] String cardId = manifest.cardId as String Set actualKeys = manifest.keySet().collect { it as String }.toSet() if (actualKeys != expectedJpaEvidenceManifestKeys) { violations << "${cardId}: manifest keys must be exactly ${expectedJpaEvidenceManifestKeys}" } if (manifest.schemaVersion != 1) { violations << "${cardId}: schemaVersion must be 1" } if (cardId == null || cardId.isBlank()) { violations << 'manifest cardId must be non-blank' } if (!(manifest.declaredState in ['selected', 'implemented-candidate'])) { violations << "${cardId}: invalid declaredState '${manifest.declaredState}'" } if (!(manifest.attainedReadiness in ['R1', 'R2'])) { violations << "${cardId}: invalid attainedReadiness '${manifest.attainedReadiness}'" } if (!(manifest.evidenceGrade in ['E1', 'E2', 'E3'])) { violations << "${cardId}: invalid evidenceGrade '${manifest.evidenceGrade}'" } if (!(manifest.profile in ['candidate', 'r2'])) { violations << "${cardId}: invalid profile '${manifest.profile}'" } Map source = manifest.source instanceof Map ? manifest.source as Map : [:] if (source.keySet().collect { it as String }.toSet() != ['revision', 'worktreeDirty', 'worktreeStatusDigest'] as Set) { violations << "${cardId}: invalid source metadata keys" } if (!((source.revision as String) ==~ /[0-9a-f]{7,40}/)) { violations << "${cardId}: invalid source revision '${source.revision}'" } if (!(source.worktreeDirty instanceof Boolean)) { violations << "${cardId}: worktreeDirty must be boolean" } if (!((source.worktreeStatusDigest as String) ==~ /[0-9a-f]{64}/)) { violations << "${cardId}: invalid worktree status digest" } Map producer = manifest.producer instanceof Map ? manifest.producer as Map : [:] if (producer.keySet().collect { it as String }.toSet() != ['gradleTask', 'ciJob'] as Set) { violations << "${cardId}: invalid producer metadata keys" } if (!((producer.gradleTask as String)?.startsWith(':'))) { violations << "${cardId}: producer Gradle task must be absolute" } if ((producer.ciJob as String)?.isBlank()) { violations << "${cardId}: producer CI job must be non-blank" } Map testResult = manifest.testResult instanceof Map ? manifest.testResult as Map : [:] Set expectedTestKeys = [ 'tasks', 'resultDirectories', 'executedTestCount', 'skippedOrAbortedCount', 'failureCount', 'errorCount', 'noSkipResult', 'executedSelectors' ] as Set if (testResult.keySet().collect { it as String }.toSet() != expectedTestKeys) { violations << "${cardId}: invalid testResult keys" } if (!((testResult.executedTestCount ?: 0) instanceof Number) || (testResult.executedTestCount as int) <= 0) { violations << "${cardId}: executed test count must be positive" } ['skippedOrAbortedCount', 'failureCount', 'errorCount'].each { String countKey -> if (!((testResult[countKey] ?: 0) instanceof Number) || (testResult[countKey] as int) != 0) { violations << "${cardId}: ${countKey} must be zero" } } if (testResult.noSkipResult != true) { violations << "${cardId}: no-skip sentinel must be true" } List required = manifest.requiredEvidence instanceof List ? (manifest.requiredEvidence as List).collect { it as String }.toSorted() : [] List covered = manifest.coveredEvidence instanceof List ? (manifest.coveredEvidence as List).collect { it as String }.toSorted() : [] List missing = manifest.missingEvidence instanceof List ? (manifest.missingEvidence as List).collect { it as String }.toSorted() : [] if (required != requiredJpaEvidence(card)) { violations << "${cardId}: required evidence drifted from registry" } if (missing != (required - covered).toSorted()) { violations << "${cardId}: missing evidence is not required minus covered" } Map postgresql = manifest.postgresql instanceof Map ? manifest.postgresql as Map : [:] if (postgresql.keySet().collect { it as String }.toSet() != ['image', 'imageDigest', 'managedEngineVersion'] as Set) { violations << "${cardId}: invalid PostgreSQL metadata keys" } if (!((postgresql.imageDigest as String) ==~ /.+@sha256:[0-9a-f]{64}/)) { violations << "${cardId}: PostgreSQL image digest must be immutable" } Map dependencies = manifest.dependencies instanceof Map ? manifest.dependencies as Map : [:] if (dependencies.keySet().collect { it as String }.toSet() != ['pgjdbc', 'hibernate', 'flyway'] as Set || dependencies.values().any { Object version -> (version as String)?.isBlank() }) { violations << "${cardId}: pgjdbc/Hibernate/Flyway versions must be present" } try { Instant.parse(manifest.generatedAt as String) } catch (RuntimeException ignored) { violations << "${cardId}: generatedAt must be an ISO-8601 instant" } if (!((manifest.date as String) ==~ /\d{4}-\d{2}-\d{2}/)) { violations << "${cardId}: date must be ISO-8601" } if ((manifest.topology as String)?.isBlank()) { violations << "${cardId}: topology must be non-blank" } if ((manifest.artifactLocation as String)?.isBlank()) { violations << "${cardId}: artifactLocation must be non-blank" } List prerequisites = manifest.prerequisites instanceof List ? manifest.prerequisites as List : [] prerequisites.eachWithIndex { Object rawPrerequisite, int index -> Map prerequisite = rawPrerequisite instanceof Map ? rawPrerequisite as Map : [:] if (prerequisite.keySet().collect { it as String }.toSet() != ['cardId', 'cardVersion', 'manifestId', 'attainedReadiness'] as Set) { violations << "${cardId}: prerequisite ${index} has invalid keys" } if (!((prerequisite.manifestId as String) ==~ /sha256:[0-9a-f]{64}/)) { violations << "${cardId}: prerequisite ${index} has invalid manifest ID" } } if (card.migration instanceof Map) { Map migration = manifest.migration instanceof Map ? manifest.migration as Map : [:] Set expectedMigrationKeys = [ 'location', 'historyTable', 'requiredCoreEpoch', 'featureRevision', 'streamLifecycleEvidenceIds' ] as Set if (migration.keySet().collect { it as String }.toSet() != expectedMigrationKeys) { violations << "${cardId}: schema-bearing manifest has invalid migration metadata" } } else if (manifest.migration != null) { violations << "${cardId}: non-schema card must not contain migration metadata" } if (card['dispatch-modes'] instanceof List) { if (manifest.dispatchModes != card['dispatch-modes']) { violations << "${cardId}: dispatch modes drifted from registry" } } else if (manifest.dispatchModes != []) { violations << "${cardId}: non-outbox card must have empty dispatch modes" } if (manifest.attainedReadiness == 'R2') { if (manifest.profile != 'r2') { violations << "${cardId}: R2 requires the r2 profile" } if (source.worktreeDirty != false) { violations << "${cardId}: R2 requires a clean worktree" } if (!missing.isEmpty()) { violations << "${cardId}: R2 has missing evidence ${missing}" } if (producer.ciJob == 'local-unpublished') { violations << "${cardId}: R2 requires a real CI job identity" } if (!((manifest.artifactLocation as String) ==~ /(?i)(https|s3|gs):\/\/\S+/)) { violations << "${cardId}: R2 requires an externally retained artifact location" } } violations } Closure> loadJpaEvidenceRegistry = { new JsonSlurper().parse(jpaEvidenceRegistryFile) as Map } Closure> verifyJpaEvidenceDirectory = { File outputDirectory, Map registry -> List violations = [] Map manifests = [:] Map manifestIds = [:] Map activeCards = (registry.cards as Map).findAll { String ignored, Object rawCard -> ((rawCard as Map).state as String) != 'not-implemented' } activeCards.each { String cardId, Object rawCard -> File cardDirectory = new File(outputDirectory, cardId) List files = cardDirectory.isDirectory() ? (cardDirectory.listFiles() ?: [] as File[]) .findAll { File file -> file.name.endsWith('.json') } : [] if (files.size() != 1) { violations << "${cardId}: expected exactly one content-addressed manifest; got ${files.size()}" return } File manifestFile = files[0] String fileHash = manifestFile.name.substring(0, manifestFile.name.length() - '.json'.length()) Map manifest = new JsonSlurper().parse(manifestFile) as Map String contentHash = sha256JpaEvidence(canonicalJpaEvidenceJson(manifest)) if (fileHash != contentHash) { violations << "${cardId}: filename hash ${fileHash} does not match content ${contentHash}" } if ((manifest.cardId as String) != cardId) { violations << "${cardId}: manifest cardId is '${manifest.cardId}'" } violations.addAll(validateJpaEvidenceManifest( rawCard as Map, manifest)) manifests[cardId] = manifest manifestIds[cardId] = "sha256:${contentHash}".toString() } manifests.each { String cardId, Object rawManifest -> Map manifest = rawManifest as Map (manifest.prerequisites as List).each { Object rawPrerequisite -> Map prerequisite = rawPrerequisite as Map String prerequisiteId = prerequisite.cardId as String if (manifestIds[prerequisiteId] != prerequisite.manifestId) { violations << "${cardId}: prerequisite ${prerequisiteId} manifest ID does not match" } } } [violations: violations, manifests: manifests, manifestIds: manifestIds] } def verifyJpaEvidenceHarnessContract = tasks.register('verifyJpaEvidenceHarnessContract') { group = 'verification' description = 'Mutation-tests JPA evidence schema, no-skip, content hash, and R2 provenance checks.' doLast { Map card = [ state: 'selected', 'required-evidence': ['real-postgresql', 'no-skip'] ] Map valid = [ schemaVersion: 1, cardId: 'jpa-contract-fixture', cardVersion: '1', declaredState: 'selected', attainedReadiness: 'R1', evidenceGrade: 'E2', profile: 'candidate', prerequisites: [], source: [ revision: 'b3add0162df8', worktreeDirty: true, worktreeStatusDigest: '0' * 64 ], producer: [ gradleTask: ':adapter:outbound:persistence-jpa:contractFixture', ciJob: 'local-unpublished' ], testResult: [ tasks: [':adapter:outbound:persistence-jpa:contractFixture'], resultDirectories: ['build/test-results/contractFixture'], executedTestCount: 1, skippedOrAbortedCount: 0, failureCount: 0, errorCount: 0, noSkipResult: true, executedSelectors: ['dev.caskeleton.ContractFixture#passes'] ], requiredEvidence: ['no-skip', 'real-postgresql'], coveredEvidence: ['no-skip', 'real-postgresql'], missingEvidence: [], readinessBlockers: ['candidate-profile-is-not-release-evidence'], postgresql: [ image: 'postgres:16-alpine', imageDigest: "postgres@sha256:${'1' * 64}".toString(), managedEngineVersion: '16' ], dependencies: [ pgjdbc: '42.7.8', hibernate: '7.1.8.Final', flyway: '11.14.1' ], generatedAt: '2026-07-28T00:00:00Z', date: '2026-07-28', topology: 'single-postgresql-testcontainer', artifactLocation: 'build/jpa-evidence/manifests', migration: null, dispatchModes: [] ] List baseline = validateJpaEvidenceManifest(card, valid) if (!baseline.isEmpty()) { throw new GradleException( "verifyJpaEvidenceHarnessContract: valid fixture failed ${baseline}") } Map skipped = new JsonSlurper().parseText(JsonOutput.toJson(valid)) as Map (skipped.testResult as Map).skippedOrAbortedCount = 1 (skipped.testResult as Map).noSkipResult = false List skippedViolations = validateJpaEvidenceManifest(card, skipped) if (!skippedViolations.any { String violation -> violation.contains('must be zero') } || !skippedViolations.any { String violation -> violation.contains('sentinel must be true') }) { throw new GradleException( "verifyJpaEvidenceHarnessContract: skip mutation escaped ${skippedViolations}") } Map dirtyR2 = new JsonSlurper().parseText(JsonOutput.toJson(valid)) as Map dirtyR2.attainedReadiness = 'R2' dirtyR2.profile = 'r2' List dirtyViolations = validateJpaEvidenceManifest(card, dirtyR2) if (!dirtyViolations.any { String violation -> violation.contains('clean worktree') } || !dirtyViolations.any { String violation -> violation.contains('real CI job') }) { throw new GradleException( "verifyJpaEvidenceHarnessContract: R2 provenance mutation escaped ${dirtyViolations}") } String validHash = sha256JpaEvidence(canonicalJpaEvidenceJson(valid)) Map mutated = new JsonSlurper().parseText(JsonOutput.toJson(valid)) as Map mutated.topology = 'mutated-topology' String mutatedHash = sha256JpaEvidence(canonicalJpaEvidenceJson(mutated)) if (validHash == mutatedHash) { throw new GradleException( 'verifyJpaEvidenceHarnessContract: content mutation did not change manifest ID') } logger.lifecycle( 'verifyJpaEvidenceHarnessContract: OK — skip, dirty/local R2, and content mutation fail closed.') } } def generateJpaEvidenceManifests = tasks.register('generateJpaEvidenceManifests') { group = 'verification' description = 'Runs active JPA card producers and writes content-addressed candidate/R2 manifests.' dependsOn verifyJpaEvidenceHarnessContract dependsOn rootProject.tasks.named('verifyJpaReadinessRegistry') Map configuredRegistry = loadJpaEvidenceRegistry() Map configuredActiveCards = (configuredRegistry.cards as Map).findAll { String ignored, Object rawCard -> ((rawCard as Map).state as String) != 'not-implemented' } configuredActiveCards.each { String cardId, Object rawCard -> Map card = rawCard as Map if (cardId != 'jpa-primary-foundation') { dependsOn jpaEvidenceTaskAtPath(card['readiness-task'] as String) } ((card['support-tasks'] ?: []) as List).each { Object taskPath -> dependsOn jpaEvidenceTaskAtPath(taskPath as String) } } Map primaryCard = configuredActiveCards['jpa-primary-foundation'] as Map (primaryCard['support-tasks'] as List).each { Object taskPath -> dependsOn jpaEvidenceTaskAtPath(taskPath as String) } outputs.dir(jpaEvidenceOutputDirectory) outputs.upToDateWhen { false } doLast { Map registry = loadJpaEvidenceRegistry() Map cards = registry.cards as Map Map activeCards = cards.findAll { String ignored, Object rawCard -> ((rawCard as Map).state as String) != 'not-implemented' } String profile = providers.gradleProperty('jpaEvidenceProfile') .orElse(providers.environmentVariable('JPA_EVIDENCE_PROFILE')) .getOrElse('candidate') if (!(profile in ['candidate', 'r2'])) { throw new GradleException( "jpaEvidenceProfile must be candidate or r2; got '${profile}'") } String ciJob = providers.environmentVariable('JPA_EVIDENCE_CI_JOB') .getOrElse(profile == 'candidate' ? 'local-unpublished' : '') String configuredArtifactLocation = providers.environmentVariable('JPA_EVIDENCE_ARTIFACT_LOCATION') .getOrElse(profile == 'candidate' ? rootProject.relativePath(jpaEvidenceOutputDirectory.get().asFile) : '') String topology = providers.environmentVariable('JPA_EVIDENCE_TOPOLOGY') .getOrElse('single-postgresql-testcontainer') String worktreeStatus = runJpaEvidenceCommand( ['git', 'status', '--porcelain=v1', '--untracked-files=all']) boolean worktreeDirty = !worktreeStatus.isBlank() String worktreeStatusDigest = sha256JpaEvidence(worktreeStatus) String imageDigest = runJpaEvidenceCommand([ 'docker', 'image', 'inspect', '--format={{index .RepoDigests 0}}', jpaEvidenceImage ]) Map dependencyVersions = resolvedJpaEvidenceVersions() List productionMetadataBlockers = [] if (profile == 'r2') { if (worktreeDirty) { productionMetadataBlockers << 'worktree-is-dirty' } if (ciJob.isBlank()) { productionMetadataBlockers << 'missing-JPA_EVIDENCE_CI_JOB' } if (configuredArtifactLocation.isBlank()) { productionMetadataBlockers << 'missing-JPA_EVIDENCE_ARTIFACT_LOCATION' } else if (!(configuredArtifactLocation ==~ /(?i)(https|s3|gs):\/\/\S+/)) { productionMetadataBlockers << 'artifact-location-is-not-externally-retained' } } if (!(imageDigest ==~ /.+@sha256:[0-9a-f]{64}/)) { productionMetadataBlockers << 'missing-immutable-postgresql-image-digest' } dependencyVersions.each { String component, String version -> if (version.isBlank()) { productionMetadataBlockers << "missing-${component}-version".toString() } } File outputDirectory = jpaEvidenceOutputDirectory.get().asFile delete(outputDirectory) outputDirectory.mkdirs() Map manifests = [:] Map manifestIds = [:] activeCards.each { String cardId, Object rawCard -> Map card = rawCard as Map List> testResults = [] if (cardId == 'jpa-primary-foundation') { (card.prerequisites as List).each { Object prerequisite -> Map prerequisiteManifest = manifests[prerequisite as String] as Map if (prerequisiteManifest != null) { testResults << (prerequisiteManifest.testResult as Map) } } } else { Task readinessTask = jpaEvidenceTaskAtPath(card['readiness-task'] as String) if (!(readinessTask instanceof Test)) { throw new GradleException( "${cardId}: readiness task ${readinessTask.path} must be a Test task") } testResults << readJpaJUnitResult(readinessTask as Test) ((card['support-tasks'] ?: []) as List).each { Object taskPath -> Task supportTask = jpaEvidenceTaskAtPath(taskPath as String) if (supportTask instanceof Test) { testResults << readJpaJUnitResult(supportTask as Test) } } } Set executedSelectors = testResults .collectMany { Map result -> result.executedSelectors as List } .toSet() Set covered = new TreeSet<>() List> scenarios = ((card.evidence as Map).scenarios as List>) scenarios.each { Map scenario -> if (executedSelectors.contains(scenario.selector as String)) { covered.addAll((scenario.covers as List).collect { it as String }) } } List> taskClaims = ((card.evidence as Map)['task-claims'] as List>) taskClaims.each { Map taskClaim -> Task evidenceTask = jpaEvidenceTaskAtPath(taskClaim.task as String) if (evidenceTask.state.executed && evidenceTask.state.failure == null && !evidenceTask.state.skipped) { covered.addAll((taskClaim.covers as List).collect { it as String }) } } int executedTestCount = testResults.sum { Map result -> result.executedTestCount as int } as int int skippedOrAbortedCount = testResults.sum { Map result -> result.skippedOrAbortedCount as int } as int int failureCount = testResults.sum { Map result -> result.failureCount as int } as int int errorCount = testResults.sum { Map result -> result.errorCount as int } as int boolean noSkipResult = executedTestCount > 0 && skippedOrAbortedCount == 0 && failureCount == 0 && errorCount == 0 if (noSkipResult) { covered << 'no-skip' } if (cardId == 'jpa-primary-foundation' && (card.prerequisites as List).every { Object prerequisite -> manifestIds.containsKey(prerequisite as String) }) { covered << 'base-card-manifests' } List required = requiredJpaEvidence(card) List coveredList = covered.findAll { String claim -> required.contains(claim) }.toList().sort() List missing = (required - coveredList).toSorted() List> prerequisites = (card.prerequisites as List).collect { Object rawPrerequisite -> String prerequisiteId = rawPrerequisite as String Map prerequisiteManifest = manifests[prerequisiteId] as Map if (prerequisiteManifest == null || manifestIds[prerequisiteId] == null) { throw new GradleException( "${cardId}: prerequisite manifest '${prerequisiteId}' was not produced first") } [ cardId: prerequisiteId, cardVersion: prerequisiteManifest.cardVersion, manifestId: manifestIds[prerequisiteId], attainedReadiness: prerequisiteManifest.attainedReadiness ] as Map } List readinessBlockers = [] if (profile == 'candidate') { readinessBlockers << 'candidate-profile-is-not-release-evidence' } readinessBlockers.addAll(productionMetadataBlockers) missing.each { String requirement -> readinessBlockers << "missing-evidence:${requirement}".toString() } prerequisites.findAll { Map prerequisite -> prerequisite.attainedReadiness != 'R2' }.each { Map prerequisite -> readinessBlockers << "prerequisite-not-R2:${prerequisite.cardId}".toString() } boolean attainedR2 = profile == 'r2' && readinessBlockers.isEmpty() && missing.isEmpty() String generatedAt = Instant.now().toString() String cardVersion = card.migration instanceof Map ? ((card.migration as Map)['feature-revision'] as Integer).toString() : rootProject.ext.traceableVersion as String String evidenceGrade = cardId == 'jpa-primary-foundation' ? 'E1' : (covered.any { String claim -> claim in [ 'concurrency', 'fault', 'publish-fault', 'migration', 'query-plan', 'optimistic-conflict' ] } ? 'E3' : 'E2') Map migration = card.migration instanceof Map ? [ location: (card.migration as Map).location, historyTable: (card.migration as Map)['history-table'], requiredCoreEpoch: (card.migration as Map)['required-core-epoch'], featureRevision: (card.migration as Map)['feature-revision'], streamLifecycleEvidenceIds: (card.migration as Map)['lifecycle-evidence'] ] as Map : null Map manifest = [ schemaVersion: 1, cardId: cardId, cardVersion: cardVersion, declaredState: card.state, attainedReadiness: attainedR2 ? 'R2' : 'R1', evidenceGrade: evidenceGrade, profile: profile, prerequisites: prerequisites, source: [ revision: rootProject.ext.sourceRevision as String, worktreeDirty: worktreeDirty, worktreeStatusDigest: worktreeStatusDigest ], producer: [ gradleTask: card['readiness-task'], ciJob: ciJob ], testResult: [ tasks: testResults.collectMany { Map result -> result.tasks as List }.toSet().toList().sort(), resultDirectories: testResults.collectMany { Map result -> result.resultDirectories as List }.toSet().toList().sort(), executedTestCount: executedTestCount, skippedOrAbortedCount: skippedOrAbortedCount, failureCount: failureCount, errorCount: errorCount, noSkipResult: noSkipResult, executedSelectors: executedSelectors.toSorted() ], requiredEvidence: required, coveredEvidence: coveredList, missingEvidence: missing, readinessBlockers: readinessBlockers.toSet().toList().sort(), postgresql: [ image: jpaEvidenceImage, imageDigest: imageDigest, managedEngineVersion: '16' ], dependencies: dependencyVersions, generatedAt: generatedAt, date: generatedAt.substring(0, 10), topology: topology, artifactLocation: configuredArtifactLocation, migration: migration, dispatchModes: card['dispatch-modes'] instanceof List ? card['dispatch-modes'] : [] ] as Map String contentHash = sha256JpaEvidence(canonicalJpaEvidenceJson(manifest)) File cardDirectory = new File(outputDirectory, cardId) cardDirectory.mkdirs() File manifestFile = new File(cardDirectory, "${contentHash}.json") manifestFile.setText(JsonOutput.prettyPrint(JsonOutput.toJson(manifest)) + '\n', 'UTF-8') manifests[cardId] = manifest manifestIds[cardId] = "sha256:${contentHash}".toString() } logger.lifecycle( "generateJpaEvidenceManifests: wrote ${manifests.size()} ${profile} " + "content-addressed card manifests to ${outputDirectory}") } } gradle.taskGraph.whenReady { graph -> if (graph.hasTask(generateJpaEvidenceManifests.get())) { [ tasks.named('test').get(), project(':app-bootstrap').tasks.named('test').get() ].each { Task testTask -> testTask.outputs.upToDateWhen { false } } } } def verifyJpaCandidateEvidence = tasks.register('verifyJpaCandidateEvidence') { group = 'verification' description = 'Validates hashes, schema, exact JUnit selectors, no-skip, and prerequisite links without claiming R2.' dependsOn generateJpaEvidenceManifests outputs.upToDateWhen { false } doLast { Map result = verifyJpaEvidenceDirectory( jpaEvidenceOutputDirectory.get().asFile, loadJpaEvidenceRegistry()) List violations = result.violations as List if (!violations.isEmpty()) { throw new GradleException( "verifyJpaCandidateEvidence: ${violations.size()} violation(s):\n " + violations.toSorted().join('\n ')) } Map manifests = result.manifests as Map manifests.each { String cardId, Object rawManifest -> Map manifest = rawManifest as Map List missing = manifest.missingEvidence as List logger.lifecycle( "${cardId}: ${manifest.attainedReadiness}/${manifest.evidenceGrade}, " + "${manifest.testResult.executedTestCount} tests, " + "missing=${missing.isEmpty() ? 'none' : missing.join(',')}") } logger.lifecycle( "verifyJpaCandidateEvidence: OK — ${manifests.size()} manifests are " + 'content-addressed, linked, zero-skip candidate evidence; no R2 claim was made.') } } tasks.register('verifyJpaPrimaryFoundationEvidence') { group = 'verification' description = 'Requires complete immutable base-card manifests from a clean, retained CI R2 evidence lane.' dependsOn generateJpaEvidenceManifests outputs.upToDateWhen { false } doLast { Map result = verifyJpaEvidenceDirectory( jpaEvidenceOutputDirectory.get().asFile, loadJpaEvidenceRegistry()) List violations = result.violations as List Map manifests = result.manifests as Map Map primary = manifests['jpa-primary-foundation'] as Map if ((primary?.profile as String) != 'r2') { violations << 'jpa-primary-foundation: run with -PjpaEvidenceProfile=r2 in the dedicated CI lane' } [ 'jpa-observability-lifecycle', 'jpa-security-baseline', 'jpa-flyway-migration', 'jpa-transaction-runtime', 'jpa-aggregate-store', 'jpa-query-model', 'jpa-primary-foundation' ].each { String cardId -> Map manifest = manifests[cardId] as Map if (manifest == null) { violations << "${cardId}: manifest is missing" } else if (manifest.attainedReadiness != 'R2') { violations << "${cardId}: attained ${manifest.attainedReadiness}; blockers=" + "${(manifest.readinessBlockers as List).join(',')}" } } if (!violations.isEmpty()) { throw new GradleException( "verifyJpaPrimaryFoundationEvidence: ${violations.size()} violation(s):\n " + violations.toSorted().join('\n ')) } logger.lifecycle( 'verifyJpaPrimaryFoundationEvidence: OK — six immutable R2 base manifests and the primary DAG are verified.') } } tasks.named('check') { dependsOn verifyJpaEvidenceHarnessContract }