feat: add JPA production capability
This commit is contained in:
@@ -0,0 +1,930 @@
|
||||
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<Object> canonicalizeJpaEvidence
|
||||
canonicalizeJpaEvidence = { Object value ->
|
||||
if (value instanceof Map) {
|
||||
Map<String, Object> 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<String> canonicalJpaEvidenceJson = { Object value ->
|
||||
JsonOutput.toJson(canonicalizeJpaEvidence(value))
|
||||
}
|
||||
|
||||
Closure<String> sha256JpaEvidence = { String value ->
|
||||
MessageDigest digest = MessageDigest.getInstance('SHA-256')
|
||||
digest.digest(value.getBytes(StandardCharsets.UTF_8)).encodeHex().toString()
|
||||
}
|
||||
|
||||
Closure<Task> 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<String> runJpaEvidenceCommand = { List<String> command ->
|
||||
providers.exec {
|
||||
commandLine command
|
||||
ignoreExitValue = true
|
||||
}.standardOutput.asText.get().trim()
|
||||
}
|
||||
|
||||
Closure<Map<String, Object>> readJpaJUnitResult = { Test testTask ->
|
||||
File resultDirectory = testTask.reports.junitXml.outputLocation.get().asFile
|
||||
List<File> 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<String> 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<String, Object>
|
||||
}
|
||||
|
||||
Closure<List<String>> requiredJpaEvidence = { Map<String, Object> card ->
|
||||
List<String> 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<Map<String, String>> resolvedJpaEvidenceVersions = {
|
||||
Map<String, String> 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<String, String>
|
||||
}
|
||||
|
||||
Set<String> 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<List<String>> validateJpaEvidenceManifest = {
|
||||
Map<String, Object> card,
|
||||
Map<String, Object> manifest ->
|
||||
List<String> violations = []
|
||||
String cardId = manifest.cardId as String
|
||||
Set<String> 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<String, Object> source = manifest.source instanceof Map
|
||||
? manifest.source as Map<String, Object>
|
||||
: [:]
|
||||
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<String, Object> producer = manifest.producer instanceof Map
|
||||
? manifest.producer as Map<String, Object>
|
||||
: [:]
|
||||
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<String, Object> testResult = manifest.testResult instanceof Map
|
||||
? manifest.testResult as Map<String, Object>
|
||||
: [:]
|
||||
Set<String> 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<String> required = manifest.requiredEvidence instanceof List
|
||||
? (manifest.requiredEvidence as List).collect { it as String }.toSorted()
|
||||
: []
|
||||
List<String> covered = manifest.coveredEvidence instanceof List
|
||||
? (manifest.coveredEvidence as List).collect { it as String }.toSorted()
|
||||
: []
|
||||
List<String> 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<String, Object> postgresql = manifest.postgresql instanceof Map
|
||||
? manifest.postgresql as Map<String, Object>
|
||||
: [:]
|
||||
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<String, Object> dependencies = manifest.dependencies instanceof Map
|
||||
? manifest.dependencies as Map<String, Object>
|
||||
: [:]
|
||||
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<Object> prerequisites = manifest.prerequisites instanceof List
|
||||
? manifest.prerequisites as List<Object>
|
||||
: []
|
||||
prerequisites.eachWithIndex { Object rawPrerequisite, int index ->
|
||||
Map<String, Object> prerequisite = rawPrerequisite instanceof Map
|
||||
? rawPrerequisite as Map<String, Object>
|
||||
: [:]
|
||||
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<String, Object> migration = manifest.migration instanceof Map
|
||||
? manifest.migration as Map<String, Object>
|
||||
: [:]
|
||||
Set<String> 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<Map<String, Object>> loadJpaEvidenceRegistry = {
|
||||
new JsonSlurper().parse(jpaEvidenceRegistryFile) as Map<String, Object>
|
||||
}
|
||||
|
||||
Closure<Map<String, Object>> verifyJpaEvidenceDirectory = {
|
||||
File outputDirectory,
|
||||
Map<String, Object> registry ->
|
||||
List<String> violations = []
|
||||
Map<String, Object> manifests = [:]
|
||||
Map<String, String> manifestIds = [:]
|
||||
Map<String, Object> activeCards = (registry.cards as Map<String, Object>).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<File> 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<String, Object> manifest =
|
||||
new JsonSlurper().parse(manifestFile) as Map<String, Object>
|
||||
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<String, Object>,
|
||||
manifest))
|
||||
manifests[cardId] = manifest
|
||||
manifestIds[cardId] = "sha256:${contentHash}".toString()
|
||||
}
|
||||
|
||||
manifests.each { String cardId, Object rawManifest ->
|
||||
Map<String, Object> manifest = rawManifest as Map<String, Object>
|
||||
(manifest.prerequisites as List).each { Object rawPrerequisite ->
|
||||
Map<String, Object> prerequisite = rawPrerequisite as Map<String, Object>
|
||||
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<String, Object> card = [
|
||||
state: 'selected',
|
||||
'required-evidence': ['real-postgresql', 'no-skip']
|
||||
]
|
||||
Map<String, Object> 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<String> baseline = validateJpaEvidenceManifest(card, valid)
|
||||
if (!baseline.isEmpty()) {
|
||||
throw new GradleException(
|
||||
"verifyJpaEvidenceHarnessContract: valid fixture failed ${baseline}")
|
||||
}
|
||||
|
||||
Map<String, Object> skipped =
|
||||
new JsonSlurper().parseText(JsonOutput.toJson(valid)) as Map<String, Object>
|
||||
(skipped.testResult as Map).skippedOrAbortedCount = 1
|
||||
(skipped.testResult as Map).noSkipResult = false
|
||||
List<String> 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<String, Object> dirtyR2 =
|
||||
new JsonSlurper().parseText(JsonOutput.toJson(valid)) as Map<String, Object>
|
||||
dirtyR2.attainedReadiness = 'R2'
|
||||
dirtyR2.profile = 'r2'
|
||||
List<String> 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<String, Object> mutated =
|
||||
new JsonSlurper().parseText(JsonOutput.toJson(valid)) as Map<String, Object>
|
||||
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<String, Object> configuredRegistry = loadJpaEvidenceRegistry()
|
||||
Map<String, Object> configuredActiveCards =
|
||||
(configuredRegistry.cards as Map<String, Object>).findAll {
|
||||
String ignored, Object rawCard ->
|
||||
((rawCard as Map).state as String) != 'not-implemented'
|
||||
}
|
||||
configuredActiveCards.each { String cardId, Object rawCard ->
|
||||
Map<String, Object> card = rawCard as Map<String, Object>
|
||||
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<String, Object> primaryCard =
|
||||
configuredActiveCards['jpa-primary-foundation'] as Map<String, Object>
|
||||
(primaryCard['support-tasks'] as List).each { Object taskPath ->
|
||||
dependsOn jpaEvidenceTaskAtPath(taskPath as String)
|
||||
}
|
||||
|
||||
outputs.dir(jpaEvidenceOutputDirectory)
|
||||
outputs.upToDateWhen { false }
|
||||
|
||||
doLast {
|
||||
Map<String, Object> registry = loadJpaEvidenceRegistry()
|
||||
Map<String, Object> cards = registry.cards as Map<String, Object>
|
||||
Map<String, Object> 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<String, String> dependencyVersions = resolvedJpaEvidenceVersions()
|
||||
List<String> 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<String, Object> manifests = [:]
|
||||
Map<String, String> manifestIds = [:]
|
||||
activeCards.each { String cardId, Object rawCard ->
|
||||
Map<String, Object> card = rawCard as Map<String, Object>
|
||||
List<Map<String, Object>> testResults = []
|
||||
if (cardId == 'jpa-primary-foundation') {
|
||||
(card.prerequisites as List).each { Object prerequisite ->
|
||||
Map<String, Object> prerequisiteManifest =
|
||||
manifests[prerequisite as String] as Map<String, Object>
|
||||
if (prerequisiteManifest != null) {
|
||||
testResults << (prerequisiteManifest.testResult as Map<String, Object>)
|
||||
}
|
||||
}
|
||||
} 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<String> executedSelectors = testResults
|
||||
.collectMany { Map<String, Object> result ->
|
||||
result.executedSelectors as List<String>
|
||||
}
|
||||
.toSet()
|
||||
Set<String> covered = new TreeSet<>()
|
||||
List<Map<String, Object>> scenarios =
|
||||
((card.evidence as Map).scenarios as List<Map<String, Object>>)
|
||||
scenarios.each { Map<String, Object> scenario ->
|
||||
if (executedSelectors.contains(scenario.selector as String)) {
|
||||
covered.addAll((scenario.covers as List).collect { it as String })
|
||||
}
|
||||
}
|
||||
List<Map<String, Object>> taskClaims =
|
||||
((card.evidence as Map)['task-claims'] as List<Map<String, Object>>)
|
||||
taskClaims.each { Map<String, Object> 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<String, Object> result -> result.executedTestCount as int
|
||||
} as int
|
||||
int skippedOrAbortedCount = testResults.sum {
|
||||
Map<String, Object> result -> result.skippedOrAbortedCount as int
|
||||
} as int
|
||||
int failureCount = testResults.sum {
|
||||
Map<String, Object> result -> result.failureCount as int
|
||||
} as int
|
||||
int errorCount = testResults.sum {
|
||||
Map<String, Object> 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<String> required = requiredJpaEvidence(card)
|
||||
List<String> coveredList = covered.findAll {
|
||||
String claim -> required.contains(claim)
|
||||
}.toList().sort()
|
||||
List<String> missing = (required - coveredList).toSorted()
|
||||
List<Map<String, Object>> prerequisites = (card.prerequisites as List).collect {
|
||||
Object rawPrerequisite ->
|
||||
String prerequisiteId = rawPrerequisite as String
|
||||
Map<String, Object> prerequisiteManifest =
|
||||
manifests[prerequisiteId] as Map<String, Object>
|
||||
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<String, Object>
|
||||
}
|
||||
|
||||
List<String> 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<String, Object> prerequisite ->
|
||||
prerequisite.attainedReadiness != 'R2'
|
||||
}.each { Map<String, Object> 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<String, Object> 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<String, Object>
|
||||
: null
|
||||
|
||||
Map<String, Object> 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<String, Object> result -> result.tasks as List<String>
|
||||
}.toSet().toList().sort(),
|
||||
resultDirectories: testResults.collectMany {
|
||||
Map<String, Object> result -> result.resultDirectories as List<String>
|
||||
}.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, Object>
|
||||
|
||||
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<String, Object> result = verifyJpaEvidenceDirectory(
|
||||
jpaEvidenceOutputDirectory.get().asFile,
|
||||
loadJpaEvidenceRegistry())
|
||||
List<String> violations = result.violations as List<String>
|
||||
if (!violations.isEmpty()) {
|
||||
throw new GradleException(
|
||||
"verifyJpaCandidateEvidence: ${violations.size()} violation(s):\n " +
|
||||
violations.toSorted().join('\n '))
|
||||
}
|
||||
Map<String, Object> manifests = result.manifests as Map<String, Object>
|
||||
manifests.each { String cardId, Object rawManifest ->
|
||||
Map<String, Object> manifest = rawManifest as Map<String, Object>
|
||||
List<String> missing = manifest.missingEvidence as List<String>
|
||||
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<String, Object> result = verifyJpaEvidenceDirectory(
|
||||
jpaEvidenceOutputDirectory.get().asFile,
|
||||
loadJpaEvidenceRegistry())
|
||||
List<String> violations = result.violations as List<String>
|
||||
Map<String, Object> manifests = result.manifests as Map<String, Object>
|
||||
Map<String, Object> primary =
|
||||
manifests['jpa-primary-foundation'] as Map<String, Object>
|
||||
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<String, Object> manifest = manifests[cardId] as Map<String, Object>
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user