diff --git a/.github/scripts/compare-independent-builds.mjs b/.github/scripts/compare-independent-builds.mjs index 70af44f..05f756f 100644 --- a/.github/scripts/compare-independent-builds.mjs +++ b/.github/scripts/compare-independent-builds.mjs @@ -11,20 +11,22 @@ import { import { isAbsolute, relative, resolve, sep } from 'node:path'; const [standaloneOneManifest, standaloneTwoManifest, - localOneManifest, localTwoManifest, - standaloneOneRoot, standaloneTwoRoot, localOneRoot, localTwoRoot, - standaloneOneGradle, standaloneTwoGradle, localOneGradle, localTwoGradle, + standaloneThreeManifest, standaloneFourManifest, + standaloneOneRoot, standaloneTwoRoot, standaloneThreeRoot, + standaloneFourRoot, standaloneOneGradle, standaloneTwoGradle, + standaloneThreeGradle, standaloneFourGradle, bexCommit, outputPath] = process.argv.slice(2); if (!standaloneOneManifest || !standaloneTwoManifest || - !localOneManifest || !localTwoManifest || + !standaloneThreeManifest || !standaloneFourManifest || !standaloneOneRoot || !standaloneTwoRoot || - !localOneRoot || !localTwoRoot || + !standaloneThreeRoot || !standaloneFourRoot || !standaloneOneGradle || !standaloneTwoGradle || - !localOneGradle || !localTwoGradle || !bexCommit || !outputPath) { + !standaloneThreeGradle || !standaloneFourGradle || + !bexCommit || !outputPath) { throw new Error( - 'usage: compare-independent-builds.mjs S1 S2 L1 L2 ' + - 'S1_ROOT S2_ROOT L1_ROOT L2_ROOT S1_GRADLE S2_GRADLE ' + - 'L1_GRADLE L2_GRADLE COMMIT OUTPUT' + 'usage: compare-independent-builds.mjs S1 S2 S3 S4 ' + + 'S1_ROOT S2_ROOT S3_ROOT S4_ROOT S1_GRADLE S2_GRADLE ' + + 'S3_GRADLE S4_GRADLE COMMIT OUTPUT' ); } @@ -219,6 +221,7 @@ function pair(first, second) { const passed = exactManifestBytesMatch && exactArtifactBytesMatch && requiredArtifactsPresent; return { + mode: 'standalone-published', status: passed ? 'passed' : 'failed', exactManifestBytesMatch, exactArtifactBytesMatch, @@ -239,8 +242,10 @@ const builds = [ standaloneOneRoot, standaloneOneGradle), buildEvidence(standaloneTwoManifest, standaloneTwoRoot, standaloneTwoGradle), - buildEvidence(localOneManifest, localOneRoot, localOneGradle), - buildEvidence(localTwoManifest, localTwoRoot, localTwoGradle) + buildEvidence(standaloneThreeManifest, + standaloneThreeRoot, standaloneThreeGradle), + buildEvidence(standaloneFourManifest, + standaloneFourRoot, standaloneFourGradle) ]; const checkoutRoots = builds.map((build) => build.report.checkoutRoot); const gitDirectories = builds.map((build) => build.report.gitDirectory); @@ -251,14 +256,15 @@ const distinctGitDirectories = new Set(gitDirectories).size === 4; const distinctGradleHomes = new Set(gradleHomes).size === 4; const distinctInputManifestFiles = new Set(manifestPaths).size === 4; const standalonePublished = pair(builds[0], builds[1]); -const localComposite = pair(builds[2], builds[3]); +const standalonePublishedReplica = pair(builds[2], builds[3]); const passed = distinctCheckoutRoots && distinctGitDirectories && distinctGradleHomes && distinctInputManifestFiles && standalonePublished.status === 'passed' && - localComposite.status === 'passed'; + standalonePublishedReplica.status === 'passed'; const report = { - schema: 'blue-bex-independent-clean-builds/2.1', + schema: 'blue-bex-independent-clean-builds/3.0', status: passed ? 'passed' : 'failed', + dependencyPolicy: 'published-only', bexCommit, checkoutCount: checkoutRoots.length, gitDirectoryCount: gitDirectories.length, @@ -269,14 +275,14 @@ const report = { distinctGradleHomes, distinctInputManifestFiles, standalonePublished, - localComposite + standalonePublishedReplica }; writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`); if (!passed) { console.error(`Independent clean-build comparison failed: ${outputPath}`); for (const [label, comparison] of [ ['standalone-published', standalonePublished], - ['local-composite', localComposite] + ['standalone-published-replica', standalonePublishedReplica] ]) { if (comparison.status === 'passed') { continue; diff --git a/.github/scripts/compare-local-published-evidence.mjs b/.github/scripts/compare-local-published-evidence.mjs deleted file mode 100644 index 228b12f..0000000 --- a/.github/scripts/compare-local-published-evidence.mjs +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env node - -import { createHash } from 'node:crypto'; -import { readFileSync, writeFileSync } from 'node:fs'; - -const [localPath, publishedPath, bexCommit, outputPath] = process.argv.slice(2); -if (!localPath || !publishedPath || !bexCommit || !outputPath) { - throw new Error( - 'usage: compare-local-published-evidence.mjs LOCAL PUBLISHED COMMIT OUTPUT' - ); -} - -const local = JSON.parse(readFileSync(localPath, 'utf8')); -const published = JSON.parse(readFileSync(publishedPath, 'utf8')); - -const semanticKeys = [ - 'identities', - 'finalTotals', - 'normativeVectorCoverage', - 'operatorCoverage', - 'representationMatrix', - 'representationMatrixResult', - 'cacheMatrix', - 'recursionEvidence', - 'finiteLoopEvidence', - 'semanticBoundaryInvocationEvidence', - 'cyclicProofEvidence', - 'cyclicProofUnavailabilityCapability', - 'intrinsicEvidence', - 'referenceEvidenceClassificationEvidence', - 'hostedLocalLimitCapability', - 'ledgerLifecycleEvidence' -]; -const gasKeys = [ - 'identities', - 'finalTotals', - 'counterCoverage', - 'gasExhaustionEvidence', - 'gasExhaustionTraceExamples', - 'maximumObservedOrderedTraceEntries' -]; - -function selected(report, keys) { - return Object.fromEntries(keys.map((key) => [key, report[key] ?? null])); -} - -function canonical(value) { - if (Array.isArray(value)) { - return `[${value.map(canonical).join(',')}]`; - } - if (value && typeof value === 'object') { - return `{${Object.keys(value).sort().map( - (key) => `${JSON.stringify(key)}:${canonical(value[key])}` - ).join(',')}}`; - } - return JSON.stringify(value); -} - -function digest(value) { - return createHash('sha256').update(canonical(value)).digest('hex'); -} - -const localSemantic = selected(local, semanticKeys); -const publishedSemantic = selected(published, semanticKeys); -const localGas = selected(local, gasKeys); -const publishedGas = selected(published, gasKeys); -const semanticAndGasParity = - canonical(localSemantic) === canonical(publishedSemantic); -const exactGasTraceParity = canonical(localGas) === canonical(publishedGas); -const sourceBound = local.commit === bexCommit && published.commit === bexCommit; -const dependenciesDistinct = - local.dependency?.mode === 'local-composite' && - published.dependency?.mode === 'standalone-published'; -const passed = semanticAndGasParity && exactGasTraceParity && sourceBound && - dependenciesDistinct; - -const report = { - schema: 'blue-bex-local-published-differential/1.0', - status: passed ? 'passed' : 'failed', - bexCommit, - localMode: local.dependency?.mode ?? 'missing', - publishedMode: published.dependency?.mode ?? 'missing', - sourceBound, - dependenciesDistinct, - semanticAndGasParity: semanticAndGasParity ? 'passed' : 'failed', - exactGasTraceParity: exactGasTraceParity ? 'passed' : 'failed', - semanticEvidenceSha256: { - local: digest(localSemantic), - published: digest(publishedSemantic) - }, - gasEvidenceSha256: { - local: digest(localGas), - published: digest(publishedGas) - } -}; -writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`); -if (!passed) { - process.exitCode = 1; -} diff --git a/.github/scripts/compare-published-conformance-evidence.mjs b/.github/scripts/compare-published-conformance-evidence.mjs new file mode 100644 index 0000000..0d5d29b --- /dev/null +++ b/.github/scripts/compare-published-conformance-evidence.mjs @@ -0,0 +1,134 @@ +#!/usr/bin/env node + +import { createHash } from 'node:crypto'; +import { readFileSync, writeFileSync } from 'node:fs'; + +const [firstPath, secondPath, thirdPath, fourthPath, + bexCommit, outputPath] = process.argv.slice(2); +if (!firstPath || !secondPath || !thirdPath || !fourthPath || + !bexCommit || !outputPath) { + throw new Error( + 'usage: compare-published-conformance-evidence.mjs ' + + 'REPORT1 REPORT2 REPORT3 REPORT4 COMMIT OUTPUT' + ); +} + +const reports = [firstPath, secondPath, thirdPath, fourthPath] + .map((path) => JSON.parse(readFileSync(path, 'utf8'))); +if (!/^[0-9a-f]{40}$/.test(bexCommit)) { + throw new Error(`invalid BEX commit: ${bexCommit}`); +} +const semanticKeys = [ + 'identities', + 'finalTotals', + 'normativeVectorCoverage', + 'operatorCoverage', + 'representationMatrix', + 'representationMatrixResult', + 'cacheMatrix', + 'recursionEvidence', + 'finiteLoopEvidence', + 'semanticBoundaryInvocationEvidence', + 'cyclicProofEvidence', + 'cyclicProofUnavailabilityCapability', + 'intrinsicEvidence', + 'referenceEvidenceClassificationEvidence', + 'hostedLocalLimitCapability', + 'ledgerLifecycleEvidence' +]; +const gasKeys = [ + 'identities', + 'finalTotals', + 'counterCoverage', + 'gasExhaustionEvidence', + 'gasExhaustionTraceExamples', + 'maximumObservedOrderedTraceEntries' +]; + +function selected(report, keys) { + return Object.fromEntries(keys.map((key) => [key, report[key] ?? null])); +} + +function canonical(value) { + if (Array.isArray(value)) { + return `[${value.map(canonical).join(',')}]`; + } + if (value && typeof value === 'object') { + return `{${Object.keys(value).sort().map( + (key) => `${JSON.stringify(key)}:${canonical(value[key])}` + ).join(',')}}`; + } + return JSON.stringify(value); +} + +function digest(value) { + return createHash('sha256').update(canonical(value)).digest('hex'); +} + +function repeated(values) { + return values.length === 4 && values.every((value) => value === values[0]); +} + +const modes = reports.map((report) => report.dependency?.mode ?? 'missing'); +const coordinates = reports.map( + (report) => report.dependency?.declaredCoordinate ?? 'missing' +); +const artifactHashes = reports.map( + (report) => report.dependency?.resolution?.artifact?.sha256 ?? 'missing' +); +const semanticHashes = reports.map( + (report) => digest(selected(report, semanticKeys)) +); +const gasHashes = reports.map( + (report) => digest(selected(report, gasKeys)) +); +const allowedIncompleteGate = + 'independent-clean-build-reproducibility-gate-not-passing'; +const sourceBound = reports.every((report) => report.commit === bexCommit); +const publishedModes = modes.every((mode) => mode === 'standalone-published'); +const dependencyResolutionPassed = reports.every((report) => + report.dependency?.resolution?.status === 'passed' && + report.languageReleaseIdentity + ?.currentDependencyExactFinalArtifactProven === true +); +const noUnexpectedFailures = reports.every((report) => + Array.isArray(report.currentModeFailures) && + report.currentModeFailures.every( + (failure) => failure === allowedIncompleteGate + ) +); +const dependencyIdentityRepeated = repeated(coordinates) && + repeated(artifactHashes) && /^[0-9a-f]{64}$/.test(artifactHashes[0]); +const semanticAndGasRepeatability = repeated(semanticHashes); +const exactGasTraceRepeatability = repeated(gasHashes); +const passed = sourceBound && publishedModes && dependencyResolutionPassed && + noUnexpectedFailures && dependencyIdentityRepeated && + semanticAndGasRepeatability && exactGasTraceRepeatability; + +const report = { + schema: 'blue-bex-published-conformance-repeatability/1.0', + status: passed ? 'passed' : 'failed', + dependencyPolicy: 'published-only', + bexCommit, + runCount: reports.length, + modes, + coordinates, + sourceBound, + dependencyResolutionPassed, + noUnexpectedFailures, + dependencyIdentityRepeated, + semanticAndGasRepeatability: semanticAndGasRepeatability + ? 'passed' : 'failed', + exactGasTraceRepeatability: exactGasTraceRepeatability + ? 'passed' : 'failed', + semanticEvidenceSha256: semanticHashes, + gasEvidenceSha256: gasHashes, + dependencyArtifactSha256: artifactHashes +}; +writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`); +if (!passed) { + console.error( + `Published conformance repeatability failed: ${outputPath}` + ); + process.exitCode = 1; +} diff --git a/.github/scripts/run-final-publication-gates.sh b/.github/scripts/run-final-publication-gates.sh index 16558a6..6f7b9a4 100644 --- a/.github/scripts/run-final-publication-gates.sh +++ b/.github/scripts/run-final-publication-gates.sh @@ -4,20 +4,18 @@ set -euo pipefail readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly BEX_REPOSITORY="$(cd "$SCRIPT_DIR/../.." && pwd)" readonly INSPECTION_FILE="$BEX_REPOSITORY/src/test/resources/hosted-release/published-api-inspection.properties" -readonly LANGUAGE_REPOSITORY_URL="${BLUE_LANGUAGE_REPOSITORY_URL:-https://github.com/bluecontract/blue-language-java.git}" readonly RELEASE_ROOT="$(mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/blue-bex-publication.XXXXXX")" -readonly LANGUAGE_CHECKOUT="$RELEASE_ROOT/blue-language-java" readonly RECEIPT_ROOT="$RELEASE_ROOT/receipts" readonly INDEPENDENT_REPORT="$RECEIPT_ROOT/independent-clean-builds.json" -readonly DIFFERENTIAL_REPORT="$RECEIPT_ROOT/local-published-differential.json" +readonly REPEATABILITY_REPORT="$RECEIPT_ROOT/published-repeatability.json" readonly RETAINED_INPUT_ROOT="$BEX_REPOSITORY/build/reports/bex-release/inputs" readonly RETAINED_ARTIFACT_ROOT="$RETAINED_INPUT_ROOT/published-artifacts" readonly BEX_COMMIT="$(git -C "$BEX_REPOSITORY" rev-parse HEAD)" readonly SOURCE_COMMIT_EPOCH="$(git -C "$BEX_REPOSITORY" show -s --format=%ct "$BEX_COMMIT")" -release_succeeded=false +retain_release_root=false cleanup() { - if [[ "$release_succeeded" != true || -z "${GITHUB_ENV:-}" ]]; then + if [[ "$retain_release_root" != true ]]; then rm -rf "$RELEASE_ROOT" fi } @@ -41,23 +39,6 @@ sha256_file() { fi } -resolve_reviewed_aggregate() { - local repository="$1" - local coordinate="$2" - local output="$3" - local group artifact version remainder group_path url - IFS=: read -r group artifact version remainder <<< "$coordinate" - if [[ -z "$group" || -z "$artifact" || -z "$version" || -n "${remainder:-}" ]]; then - echo "Reviewed Language coordinate is not group:artifact:version: $coordinate" >&2 - exit 1 - fi - group_path="${group//./\/}" - url="${repository%/}/$group_path/$artifact/$version/$artifact-$version.jar" - mkdir -p "$(dirname "$output")" - curl --fail --silent --show-error --location --retry 3 \ - --output "$output" "$url" -} - artifact_manifest() { local checkout="$1" local output="$2" @@ -88,7 +69,6 @@ clone_bex() { run_isolated_build() { local checkout="$1" local gradle_home="$2" - local mode="$3" local arguments=( --no-daemon -p "$checkout" @@ -97,17 +77,15 @@ run_isolated_build() { bexConformance sourceReleaseArchive ) - if [[ "$mode" == "local-composite" ]]; then - arguments+=("-PblueLanguageCompositePath=$LANGUAGE_CHECKOUT") - fi GRADLE_USER_HOME="$gradle_home" \ "$checkout/gradlew" "${arguments[@]}" } readonly LANGUAGE_COMMIT="$(property_value source.commit)" -readonly LANGUAGE_TAG="$(property_value source.tag)" -readonly LANGUAGE_ARTIFACT_REPOSITORY="$(property_value repository)" readonly LANGUAGE_COORDINATE="$(property_value coordinate)" +readonly LANGUAGE_COORDINATE_TAIL="${LANGUAGE_COORDINATE#*:}" +readonly LANGUAGE_AGGREGATE_ARTIFACT="${LANGUAGE_COORDINATE_TAIL%%:*}" +readonly LANGUAGE_VERSION="${LANGUAGE_COORDINATE##*:}" readonly LANGUAGE_ARTIFACT_SHA256="$(property_value artifact.sha256)" readonly LANGUAGE_API_STATUS="$(property_value status)" readonly EXPECTED_RELEASE_TAG="v$(sed -n 's/^version = "\([^"]*\)"/\1/p' "$BEX_REPOSITORY/.cz.toml" | head -n 1)" @@ -116,6 +94,10 @@ if [[ "$LANGUAGE_API_STATUS" != "compatible-with-final-hosted-adapter" ]]; then echo "Published Language inspection is not compatible: $LANGUAGE_API_STATUS" >&2 exit 1 fi +if [[ ! "$LANGUAGE_COORDINATE" =~ ^blue\.language:blue-language-java:[0-9A-Za-z][0-9A-Za-z._-]*$ ]]; then + echo "Published Language aggregate coordinate is invalid: $LANGUAGE_COORDINATE" >&2 + exit 1 +fi if [[ ! "$LANGUAGE_COMMIT" =~ ^[0-9a-fA-F]{40}$ ]]; then echo "Published Language source commit is missing." >&2 exit 1 @@ -133,110 +115,129 @@ if ! git -C "$BEX_REPOSITORY" tag --points-at HEAD | grep -Fxq "$EXPECTED_RELEAS exit 1 fi -git clone --quiet --filter=blob:none --no-checkout \ - "$LANGUAGE_REPOSITORY_URL" "$LANGUAGE_CHECKOUT" -git -C "$LANGUAGE_CHECKOUT" fetch --quiet --depth=1 origin \ - "refs/tags/$LANGUAGE_TAG:refs/tags/$LANGUAGE_TAG" -git -C "$LANGUAGE_CHECKOUT" checkout --quiet --detach "refs/tags/$LANGUAGE_TAG" -if [[ "$(git -C "$LANGUAGE_CHECKOUT" rev-parse HEAD)" != "$LANGUAGE_COMMIT" ]]; then - echo "Published Language tag does not resolve to the reviewed commit." >&2 - exit 1 -fi -if [[ -n "$(git -C "$LANGUAGE_CHECKOUT" status --porcelain --untracked-files=all)" ]]; then - echo "Language release checkout is dirty." >&2 - exit 1 -fi - export CI=true export SOURCE_DATE_EPOCH="$SOURCE_COMMIT_EPOCH" mkdir -p "$RECEIPT_ROOT" cd "$BEX_REPOSITORY" -./gradlew --no-daemon clean bexWorkingVerification \ - "-PblueLanguageCompositePath=$LANGUAGE_CHECKOUT" -./gradlew --no-daemon bexModernizationVerification \ - "-PblueLanguageCompositePath=$LANGUAGE_CHECKOUT" declare -a checkouts=( "$RELEASE_ROOT/standalone-one" "$RELEASE_ROOT/standalone-two" - "$RELEASE_ROOT/local-one" - "$RELEASE_ROOT/local-two" + "$RELEASE_ROOT/standalone-three" + "$RELEASE_ROOT/standalone-four" ) for checkout in "${checkouts[@]}"; do clone_bex "$checkout" done -run_isolated_build "${checkouts[0]}" "$RELEASE_ROOT/gradle-standalone-one" standalone-published -run_isolated_build "${checkouts[1]}" "$RELEASE_ROOT/gradle-standalone-two" standalone-published -run_isolated_build "${checkouts[2]}" "$RELEASE_ROOT/gradle-local-one" local-composite -run_isolated_build "${checkouts[3]}" "$RELEASE_ROOT/gradle-local-two" local-composite +run_isolated_build "${checkouts[0]}" "$RELEASE_ROOT/gradle-standalone-one" +run_isolated_build "${checkouts[1]}" "$RELEASE_ROOT/gradle-standalone-two" +run_isolated_build "${checkouts[2]}" "$RELEASE_ROOT/gradle-standalone-three" +run_isolated_build "${checkouts[3]}" "$RELEASE_ROOT/gradle-standalone-four" readonly STANDALONE_ONE_MANIFEST="$RECEIPT_ROOT/standalone-one.sha256" readonly STANDALONE_TWO_MANIFEST="$RECEIPT_ROOT/standalone-two.sha256" -readonly LOCAL_ONE_MANIFEST="$RECEIPT_ROOT/local-one.sha256" -readonly LOCAL_TWO_MANIFEST="$RECEIPT_ROOT/local-two.sha256" +readonly STANDALONE_THREE_MANIFEST="$RECEIPT_ROOT/standalone-three.sha256" +readonly STANDALONE_FOUR_MANIFEST="$RECEIPT_ROOT/standalone-four.sha256" artifact_manifest "${checkouts[0]}" "$STANDALONE_ONE_MANIFEST" artifact_manifest "${checkouts[1]}" "$STANDALONE_TWO_MANIFEST" -artifact_manifest "${checkouts[2]}" "$LOCAL_ONE_MANIFEST" -artifact_manifest "${checkouts[3]}" "$LOCAL_TWO_MANIFEST" +artifact_manifest "${checkouts[2]}" "$STANDALONE_THREE_MANIFEST" +artifact_manifest "${checkouts[3]}" "$STANDALONE_FOUR_MANIFEST" +comparison_failed=false if ! node "$SCRIPT_DIR/compare-independent-builds.mjs" \ "$STANDALONE_ONE_MANIFEST" "$STANDALONE_TWO_MANIFEST" \ - "$LOCAL_ONE_MANIFEST" "$LOCAL_TWO_MANIFEST" \ + "$STANDALONE_THREE_MANIFEST" "$STANDALONE_FOUR_MANIFEST" \ "${checkouts[0]}" "${checkouts[1]}" \ "${checkouts[2]}" "${checkouts[3]}" \ "$RELEASE_ROOT/gradle-standalone-one" \ "$RELEASE_ROOT/gradle-standalone-two" \ - "$RELEASE_ROOT/gradle-local-one" \ - "$RELEASE_ROOT/gradle-local-two" \ + "$RELEASE_ROOT/gradle-standalone-three" \ + "$RELEASE_ROOT/gradle-standalone-four" \ "$BEX_COMMIT" "$INDEPENDENT_REPORT"; then + comparison_failed=true +fi +if ! node "$SCRIPT_DIR/compare-published-conformance-evidence.mjs" \ + "${checkouts[0]}/blue-bex-conformance/build/reports/bex-conformance/report.json" \ + "${checkouts[1]}/blue-bex-conformance/build/reports/bex-conformance/report.json" \ + "${checkouts[2]}/blue-bex-conformance/build/reports/bex-conformance/report.json" \ + "${checkouts[3]}/blue-bex-conformance/build/reports/bex-conformance/report.json" \ + "$BEX_COMMIT" "$REPEATABILITY_REPORT"; then + comparison_failed=true +fi +if [[ "$comparison_failed" == true ]]; then mkdir -p "$RETAINED_INPUT_ROOT" - cp "$INDEPENDENT_REPORT" \ + [[ ! -f "$INDEPENDENT_REPORT" ]] || cp "$INDEPENDENT_REPORT" \ "$RETAINED_INPUT_ROOT/independent-clean-builds.json" - echo "Retained failed independent-build report under build/reports." >&2 + [[ ! -f "$REPEATABILITY_REPORT" ]] || cp "$REPEATABILITY_REPORT" \ + "$RETAINED_INPUT_ROOT/published-repeatability.json" + echo "Retained failed published-only comparison reports under build/reports." >&2 exit 1 fi -node "$SCRIPT_DIR/compare-local-published-evidence.mjs" \ - "${checkouts[2]}/blue-bex-conformance/build/reports/bex-conformance/report.json" \ - "${checkouts[0]}/blue-bex-conformance/build/reports/bex-conformance/report.json" \ - "$BEX_COMMIT" "$DIFFERENTIAL_REPORT" - -readonly REVIEWED_AGGREGATE_ARTIFACT="$RECEIPT_ROOT/published-language-aggregate.jar" -resolve_reviewed_aggregate \ - "$LANGUAGE_ARTIFACT_REPOSITORY" \ - "$LANGUAGE_COORDINATE" \ - "$REVIEWED_AGGREGATE_ARTIFACT" +readonly PUBLISHED_CACHE_ROOT="$RELEASE_ROOT/gradle-standalone-one/caches/modules-2/files-2.1/blue.language" +aggregate_artifacts=() +while IFS= read -r -d '' artifact; do + aggregate_artifacts+=("$artifact") +done < <(find \ + "$PUBLISHED_CACHE_ROOT/$LANGUAGE_AGGREGATE_ARTIFACT/$LANGUAGE_VERSION" \ + -type f \ + -name "$LANGUAGE_AGGREGATE_ARTIFACT-$LANGUAGE_VERSION.jar" \ + -print0) +if [[ "${#aggregate_artifacts[@]}" -ne 1 ]]; then + echo "Expected exactly one resolved aggregate Language JAR; found ${#aggregate_artifacts[@]}." >&2 + exit 1 +fi +readonly REVIEWED_AGGREGATE_ARTIFACT="${aggregate_artifacts[0]}" if [[ "$(sha256_file "$REVIEWED_AGGREGATE_ARTIFACT")" != "$LANGUAGE_ARTIFACT_SHA256" ]]; then echo "Resolved aggregate Language artifact does not match the reviewed SHA-256." >&2 exit 1 fi -published_artifacts=("$REVIEWED_AGGREGATE_ARTIFACT") -while IFS= read -r -d '' artifact; do - published_artifacts+=("$artifact") -done < <(find \ - "$RELEASE_ROOT/gradle-standalone-one/caches/modules-2/files-2.1/blue.language" \ - -type f -name '*.jar' -print0) -if [[ "${#published_artifacts[@]}" -eq 1 ]]; then - echo "No focused published Language module artifacts were resolved in the isolated cache." >&2 +IFS=',' read -r -a FOCUSED_RUNTIME_LANGUAGE_MODULES <<< \ + "$(property_value release.resolvedRuntimeArtifacts)" +if [[ "${#FOCUSED_RUNTIME_LANGUAGE_MODULES[@]}" -eq 0 ]]; then + echo "Reviewed runtime Language artifact list is empty." >&2 exit 1 fi -artifact_match=false -for artifact in "${published_artifacts[@]}"; do - if [[ "$(sha256_file "$artifact")" == "$LANGUAGE_ARTIFACT_SHA256" ]]; then - artifact_match=true +published_artifacts=("$REVIEWED_AGGREGATE_ARTIFACT") +for module in "${FOCUSED_RUNTIME_LANGUAGE_MODULES[@]}"; do + module_artifacts=() + while IFS= read -r -d '' artifact; do + module_artifacts+=("$artifact") + done < <(find "$PUBLISHED_CACHE_ROOT/$module/$LANGUAGE_VERSION" \ + -type f -name "$module-$LANGUAGE_VERSION.jar" -print0) + if [[ "${#module_artifacts[@]}" -ne 1 ]]; then + echo "Expected exactly one resolved $module $LANGUAGE_VERSION JAR; found ${#module_artifacts[@]}." >&2 + exit 1 + fi + artifact="${module_artifacts[0]}" + expected_hash="$(property_value "artifact.$module.sha256")" + if [[ ! "$expected_hash" =~ ^[0-9a-f]{64}$ \ + || "$(sha256_file "$artifact")" != "$expected_hash" ]]; then + echo "Resolved $module JAR does not match its reviewed SHA-256." >&2 + exit 1 fi + published_artifacts+=("$artifact") done -if [[ "$artifact_match" != true ]]; then - echo "No resolved Language artifact matches the reviewed aggregate SHA-256." >&2 +readonly EXPECTED_ARTIFACT_COUNT="$((${#FOCUSED_RUNTIME_LANGUAGE_MODULES[@]} + 1))" +if [[ "${#published_artifacts[@]}" -ne "$EXPECTED_ARTIFACT_COUNT" ]]; then + echo "Published Language artifact set is incomplete." >&2 exit 1 fi + +# Clean the root checkout before retaining comparison inputs. The next single +# invocation uses this same empty-cache Gradle home for the complete working, +# modernization, and release graph. +readonly ROOT_RELEASE_GRADLE_HOME="$RELEASE_ROOT/gradle-root-release" +GRADLE_USER_HOME="$ROOT_RELEASE_GRADLE_HOME" \ + ./gradlew --no-daemon clean + mkdir -p "$RETAINED_ARTIFACT_ROOT" cp "$INDEPENDENT_REPORT" \ "$RETAINED_INPUT_ROOT/independent-clean-builds.json" -cp "$DIFFERENTIAL_REPORT" \ - "$RETAINED_INPUT_ROOT/local-published-differential.json" +cp "$REPEATABILITY_REPORT" \ + "$RETAINED_INPUT_ROOT/published-repeatability.json" for artifact in "${published_artifacts[@]}"; do cp "$artifact" "$RETAINED_ARTIFACT_ROOT/$(basename "$artifact")" done @@ -246,27 +247,22 @@ while IFS= read -r -d '' artifact; do done < <(find "$RETAINED_ARTIFACT_ROOT" -type f -name '*.jar' -print0) readonly RETAINED_ARTIFACT_PATHS="$(IFS=:; echo "${retained_artifacts[*]}")" -# Re-run the root conformance surface in an exact-version empty cache so its -# detailed mode matrix observes both local-composite and published execution. -GRADLE_USER_HOME="$RELEASE_ROOT/gradle-root-standalone" \ - ./gradlew --no-daemon bexConformance -# The modernization report depends on the working lifecycle and therefore -# still requires the reviewed local Language composite. -./gradlew --no-daemon generateBexModernizationReport \ - "-PblueLanguageCompositePath=$LANGUAGE_CHECKOUT" - -./gradlew --no-daemon bexReleaseVerify \ - "-PblueLanguageCompositePath=$LANGUAGE_CHECKOUT" \ +# Run the complete root release surface once against one fresh cache. The task +# graph includes the published working and modernization gates. +GRADLE_USER_HOME="$ROOT_RELEASE_GRADLE_HOME" \ + ./gradlew --no-daemon bexReleaseVerify \ "-PbexPublishedLanguageCoordinate=$LANGUAGE_COORDINATE" \ "-PbexPublishedLanguageSha256=$LANGUAGE_ARTIFACT_SHA256" \ "-PbexPublishedLanguageArtifacts=$RETAINED_ARTIFACT_PATHS" \ - "-PbexLocalPublishedDifferential=$RETAINED_INPUT_ROOT/local-published-differential.json" \ + "-PbexPublishedRepeatability=$RETAINED_INPUT_ROOT/published-repeatability.json" \ "-PbexIndependentCleanBuildReport=$RETAINED_INPUT_ROOT/independent-clean-builds.json" jq -e '.releaseReady == true' \ "$BEX_REPOSITORY/build/reports/bex-release/final.json" >/dev/null -if [[ -n "${GITHUB_ENV:-}" ]]; then - printf 'BLUE_LANGUAGE_COMPOSITE_PATH=%s\n' "$LANGUAGE_CHECKOUT" >> "$GITHUB_ENV" -fi -release_succeeded=true +# The strict report deliberately re-opens every independent checkout and +# manifest instead of trusting JSON alone. Keep those ephemeral inputs alive +# for the subsequent publish and JReleaser Gradle invocations. RUNNER_TEMP is +# discarded with the CI job; failed gates still remove it via the EXIT trap. +retain_release_root=true +echo "Retained live publication evidence at $RELEASE_ROOT" diff --git a/.github/workflows/release-rc.yml b/.github/workflows/release-rc.yml index 85b3107..90bedfe 100644 --- a/.github/workflows/release-rc.yml +++ b/.github/workflows/release-rc.yml @@ -43,13 +43,13 @@ jobs: run: git fetch origin main:refs/remotes/origin/main --tags - name: Set up Java 8 test runtime - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '8' distribution: 'corretto' - name: Set up JDK 25 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '25' distribution: 'corretto' @@ -60,7 +60,7 @@ jobs: node-version: '22' - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v4 - name: Configure Git run: | @@ -78,26 +78,23 @@ jobs: git tag -a "v${{ steps.version.outputs.version }}" -m "Release ${{ steps.version.outputs.version }}" - name: Prove full BEX publication readiness - env: - BLUE_LANGUAGE_REPOSITORY_URL: https://github.com/bluecontract/blue-language-java.git run: bash .github/scripts/run-final-publication-gates.sh - name: Execute Gradle publish - run: >- - ./gradlew publish - -PblueLanguageCompositePath="$BLUE_LANGUAGE_COMPOSITE_PATH" + env: + GRADLE_USER_HOME: ${{ runner.temp }}/blue-bex-publish-gradle-home + run: ./gradlew publish - name: Execute Gradle release env: + GRADLE_USER_HOME: ${{ runner.temp }}/blue-bex-jreleaser-gradle-home JRELEASER_GITHUB_TOKEN: ${{ secrets.WORKFLOW_PAT }} JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.MAVENCENTRAL_USERNAME }} JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.MAVENCENTRAL_PASSWORD }} JRELEASER_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.GPG_PUBLIC_KEY }} JRELEASER_GPG_SECRET_KEY: ${{ secrets.GPG_SECRET_KEY }} - run: >- - ./gradlew jreleaserFullRelease - -PblueLanguageCompositePath="$BLUE_LANGUAGE_COMPOSITE_PATH" + run: ./gradlew jreleaserFullRelease - name: Push release commit and tag run: git push origin HEAD:next --follow-tags diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a049b70..b84d1bd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,41 +23,43 @@ jobs: fi - name: Set up Java 8 test runtime - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '8' distribution: 'corretto' - name: Set up JDK 25 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '25' distribution: 'corretto' + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v4 - name: Prove full BEX publication readiness - env: - BLUE_LANGUAGE_REPOSITORY_URL: https://github.com/bluecontract/blue-language-java.git run: bash .github/scripts/run-final-publication-gates.sh - name: Execute Gradle publish - run: >- - ./gradlew publish - -PblueLanguageCompositePath="$BLUE_LANGUAGE_COMPOSITE_PATH" + env: + GRADLE_USER_HOME: ${{ runner.temp }}/blue-bex-publish-gradle-home + run: ./gradlew publish - name: Execute Gradle release env: + GRADLE_USER_HOME: ${{ runner.temp }}/blue-bex-jreleaser-gradle-home JRELEASER_GITHUB_TOKEN: ${{ secrets.WORKFLOW_PAT }} JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.MAVENCENTRAL_USERNAME }} JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.MAVENCENTRAL_PASSWORD }} JRELEASER_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.GPG_PUBLIC_KEY }} JRELEASER_GPG_SECRET_KEY: ${{ secrets.GPG_SECRET_KEY }} - run: >- - ./gradlew jreleaserFullRelease - -PblueLanguageCompositePath="$BLUE_LANGUAGE_COMPOSITE_PATH" + run: ./gradlew jreleaserFullRelease - name: Archive artifacts uses: actions/upload-artifact@v4 diff --git a/README.md b/README.md index 5272b62..006b919 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ Two fail-closed gates intentionally answer different questions: ```text bexWorkingVerification exact local modular Blue Language checkout +bexSdkStageVerify exact isolated candidate-repository verification bexReleaseVerify independently reproducible published dependencies ``` @@ -118,6 +119,13 @@ evidence must remain red or `not-executed`. This README does not claim that an unexecuted gate, benchmark, or release has passed. Consult the current generated reports under `build/reports`. +`bexSdkStageVerify` is a third, deliberately non-publication lane. It resolves +the locked Language candidate only from the explicit staged repository, hashes +every focused staged Language JAR against the artifact actually resolved, and +fails on any same-run conformance blocker. It never publishes BEX artifacts; +the separate `publish` task performs that mutation after verification. See +[Release](docs/release.md) for the exact commands and source lock. + The normative package currently contains 60 vectors, 105 behavior fixtures, 30 gas microfixtures, and coverage for 86 operators. Exact identities are recorded in [Conformance](docs/conformance.md). diff --git a/blue-bex-conformance/build.gradle.kts b/blue-bex-conformance/build.gradle.kts index 4295f14..8b7f1c9 100644 --- a/blue-bex-conformance/build.gradle.kts +++ b/blue-bex-conformance/build.gradle.kts @@ -570,15 +570,21 @@ val writeBexConformanceReport = tasks.register( mainClass.set("blue.bex.conformance.BexConformanceReportMain") val composite = providers.gradleProperty("blueLanguageCompositePath") .orElse("") + val stagedRepository = providers.gradleProperty("blueLanguageRepository") + .orElse("") doFirst { val compositePath = composite.get() + val stagedRepositoryPath = stagedRepository.get() setArgs(listOf( rootProject.projectDir.absolutePath, layout.buildDirectory.get().asFile.absolutePath, gradle.gradleVersion, project.version.toString(), - if (compositePath.isBlank()) - "standalone-published" else "local-composite", + when { + compositePath.isNotBlank() -> "local-composite" + stagedRepositoryPath.isNotBlank() -> "staged-repository" + else -> "standalone-published" + }, "blue.language:blue-language-java:$languageVersion", rootProject.layout.projectDirectory.dir( ".gradle/bex-hosted-release").asFile.absolutePath, diff --git a/blue-bex-contracts/src/main/java/blue/bex/contracts/BexContractsExecutionContext.java b/blue-bex-contracts/src/main/java/blue/bex/contracts/BexContractsExecutionContext.java index a8c4347..a95ca7d 100644 --- a/blue-bex-contracts/src/main/java/blue/bex/contracts/BexContractsExecutionContext.java +++ b/blue-bex-contracts/src/main/java/blue/bex/contracts/BexContractsExecutionContext.java @@ -55,7 +55,10 @@ public static BexExecutionContext.Builder configure( new ProcessorExecutionContextBexSemanticIdentityBoundary( exactContext)); exactBuilder.failureBoundary(BexContractsFailureBoundary.INSTANCE); - exactBuilder.event(BexValues.nodeSnapshot(exactContext.event())); + FrozenNode exactEvent = exactContext.frozenEvent(); + exactBuilder.event(exactEvent != null + ? BexValues.frozen(exactEvent) + : BexValues.nodeSnapshot(exactContext.event())); FrozenNode processEvent = exactContext.frozenProcessEvent(); exactBuilder.processingEvent(processEvent != null ? BexValues.frozen(processEvent) diff --git a/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExactBlueValueCapability.java b/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExactBlueValueCapability.java new file mode 100644 index 0000000..dfb4c56 --- /dev/null +++ b/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExactBlueValueCapability.java @@ -0,0 +1,31 @@ +package blue.bex.contracts; + +import blue.bex.output.BexExactValueCapability; +import blue.language.processor.ExactBlueValue; + +import java.util.Objects; + +/** + * Processor-hosted BEX capability for one invocation-admitted exact value. + * + *

The constructor is package-private so only the Contracts bridge can wrap + * a Language-issued handle. Consumers may carry the handle back into the same + * processor invocation; Language revalidates ownership at that boundary.

+ */ +public final class ProcessorExactBlueValueCapability + implements BexExactValueCapability { + private final ExactBlueValue exactValue; + + ProcessorExactBlueValueCapability(ExactBlueValue exactValue) { + this.exactValue = Objects.requireNonNull(exactValue, "exactValue"); + } + + /** + * Returns the Language-issued invocation capability. + * + * @return the exact value capability owned by the current invocation + */ + public ExactBlueValue exactValue() { + return exactValue; + } +} diff --git a/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexGasLedgerHost.java b/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexGasLedgerHost.java index 0918ad9..e4bc217 100644 --- a/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexGasLedgerHost.java +++ b/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexGasLedgerHost.java @@ -235,11 +235,7 @@ public void charge( delegate.charge( counter, quantity, - GasChargeContext.of( - exact.scopePath(), - exact.contractKey(), - exact.logicalPath(), - exact.reason())); + contractsAttribution(exact)); } catch (GasLimitExceededException exhausted) { throw new BexHostGasExhaustion( exhausted.namespace(), @@ -252,4 +248,15 @@ public void charge( } } } + + static GasChargeContext contractsAttribution( + BexGasChargeContext context) { + BexGasChargeContext exact = context != null + ? context : BexGasChargeContext.empty(); + return GasChargeContext.of( + null, + exact.contractKey(), + exact.logicalPath(), + exact.reason()); + } } diff --git a/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexSemanticIdentityBoundary.java b/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexSemanticIdentityBoundary.java index cf760b4..a63c268 100644 --- a/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexSemanticIdentityBoundary.java +++ b/blue-bex-contracts/src/main/java/blue/bex/contracts/ProcessorExecutionContextBexSemanticIdentityBoundary.java @@ -5,6 +5,7 @@ import blue.language.model.Node; import blue.language.processor.ExactBlueValue; import blue.language.processor.ProcessorExecutionContext; +import blue.language.snapshot.FrozenNode; import java.util.Objects; @@ -24,6 +25,31 @@ public BexEstablishedIdentity establishIdentity(Node node) { Objects.requireNonNull(node, "node")); return new BexEstablishedIdentity( exact.blueId(), - exact.frozenValue()); + exact.frozenValue(), + new ProcessorExactBlueValueCapability(exact)); } + + @Override + public BexEstablishedIdentity carryExactIdentity( + String blueId, + FrozenNode frozenValue) { + ExactBlueValue exact = context.semanticOutputBoundary() + .carryExactValue( + Objects.requireNonNull(blueId, "blueId"), + Objects.requireNonNull(frozenValue, "frozenValue")); + if (!Objects.requireNonNull(blueId, "blueId").equals( + exact.blueId())) { + throw new IllegalArgumentException( + "Exact BEX value identity changed at the processor boundary: " + + "expected " + blueId + " but found " + + exact.blueId() + " (strict=" + + frozenValue.isStrictCanonical() + ", reference=" + + frozenValue.isReferenceOnly() + ")"); + } + return new BexEstablishedIdentity( + exact.blueId(), + exact.frozenValue(), + new ProcessorExactBlueValueCapability(exact)); + } + } diff --git a/blue-bex-contracts/src/test/java/blue/bex/contracts/ProcessorExecutionContextBexGasLedgerHostTest.java b/blue-bex-contracts/src/test/java/blue/bex/contracts/ProcessorExecutionContextBexGasLedgerHostTest.java new file mode 100644 index 0000000..b8a1b35 --- /dev/null +++ b/blue-bex-contracts/src/test/java/blue/bex/contracts/ProcessorExecutionContextBexGasLedgerHostTest.java @@ -0,0 +1,27 @@ +package blue.bex.contracts; + +import blue.bex.gas.BexGasChargeContext; +import blue.language.processor.GasChargeContext; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +final class ProcessorExecutionContextBexGasLedgerHostTest { + + @Test + void keepsBexDiagnosticPathsOutOfManagedScopeAttribution() { + GasChargeContext projected = + ProcessorExecutionContextBexGasLedgerHost + .contractsAttribution(BexGasChargeContext.of( + "function $root /do/0/$appendChange", + "compute", + "$appendChange", + "execute statement")); + + assertNull(projected.scopePath()); + assertEquals("compute", projected.contractKey()); + assertEquals("$appendChange", projected.logicalPath()); + assertEquals("execute statement", projected.reason()); + } +} diff --git a/blue-bex-core/src/main/java/blue/bex/output/BexAdmittedValue.java b/blue-bex-core/src/main/java/blue/bex/output/BexAdmittedValue.java index 5a2e824..25e3233 100644 --- a/blue-bex-core/src/main/java/blue/bex/output/BexAdmittedValue.java +++ b/blue-bex-core/src/main/java/blue/bex/output/BexAdmittedValue.java @@ -14,18 +14,21 @@ public final class BexAdmittedValue { private final Node node; private final String nodeBlueId; private final boolean reconstructed; + private final BexExactValueCapability exactCapability; BexAdmittedValue(BexValue value, BexValue semanticValue, Node node, String nodeBlueId, - boolean reconstructed) { + boolean reconstructed, + BexExactValueCapability exactCapability) { this.value = Objects.requireNonNull(value, "value"); this.semanticValue = Objects.requireNonNull( semanticValue, "semanticValue"); this.node = Objects.requireNonNull(node, "node"); this.nodeBlueId = Objects.requireNonNull(nodeBlueId, "nodeBlueId"); this.reconstructed = reconstructed; + this.exactCapability = exactCapability; } /** @@ -60,4 +63,13 @@ public String nodeBlueId() { public boolean reconstructed() { return reconstructed; } + + /** + * Returns the optional opaque exact capability retained by the host. + * + * @return the retained capability, or {@code null} when none was supplied + */ + public BexExactValueCapability exactCapability() { + return exactCapability; + } } diff --git a/blue-bex-core/src/main/java/blue/bex/output/BexEstablishedIdentity.java b/blue-bex-core/src/main/java/blue/bex/output/BexEstablishedIdentity.java index e26a5f9..98c40f1 100644 --- a/blue-bex-core/src/main/java/blue/bex/output/BexEstablishedIdentity.java +++ b/blue-bex-core/src/main/java/blue/bex/output/BexEstablishedIdentity.java @@ -15,13 +15,22 @@ public final class BexEstablishedIdentity { private final String blueId; private final FrozenNode frozenValue; + private final BexExactValueCapability exactCapability; public BexEstablishedIdentity(String blueId, FrozenNode frozenValue) { + this(blueId, frozenValue, null); + } + + public BexEstablishedIdentity( + String blueId, + FrozenNode frozenValue, + BexExactValueCapability exactCapability) { this.blueId = BlueIds.requireBlueIdOrCyclicMember( Objects.requireNonNull(blueId, "blueId"), "BEX established output blueId"); this.frozenValue = Objects.requireNonNull( frozenValue, "frozenValue"); + this.exactCapability = exactCapability; } public String blueId() { @@ -31,4 +40,13 @@ public String blueId() { public FrozenNode frozenValue() { return frozenValue; } + + /** + * Returns the optional opaque capability supplied by the host boundary. + * + * @return the host capability, or {@code null} when none was supplied + */ + public BexExactValueCapability exactCapability() { + return exactCapability; + } } diff --git a/blue-bex-core/src/main/java/blue/bex/output/BexExactValueCapability.java b/blue-bex-core/src/main/java/blue/bex/output/BexExactValueCapability.java new file mode 100644 index 0000000..2989c7c --- /dev/null +++ b/blue-bex-core/src/main/java/blue/bex/output/BexExactValueCapability.java @@ -0,0 +1,12 @@ +package blue.bex.output; + +/** + * Opaque host-owned capability accompanying an exact BEX output value. + * + *

BEX never interprets or manufactures implementations. A hosted runtime + * may retain an invocation-scoped proof here so its result adapter can carry + * the already-admitted value without reopening a provider or rebuilding the + * value from its semantic cursor.

+ */ +public interface BexExactValueCapability { +} diff --git a/blue-bex-core/src/main/java/blue/bex/output/BexOutputAdmission.java b/blue-bex-core/src/main/java/blue/bex/output/BexOutputAdmission.java index ec0bbe2..d1bf700 100644 --- a/blue-bex-core/src/main/java/blue/bex/output/BexOutputAdmission.java +++ b/blue-bex-core/src/main/java/blue/bex/output/BexOutputAdmission.java @@ -6,6 +6,7 @@ import blue.bex.value.BexBlueNodeWriter; import blue.bex.value.BexValue; import blue.bex.value.BexValues; +import blue.bex.value.BexFrozenWriter; import blue.language.model.Node; import blue.language.snapshot.FrozenNode; import blue.language.identity.BlueIds; @@ -57,12 +58,22 @@ public BexAdmittedValue admit(BexValue value, BexOutputKind kind) { if (value.isExact()) { String exactId = BlueIds.requireBlueIdOrCyclicMember( value.exactBlueId(), "BEX exact output blueId"); + BexEstablishedIdentity carried = Objects.requireNonNull( + semanticIdentity.carryExactIdentity( + exactId, + BexFrozenWriter.toFrozen(value)), + "carried exact identity"); + if (!exactId.equals(carried.blueId())) { + throw new BexException( + "Exact BEX output identity changed at the host boundary"); + } return new BexAdmittedValue( value, value, new Node().blueId(exactId), exactId, - false); + false, + carried.exactCapability()); } BexAdmittedValue prior = admittedTransientValues.get(value); @@ -103,7 +114,8 @@ public BexAdmittedValue admit(BexValue value, BexOutputKind kind) { EstablishedTransientIdentity identity = new EstablishedTransientIdentity( established.frozenValue(), - blueId); + blueId, + established.exactCapability()); BexAdmittedValue admitted = identity.admit(value); admittedTransientValues.put(value, admitted); @@ -113,14 +125,17 @@ public BexAdmittedValue admit(BexValue value, BexOutputKind kind) { private static final class EstablishedTransientIdentity { private final FrozenNode frozenValue; private final String blueId; + private final BexExactValueCapability exactCapability; private EstablishedTransientIdentity( FrozenNode frozenValue, - String blueId) { + String blueId, + BexExactValueCapability exactCapability) { this.frozenValue = Objects.requireNonNull( frozenValue, "frozenValue"); this.blueId = Objects.requireNonNull( blueId, "blueId"); + this.exactCapability = exactCapability; } private BexAdmittedValue admit( @@ -134,7 +149,8 @@ private BexAdmittedValue admit( exact, frozenValue.toNode(), blueId, - true); + true, + exactCapability); } } diff --git a/blue-bex-core/src/main/java/blue/bex/output/BexSemanticIdentityBoundary.java b/blue-bex-core/src/main/java/blue/bex/output/BexSemanticIdentityBoundary.java index d84ed2c..13f0d53 100644 --- a/blue-bex-core/src/main/java/blue/bex/output/BexSemanticIdentityBoundary.java +++ b/blue-bex-core/src/main/java/blue/bex/output/BexSemanticIdentityBoundary.java @@ -23,4 +23,22 @@ public interface BexSemanticIdentityBoundary { }; BexEstablishedIdentity establishIdentity(Node node); + + /** + * Carries an existing exact value through the host boundary. + * + *

The standalone boundary retains only the supplied identity and + * representation. Hosted boundaries may override this method to attach an + * invocation-owned exact capability.

+ * + * @param blueId the already established exact Blue identity + * @param frozenValue the verified frozen value carrying that identity + * @return the established identity and any host-owned exact capability + */ + default BexEstablishedIdentity carryExactIdentity( + String blueId, + FrozenNode frozenValue) { + return new BexEstablishedIdentity(blueId, frozenValue); + } + } diff --git a/blue-bex-core/src/main/java/blue/bex/value/AdmittedExactBexValue.java b/blue-bex-core/src/main/java/blue/bex/value/AdmittedExactBexValue.java index e0d4bc0..2f1b787 100644 --- a/blue-bex-core/src/main/java/blue/bex/value/AdmittedExactBexValue.java +++ b/blue-bex-core/src/main/java/blue/bex/value/AdmittedExactBexValue.java @@ -105,6 +105,13 @@ public BexValue get(String key) { if (establishedChild.isExact() && establishedChild.exactBlueId().equals( suppliedChild.exactBlueId())) { + /* + * The run-local child already carries this exact identity and + * its verified semantic cursor. Rewrapping it behind the + * aggregate's canonical pure reference would discard that + * cursor and spuriously require provider evidence for ordinary + * reads of the just-produced value. + */ return suppliedChild; } /* @@ -170,6 +177,14 @@ Object rawScalar() { return BexValues.rawScalar(establishedValue); } + FrozenNode establishedFrozenValue() { + if (!(establishedValue instanceof FrozenNodeBexValue)) { + throw new IllegalStateException( + "Admitted exact value has no frozen host representation"); + } + return ((FrozenNodeBexValue) establishedValue).canonicalNode(); + } + /* * Blue's resolved frozen representation retains an empty object member as * an empty node, which is also the representation of BEX null. The exact diff --git a/blue-bex-core/src/main/java/blue/bex/value/BexFrozenWriter.java b/blue-bex-core/src/main/java/blue/bex/value/BexFrozenWriter.java index 97ac29e..86fb42f 100644 --- a/blue-bex-core/src/main/java/blue/bex/value/BexFrozenWriter.java +++ b/blue-bex-core/src/main/java/blue/bex/value/BexFrozenWriter.java @@ -33,6 +33,15 @@ private FrozenNode toFrozenInternal(BexValue value) { if (value instanceof FrozenNodeBexValue) { return ((FrozenNodeBexValue) value).canonicalNode(); } + if (value instanceof AdmittedExactBexValue) { + /* + * The semantic-output host has already authenticated this exact + * frozen representation. Re-materializing the supplied diagnostic + * cursor can generalize nominal types or schema fields and thereby + * change the identity the host established. + */ + return ((AdmittedExactBexValue) value).establishedFrozenValue(); + } if (metrics != null) { metrics.incrementFrozenWriterNodeFallbacks(); } diff --git a/blue-bex-core/src/main/java/blue/bex/value/BexValues.java b/blue-bex-core/src/main/java/blue/bex/value/BexValues.java index 829a088..dc0d9c3 100644 --- a/blue-bex-core/src/main/java/blue/bex/value/BexValues.java +++ b/blue-bex-core/src/main/java/blue/bex/value/BexValues.java @@ -202,10 +202,39 @@ private static ResolvedSnapshot loadReference( ? Collections.singletonList(blueId) : outstanding); } - FrozenNode canonicalReference = FrozenNode.fromNode( - new Node().blueId(blueId)); + Node verifiedFragmentNode = result.value().get().clone(); FrozenNode verifiedDirectFragment = FrozenNode.fromResolvedNode( - result.value().get()); + verifiedFragmentNode); + FrozenNode canonicalReference; + if (blueId.indexOf('#') >= 0) { + /* + * A cyclic member has no independently hashable body. Keep its + * complete-set identity opaque even after semantic materialization. + */ + canonicalReference = FrozenNode.fromNode( + new Node().blueId(blueId)); + } else { + if (verifiedFragmentNode.getBlueId() != null + && !blueId.equals(verifiedFragmentNode.getBlueId())) { + throw new BexInvalidExecutionEvidenceException( + "Verified exact reference fragment changed identity for " + + blueId); + } + verifiedFragmentNode.blueId(null); + try { + canonicalReference = FrozenNode.fromNode( + verifiedFragmentNode); + } catch (RuntimeException invalid) { + throw new BexInvalidExecutionEvidenceException( + "Verified exact reference fragment is not canonical for " + + blueId); + } + if (!blueId.equals(canonicalReference.blueId())) { + throw new BexInvalidExecutionEvidenceException( + "Verified exact reference fragment does not identify " + + blueId); + } + } return new ResolvedSnapshot( canonicalReference, verifiedDirectFragment); } diff --git a/blue-bex-core/src/main/java/blue/bex/value/FrozenNodeBexValue.java b/blue-bex-core/src/main/java/blue/bex/value/FrozenNodeBexValue.java index d1d3b0c..0b4a2bd 100644 --- a/blue-bex-core/src/main/java/blue/bex/value/FrozenNodeBexValue.java +++ b/blue-bex-core/src/main/java/blue/bex/value/FrozenNodeBexValue.java @@ -58,7 +58,10 @@ FrozenNode node() { } FrozenNode canonicalNode() { - return canonicalIdentityNode; + FrozenNode materialized = materializedCanonicalNode; + return materialized != null + ? materialized + : canonicalIdentityNode; } Object rawScalar() { diff --git a/build-logic/src/main/java/blue/bex/buildlogic/LanguageDependencyModeExtension.java b/build-logic/src/main/java/blue/bex/buildlogic/LanguageDependencyModeExtension.java index 2e1696c..43dcd22 100644 --- a/build-logic/src/main/java/blue/bex/buildlogic/LanguageDependencyModeExtension.java +++ b/build-logic/src/main/java/blue/bex/buildlogic/LanguageDependencyModeExtension.java @@ -6,14 +6,17 @@ /** Typed coordinates and dependency mode shared by BEX module builds. */ public abstract class LanguageDependencyModeExtension { public LanguageDependencyModeExtension(ObjectFactory objects) { - getVersion().convention("3.1.0-rc.20"); + getVersion().convention("3.1.0-rc.21"); getCompositePropertyName().convention("blueLanguageCompositePath"); + getRepositoryPropertyName().convention("blueLanguageRepository"); } public abstract Property getVersion(); public abstract Property getCompositePropertyName(); + public abstract Property getRepositoryPropertyName(); + public String coordinate(String artifact) { return "blue.language:" + artifact + ":" + getVersion().get(); } diff --git a/build-logic/src/main/java/blue/bex/buildlogic/LanguageDependencyModePlugin.java b/build-logic/src/main/java/blue/bex/buildlogic/LanguageDependencyModePlugin.java index a0aec80..d9b9681 100644 --- a/build-logic/src/main/java/blue/bex/buildlogic/LanguageDependencyModePlugin.java +++ b/build-logic/src/main/java/blue/bex/buildlogic/LanguageDependencyModePlugin.java @@ -17,6 +17,20 @@ public void apply(Project project) { project.getExtensions().create( EXTENSION, LanguageDependencyModeExtension.class); + String repositoryProperty = + extension.getRepositoryPropertyName().get(); + Object configuredRepository = project.findProperty( + repositoryProperty); + if (configuredRepository != null + && !configuredRepository.toString().trim().isEmpty()) { + String repositoryPath = configuredRepository.toString().trim(); + project.getRepositories().maven(repository -> { + repository.setName("stagedBlueLanguage"); + repository.setUrl(project.uri(repositoryPath)); + repository.content(content -> + content.includeGroup("blue.language")); + }); + } project.getRepositories().mavenCentral(); project.getPluginManager().withPlugin("java", ignored -> @@ -32,8 +46,18 @@ public void apply(Project project) { String composite = (String) project.findProperty(property); boolean local = composite != null && !composite.trim().isEmpty(); + boolean staged = configuredRepository != null + && !configuredRepository.toString() + .trim().isEmpty(); task.getMode().set(local - ? "local-composite" : "standalone-published"); + ? "local-composite" + : staged + ? "staged-repository" + : "standalone-published"); + if (staged) { + task.getStagedRepositoryPath().set( + configuredRepository.toString().trim()); + } task.getModuleName().set(project.getName()); task.getDeclaredLanguageVersion().set( extension.getVersion()); @@ -91,6 +115,10 @@ public void apply(Project project) { task.doLast(ignored -> { String property = extension.getCompositePropertyName().get(); String value = (String) project.findProperty(property); + String repositoryName = + extension.getRepositoryPropertyName().get(); + String repositoryValue = + (String) project.findProperty(repositoryName); if (value != null && !value.trim().isEmpty()) { File checkout = project.file(value.trim()); if (!checkout.isDirectory()) { @@ -98,6 +126,22 @@ public void apply(Project project) { property + " is not a directory: " + checkout); } } + if (value != null && !value.trim().isEmpty() + && repositoryValue != null + && !repositoryValue.trim().isEmpty()) { + throw new GradleException( + "Choose either " + property + " or " + + repositoryName + ", not both"); + } + if (repositoryValue != null + && !repositoryValue.trim().isEmpty()) { + File repository = project.file(repositoryValue.trim()); + if (!repository.isDirectory()) { + throw new GradleException( + repositoryName + " is not a directory: " + + repository); + } + } if (project.getRepositories().stream().anyMatch(repository -> repository.getName().equalsIgnoreCase("MavenLocal"))) { throw new GradleException( diff --git a/build-logic/src/main/java/blue/bex/buildlogic/PublicationConventionsPlugin.java b/build-logic/src/main/java/blue/bex/buildlogic/PublicationConventionsPlugin.java index 2512df3..784e357 100644 --- a/build-logic/src/main/java/blue/bex/buildlogic/PublicationConventionsPlugin.java +++ b/build-logic/src/main/java/blue/bex/buildlogic/PublicationConventionsPlugin.java @@ -59,13 +59,28 @@ public void apply(Project project) { } publishing.getRepositories().maven(repository -> { repository.setName("staging"); - repository.setUrl(project.getRootProject().getLayout() - .getBuildDirectory().dir("staging-deploy")); + Object configured = project.getRootProject() + .findProperty("bexSdkStagingRepository"); + if (configured == null + || configured.toString().trim().isEmpty()) { + repository.setUrl(project.getRootProject() + .getLayout().getBuildDirectory() + .dir("staging-deploy")); + } else { + repository.setUrl(project.uri( + configured.toString().trim())); + } }); }); project.getTasks().withType(AbstractPublishToMaven.class) - .configureEach(task -> task.dependsOn( - project.getRootProject().getTasks() - .named("bexReleaseVerify"))); + .configureEach(task -> { + Object localStage = project.getRootProject() + .findProperty("bexSdkStagingRepository"); + String gate = localStage != null + && !localStage.toString().trim().isEmpty() + ? "bexSdkStageVerify" : "bexReleaseVerify"; + task.dependsOn(project.getRootProject().getTasks() + .named(gate)); + }); } } diff --git a/build-logic/src/main/java/blue/bex/buildlogic/ReleaseEvidencePlugin.java b/build-logic/src/main/java/blue/bex/buildlogic/ReleaseEvidencePlugin.java index 31bc90b..3ae42b1 100644 --- a/build-logic/src/main/java/blue/bex/buildlogic/ReleaseEvidencePlugin.java +++ b/build-logic/src/main/java/blue/bex/buildlogic/ReleaseEvidencePlugin.java @@ -60,21 +60,21 @@ public void apply(Project project) { "reports/bex-release/inputs/" + "published-artifacts"), spec -> spec.include("*.jar"))); - Object differential = project.findProperty( - "bexLocalPublishedDifferential"); - if (differential != null - && !differential.toString().trim().isEmpty()) { - task.getDifferentialReport().fileValue( - project.file(differential.toString())); + Object repeatability = project.findProperty( + "bexPublishedRepeatability"); + if (repeatability != null + && !repeatability.toString().trim().isEmpty()) { + task.getRepeatabilityReport().fileValue( + project.file(repeatability.toString())); } else { File retained = project.getLayout() .getBuildDirectory().file( "reports/bex-release/inputs/" - + "local-published-" - + "differential.json") + + "published-" + + "repeatability.json") .get().getAsFile(); if (retained.isFile()) { - task.getDifferentialReport().fileValue(retained); + task.getRepeatabilityReport().fileValue(retained); } } task.getOutputFile().set( diff --git a/build-logic/src/main/java/blue/bex/buildlogic/RootOrchestrationPlugin.java b/build-logic/src/main/java/blue/bex/buildlogic/RootOrchestrationPlugin.java index e53241f..aa413ea 100644 --- a/build-logic/src/main/java/blue/bex/buildlogic/RootOrchestrationPlugin.java +++ b/build-logic/src/main/java/blue/bex/buildlogic/RootOrchestrationPlugin.java @@ -3,15 +3,12 @@ import blue.bex.buildlogic.tasks.GenerateModernizationReportTask; import blue.bex.buildlogic.tasks.GenerateReleaseReportTask; import blue.bex.buildlogic.tasks.GenerateWorkingReportTask; +import blue.bex.buildlogic.tasks.VerifySdkStageReportTask; import blue.bex.buildlogic.tasks.VerifyPublishedLanguageTask; -import groovy.json.JsonSlurper; import java.io.File; import java.io.IOException; import java.nio.file.Files; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; -import java.util.Map; import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; @@ -38,7 +35,10 @@ public void apply(Project project) { "Runs all normative BEX conformance evidence."); TaskProvider local = lifecycle( project, "bexLocalLanguageVerification", - "Verifies every module against the explicit local Language composite."); + "Optional developer check against a local Language composite."); + TaskProvider publishedDependencies = lifecycle( + project, "bexPublishedDependencyVerification", + "Verifies every module against authenticated published Language."); TaskProvider compatibility = lifecycle( project, "bexCompatibilityCheck", "Verifies API, bytecode, dependency, and semantic compatibility."); @@ -47,14 +47,41 @@ public void apply(Project project) { "Verifies deterministic BEX-owned module and aggregate archives."); TaskProvider working = lifecycle( project, "bexWorkingVerification", - "Runs the mandatory local-composite working gate."); + "Runs the mandatory published-Language working gate."); TaskProvider modern = lifecycle( project, "bexModernizationVerification", "Runs the complete architecture, documentation, property, and " + "serious benchmark gate."); TaskProvider release = lifecycle( project, "bexReleaseVerify", - "Runs the strict published/local public-release gate."); + "Runs the strict published-only public-release gate."); + TaskProvider sdkStage = lifecycle( + project, "bexSdkStageVerify", + "Verifies the isolated SDK candidate without publishing it."); + TaskProvider sdkStageReport = + project.getTasks().register( + "verifyBexSdkStageReport", + VerifySdkStageReportTask.class, + task -> { + task.setGroup("verification"); + task.setDescription( + "Fails on any staged dependency or " + + "conformance blocker."); + task.getConformanceReport().set( + project.getLayout().getProjectDirectory().file( + "blue-bex-conformance/build/reports/" + + "bex-conformance/report.json")); + task.getCandidateBaseline().set( + project.getLayout().getProjectDirectory().file( + "gradle/verification/" + + "sdk-stage-language-baseline.json")); + task.getProjectVersion().set(project.provider( + () -> String.valueOf(project.getVersion()))); + task.getOutputFile().set( + project.getLayout().getBuildDirectory().file( + "reports/bex-sdk-stage/" + + "verification.json")); + }); TaskProvider publishedLanguage = project.getTasks().named( "bexPublishedLanguageVerification", @@ -149,8 +176,6 @@ public void apply(Project project) { }); File latestLanguageBaseline = project.getLayout().getProjectDirectory().file( "gradle/verification/latest-language-baseline.json").getAsFile(); - LanguageBaseline languageBaseline = readLanguageBaseline( - latestLanguageBaseline); TaskProvider baselineReceipt = project.getTasks().register( "writeLatestLanguageBaselineReport", Copy.class, task -> { task.setGroup("verification"); @@ -166,7 +191,7 @@ public void apply(Project project) { task -> { task.setGroup("verification"); task.setDescription( - "Writes the exact local-composite BEX " + "Writes the exact published-Language BEX " + "working-readiness receipt."); task.getTestResultsDirectory().set( project.getLayout().getProjectDirectory().dir( @@ -174,23 +199,6 @@ public void apply(Project project) { + "test-results/test")); task.getBexRepository().set( project.getLayout().getProjectDirectory()); - task.getLanguageRepositoryPath().set( - project.getProviders().gradleProperty( - "blueLanguageCompositePath") - .orElse("")); - task.getExpectedLanguageCommit().set( - languageBaseline.exactHead); - task.getVerifiedImplementationCommit().set( - languageBaseline.verifiedImplementationCommit); - task.getAllowedLanguageDeltaPaths().set( - languageBaseline.documentationOnlyDiffPaths); - task.getLocalCompositeCommand().set( - "./gradlew --no-daemon clean " - + "bexWorkingVerification " - + "-PblueLanguageCompositePath=" - + project.getProviders().gradleProperty( - "blueLanguageCompositePath") - .orElse("").get()); task.getProjectVersion().set(project.provider( () -> String.valueOf(project.getVersion()))); task.getFailOnIncomplete().set(true); @@ -238,21 +246,22 @@ public void apply(Project project) { .fileValue(retained); } } - Object differential = project.findProperty( - "bexLocalPublishedDifferential"); - if (differential != null - && !differential.toString().trim().isEmpty()) { - task.getDifferentialReport().fileValue( - project.file(differential.toString())); + Object repeatability = project.findProperty( + "bexPublishedRepeatability"); + if (repeatability != null + && !repeatability.toString().trim().isEmpty()) { + task.getRepeatabilityReport().fileValue( + project.file(repeatability.toString())); } else { File retained = project.getLayout() .getBuildDirectory().file( "reports/bex-release/inputs/" - + "local-published-" - + "differential.json") + + "published-" + + "repeatability.json") .get().getAsFile(); if (retained.isFile()) { - task.getDifferentialReport().fileValue(retained); + task.getRepeatabilityReport() + .fileValue(retained); } } task.getJsonOutputFile().set( @@ -287,6 +296,25 @@ public void apply(Project project) { + checkout); } })); + publishedDependencies.configure(task -> { + task.dependsOn(Arrays.asList( + core.getTasks().named("verifyLanguageDependencyMode"), + contracts.getTasks().named( + "verifyLanguageDependencyMode"), + suite.getTasks().named("verifyLanguageDependencyMode"), + aggregate.getTasks().named( + "verifyLanguageDependencyMode"), + examples.getTasks().named("verifyLanguageDependencyMode"), + core.getTasks().named("writeLanguageDependencyEvidence"), + contracts.getTasks().named( + "writeLanguageDependencyEvidence"), + suite.getTasks().named("writeLanguageDependencyEvidence"), + aggregate.getTasks().named( + "writeLanguageDependencyEvidence"), + examples.getTasks().named( + "writeLanguageDependencyEvidence"))); + task.doFirst(unused -> requirePublishedOnly(project)); + }); project.getTasks().named("check").configure(task -> task.dependsOn(check)); @@ -325,6 +353,39 @@ public void apply(Project project) { contracts.getTasks().named("verifyReproducibleArchives"), aggregate.getTasks().named("verifyReproducibleArchives"), verifySourceArchive)); + sdkStage.configure(task -> { + task.dependsOn( + compatibility, + reproducibility, + sdkStageReport, + core.getTasks().named("verifyLanguageDependencyMode"), + contracts.getTasks().named("verifyLanguageDependencyMode"), + aggregate.getTasks().named("verifyLanguageDependencyMode")); + task.doFirst(unused -> { + requireLocalStageProperty( + project, "blueLanguageRepository", true); + requireLocalStageProperty( + project, "bexSdkStagingRepository", false); + String selectedVersion = requireLocalStageProperty( + project, "bexLocalStageVersion", false); + if (!selectedVersion.equals( + String.valueOf(project.getVersion()))) { + throw new GradleException( + "bexLocalStageVersion does not match project " + + "version " + project.getVersion()); + } + Object composite = project.findProperty( + "blueLanguageCompositePath"); + if (composite != null + && !composite.toString().trim().isEmpty()) { + throw new GradleException( + "SDK staging forbids included-build Language " + + "substitution"); + } + }); + }); + sdkStageReport.configure(task -> task.dependsOn( + suite.getTasks().named("writeBexConformanceReport"))); modernization.configure(task -> task.dependsOn( working, publishedLanguage, @@ -338,7 +399,7 @@ public void apply(Project project) { sourceArchive)); workingReport.configure(task -> { task.dependsOn( - local, compatibility, reproducibility, + publishedDependencies, compatibility, reproducibility, suite.getTasks().named("jmhSmoke"), project.getTasks().named("verifyBexArchitecture"), core.getTasks().named("assemble"), @@ -362,6 +423,9 @@ public void apply(Project project) { project.getLayout().getProjectDirectory().file( "src/test/resources/hosted-release/" + "required-public-api.txt"), + project.getLayout().getProjectDirectory().file( + "src/test/resources/hosted-release/" + + "published-api-inspection.properties"), project.getLayout().getProjectDirectory().file( "docs/latest-language-api-migration.json"), project.getLayout().getProjectDirectory().file( @@ -400,13 +464,24 @@ public void apply(Project project) { "blue-bex-contracts/src/main/java/**/*.java"))); }); working.configure(task -> task.dependsOn( - local, compatibility, reproducibility, + publishedDependencies, compatibility, reproducibility, workingReport, project.getTasks().named("generateBexSourceFingerprint"))); modern.configure(task -> task.dependsOn(working, modernization)); releaseReport.configure(task -> task.dependsOn( modern, publishedLanguage)); - release.configure(task -> task.dependsOn(releaseReport)); + release.configure(task -> { + task.dependsOn(releaseReport); + task.doFirst(unused -> requirePublishedOnly(project)); + }); + }); + project.getGradle().getTaskGraph().whenReady(graph -> { + if (graph.hasTask(publishedDependencies.get()) + || graph.hasTask(working.get()) + || graph.hasTask(modern.get()) + || graph.hasTask(release.get())) { + requirePublishedOnly(project); + } }); } @@ -435,75 +510,31 @@ private static TaskProvider sourceArchive( }); } - private static LanguageBaseline readLanguageBaseline(File baselineFile) { - final Object parsed; - try { - parsed = new JsonSlurper().parseText( - Files.readString(baselineFile.toPath())); - } catch (IOException | RuntimeException exception) { + private static String requireLocalStageProperty( + Project project, String name, boolean mustBeDirectory) { + Object configured = project.findProperty(name); + if (configured == null || configured.toString().trim().isEmpty()) { throw new GradleException( - "Cannot read latest Language baseline: " + baselineFile, - exception); - } - - Map root = requireObject(parsed, "Language baseline root"); - Map language = requireObject( - root.get("language"), "Language baseline language"); - return new LanguageBaseline( - requireString(language, "exactHead"), - requireString(language, "verifiedImplementationCommit"), - requireStringList(language, "documentationOnlyDiffPaths")); - } - - private static Map requireObject(Object value, String description) { - if (!(value instanceof Map)) { - throw new GradleException(description + " must be a JSON object"); + "bexSdkStageVerify requires -P" + name + "="); } - return (Map) value; - } - - private static String requireString(Map object, String field) { - Object value = object.get(field); - if (!(value instanceof String) - || ((String) value).trim().isEmpty()) { + String selected = configured.toString().trim(); + if (mustBeDirectory && !project.file(selected).isDirectory()) { throw new GradleException( - "Language baseline " + field + " must be a non-empty string"); + name + " is not a directory: " + project.file(selected)); } - return (String) value; + return selected; } - private static List requireStringList( - Map object, String field) { - Object value = object.get(field); - if (!(value instanceof List)) { - throw new GradleException( - "Language baseline " + field + " must be a JSON array"); - } - List result = new ArrayList<>(); - for (Object item : (List) value) { - if (!(item instanceof String) - || ((String) item).trim().isEmpty()) { + private static void requirePublishedOnly(Project project) { + for (String property : new String[] { + "blueLanguageCompositePath", "blueLanguageRepository" + }) { + Object configured = project.findProperty(property); + if (configured != null + && !configured.toString().trim().isEmpty()) { throw new GradleException( - "Language baseline " + field - + " must contain only non-empty strings"); + "Published BEX verification forbids -P" + property); } - result.add((String) item); - } - return result; - } - - private static final class LanguageBaseline { - private final String exactHead; - private final String verifiedImplementationCommit; - private final List documentationOnlyDiffPaths; - - private LanguageBaseline( - String exactHead, - String verifiedImplementationCommit, - List documentationOnlyDiffPaths) { - this.exactHead = exactHead; - this.verifiedImplementationCommit = verifiedImplementationCommit; - this.documentationOnlyDiffPaths = documentationOnlyDiffPaths; } } diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateDependencyEvidenceTask.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateDependencyEvidenceTask.java index c39d662..ce728d4 100644 --- a/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateDependencyEvidenceTask.java +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateDependencyEvidenceTask.java @@ -22,6 +22,7 @@ import org.gradle.api.tasks.Classpath; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.TaskAction; @@ -45,6 +46,10 @@ public abstract class GenerateDependencyEvidenceTask extends DefaultTask { @Input public abstract Property getExactVersionCacheInitiallyAbsent(); + @Input + @Optional + public abstract Property getStagedRepositoryPath(); + @Internal public abstract DirectoryProperty getLanguageCheckout(); @@ -70,6 +75,49 @@ public void generate() { + ",\"bytes\":" + artifact.length() + ",\"sha256\":" + quote(sha256(artifact)) + "}"); } + String mode = getMode().get(); + boolean stagedRepositoryMode = + "staged-repository".equals(mode); + String stagedRepository = getStagedRepositoryPath().isPresent() + ? getStagedRepositoryPath().get().trim() : ""; + List stagedArtifactJson = new ArrayList<>(); + boolean stagedArtifactsMatch = stagedRepositoryMode; + if (stagedRepositoryMode) { + File repository = new File(stagedRepository).getCanonicalFile(); + String version = getDeclaredLanguageVersion().get(); + String[] focusedModules = { + "blue-language-model", + "blue-language-core", + "blue-language-mapping", + "blue-contracts-core", + "blue-language-java" + }; + for (String module : focusedModules) { + File stagedArtifact = new File( + repository, + "blue/language/" + module + "/" + version + "/" + + module + "-" + version + ".jar"); + File resolvedArtifact = artifacts.stream() + .filter(candidate -> candidate.getName().equals( + stagedArtifact.getName())) + .findFirst() + .orElse(null); + boolean matches = stagedArtifact.isFile() + && resolvedArtifact != null + && sha256(stagedArtifact).equals( + sha256(resolvedArtifact)); + stagedArtifactsMatch &= matches; + stagedArtifactJson.add( + " {\"module\":" + quote(module) + + ",\"path\":" + quote(unix(stagedArtifact)) + + ",\"sha256\":" + quote( + stagedArtifact.isFile() + ? sha256(stagedArtifact) : "") + + ",\"matchesResolvedArtifact\":" + + matches + "}"); + } + stagedRepository = repository.getAbsolutePath(); + } String languageHead = ""; String languageStatus = "not-applicable"; if (getLanguageCheckout().isPresent()) { @@ -83,13 +131,14 @@ public void generate() { String bexHead = gitText(bex, "rev-parse", "HEAD").trim(); boolean resolved = !artifactJson.isEmpty() && !getResolvedComponents().get().isEmpty() - && !"dirty".equals(languageStatus); + && !"dirty".equals(languageStatus) + && (!stagedRepositoryMode || stagedArtifactsMatch); String json = "{\n" + " \"schema\": \"blue-bex-dependency-evidence/1.0\",\n" + " \"status\": " + quote(resolved ? "passed" : "failed") + ",\n" + " \"module\": " + quote(getModuleName().get()) + ",\n" - + " \"mode\": " + quote(getMode().get()) + ",\n" + + " \"mode\": " + quote(mode) + ",\n" + " \"declaredLanguageVersion\": " + quote(getDeclaredLanguageVersion().get()) + ",\n" + " \"bexCommit\": " + quote(bexHead) + ",\n" @@ -98,6 +147,14 @@ public void generate() { + quote(languageStatus) + ",\n" + " \"exactVersionCacheInitiallyAbsent\": " + getExactVersionCacheInitiallyAbsent().get() + ",\n" + + " \"stagedRepositoryEvidenceApplicable\": " + + stagedRepositoryMode + ",\n" + + " \"stagedRepositoryPath\": " + + quote(stagedRepository) + ",\n" + + " \"stagedRepositoryArtifactsMatchResolved\": " + + stagedArtifactsMatch + ",\n" + + " \"stagedRepositoryArtifacts\": [\n" + + String.join(",\n", stagedArtifactJson) + "\n ],\n" + " \"resolvedComponents\": " + jsonArray(getResolvedComponents().get()) + ",\n" + " \"artifacts\": [\n" diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateModernizationReportTask.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateModernizationReportTask.java index 9ca7bea..a51cf0a 100644 --- a/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateModernizationReportTask.java +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateModernizationReportTask.java @@ -187,7 +187,8 @@ && sectionPassed(conformance, + "- Public release ready: " + releaseReady + "\n\n" + (releaseReady ? "Published dependency evidence is complete." : "Published release remains fail-closed until matching " - + "Language artifacts and differential evidence exist.") + + "Language artifacts and published repeatability " + + "evidence exist.") + "\n"; File markdownFile = getMarkdownOutputFile().get().getAsFile(); markdownFile.getParentFile().mkdirs(); diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateReleaseReportTask.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateReleaseReportTask.java index d01d04b..51cefca 100644 --- a/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateReleaseReportTask.java +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateReleaseReportTask.java @@ -42,7 +42,7 @@ public abstract class GenerateReleaseReportTask extends DefaultTask { @InputFile @Optional @PathSensitive(PathSensitivity.NONE) - public abstract RegularFileProperty getDifferentialReport(); + public abstract RegularFileProperty getRepeatabilityReport(); @Internal public abstract DirectoryProperty getRepositoryDirectory(); @@ -64,15 +64,15 @@ public void generate() { String published = read( getPublishedLanguageReport().get().getAsFile()); String independent = optionalText(getIndependentCleanBuildReport()); - String differential = optionalText(getDifferentialReport()); + String repeatability = optionalText(getRepeatabilityReport()); Map modernizationEvidence = ReleaseEvidenceJson.parseOrEmpty(modernization); Map publishedEvidence = ReleaseEvidenceJson.parseOrEmpty(published); Map independentEvidence = ReleaseEvidenceJson.parseOrEmpty(independent); - Map differentialEvidence = - ReleaseEvidenceJson.parseOrEmpty(differential); + Map repeatabilityEvidence = + ReleaseEvidenceJson.parseOrEmpty(repeatability); File repository = getRepositoryDirectory().get().getAsFile(); String commit = gitText(repository, "rev-parse", "HEAD").trim(); boolean clean = gitBytes(repository, "status", "--porcelain", "-z") @@ -93,9 +93,9 @@ public void generate() { boolean independentReady = ReleaseEvidenceJson.independentBuildsPassed( independentEvidence, commit); - boolean differentialReady = - ReleaseEvidenceJson.differentialPassed( - differentialEvidence, commit); + boolean repeatabilityReady = + ReleaseEvidenceJson.publishedRepeatabilityPassed( + repeatabilityEvidence, commit); List blockers = new ArrayList<>(); addBlocker(blockers, modernizationReady, @@ -106,8 +106,8 @@ public void generate() { "matching published Language artifacts are not authenticated"); addBlocker(blockers, independentReady, "two isolated clean-build pairs are absent or do not match"); - addBlocker(blockers, differentialReady, - "local/published semantic and exact-gas differential is absent"); + addBlocker(blockers, repeatabilityReady, + "published semantic and exact-gas repeatability is absent"); addBlocker(blockers, clean, "BEX source checkout is dirty"); addBlocker(blockers, exactTag, @@ -115,7 +115,8 @@ public void generate() { boolean releaseReady = blockers.isEmpty(); String json = "{\n" - + " \"schema\": \"blue-bex-strict-release/1.0\",\n" + + " \"schema\": \"blue-bex-strict-release/2.0\",\n" + + " \"dependencyPolicy\": \"published-only\",\n" + " \"bexCommit\": " + quote(commit) + ",\n" + " \"sourceState\": {\"clean\":" + clean + ",\"expectedTag\":" @@ -133,8 +134,8 @@ public void generate() { + " \"independentCleanBuildStatus\": " + quote(independentReady ? "passed" : "not-executed") + ",\n" - + " \"localPublishedDifferentialStatus\": " - + quote(differentialReady ? "passed" : "not-executed") + + " \"publishedRepeatabilityStatus\": " + + quote(repeatabilityReady ? "passed" : "not-executed") + ",\n" + " \"blockers\": " + jsonStrings(blockers) + ",\n" + " \"releaseReady\": " + releaseReady + "\n" @@ -149,8 +150,8 @@ public void generate() { + "- Published Language: " + pass(publishedReady) + "\n" + "- Independent clean builds: " + pass(independentReady) + "\n" - + "- Local/published differential: " - + pass(differentialReady) + "\n" + + "- Published semantic/gas repeatability: " + + pass(repeatabilityReady) + "\n" + "- Clean exact tagged source: " + pass(clean && exactTag) + "\n" + "- `releaseReady`: `" + releaseReady + "`\n\n" diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateWorkingReportTask.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateWorkingReportTask.java index 87f433f..ff7aa5b 100644 --- a/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateWorkingReportTask.java +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/GenerateWorkingReportTask.java @@ -12,9 +12,9 @@ import java.util.Arrays; import java.util.Comparator; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Properties; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -27,7 +27,6 @@ import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputDirectory; @@ -39,7 +38,7 @@ import org.gradle.api.tasks.TaskAction; import org.w3c.dom.Element; -/** Writes the exact local-composite BEX working-readiness receipt. */ +/** Writes the exact published-Language BEX working-readiness receipt. */ public abstract class GenerateWorkingReportTask extends DefaultTask { private static final Pattern FORBIDDEN_IMPORT = Pattern.compile( "(?m)^import\\s+blue\\.language\\.(?:utils\\.|" @@ -69,21 +68,6 @@ public abstract class GenerateWorkingReportTask extends DefaultTask { @Internal public abstract DirectoryProperty getBexRepository(); - @Input - public abstract Property getLanguageRepositoryPath(); - - @Input - public abstract Property getExpectedLanguageCommit(); - - @Input - public abstract Property getVerifiedImplementationCommit(); - - @Input - public abstract ListProperty getAllowedLanguageDeltaPaths(); - - @Input - public abstract Property getLocalCompositeCommand(); - @Input public abstract Property getProjectVersion(); @@ -97,10 +81,7 @@ public abstract class GenerateWorkingReportTask extends DefaultTask { public void generate() { try { File bex = getBexRepository().get().getAsFile(); - File language = new File(getLanguageRepositoryPath().get()) - .getCanonicalFile(); GitState bexState = gitState(bex); - GitState languageState = gitState(language); List evidence = regularFiles(getEvidenceFiles()); String conformance = textFor(evidence, "/bex-conformance/report.json"); @@ -119,6 +100,16 @@ public void generate() { evidence, "/hosted-release/required-public-api.txt"); String jmh = textFor(evidence, "/jmh/smoke-results.json"); String published = textFor(evidence, "published-language.json"); + String publishedInspection = textFor( + evidence, "published-api-inspection.properties"); + Properties inspection = properties(publishedInspection); + String languageCoordinate = inspection.getProperty( + "coordinate", "").trim(); + String languageVersion = coordinateVersion(languageCoordinate); + String languageCommit = inspection.getProperty( + "source.commit", "").trim(); + String languageTag = inspection.getProperty( + "source.tag", "").trim(); TestTotals tests = readTests( getTestResultsDirectory().get().getAsFile()); @@ -147,36 +138,36 @@ && hasInventory(generatedApiClassification, .collect(Collectors.toList()); boolean dependenciesPassed = dependencyFiles.size() == 5; List dependencyJson = new ArrayList<>(); + StringBuilder combinedDependencies = new StringBuilder(); for (File file : dependencyFiles) { String text = read(file); dependencyJson.add(text.trim()); - dependenciesPassed &= text.contains( - "\"mode\": \"local-composite\"") - && text.contains("\"languageCommit\": \"" - + getExpectedLanguageCommit().get() + "\"") + combinedDependencies.append(text).append('\n'); + dependenciesPassed &= text.contains("\"status\": \"passed\"") + && text.contains( + "\"mode\": \"standalone-published\"") + && text.contains("\"declaredLanguageVersion\": \"" + + languageVersion + "\"") && text.contains( - "\"languageCheckoutState\": \"clean\"") + "\"languageCheckoutState\": " + + "\"not-applicable\"") && text.contains("\"bexCommit\": \"" + bexState.head + "\"") + && !text.contains("project :blue-language-") && shaCount(text) > 0; } - - Set actualDelta = new LinkedHashSet<>(gitLines( - language, "diff", "--name-only", - getVerifiedImplementationCommit().get() + "..HEAD")); - Set allowedDelta = new LinkedHashSet<>( - getAllowedLanguageDeltaPaths().get()); - boolean languageCodeEquivalent = languageState.head.equals( - getExpectedLanguageCommit().get()) - && !languageState.dirty - && actualDelta.equals(allowedDelta); + dependenciesPassed &= languageCoordinate.equals( + "blue.language:blue-language-java:" + languageVersion) + && languageCommit.matches("[0-9a-f]{40}") + && languageTag.equals("v" + languageVersion) + && focusedArtifactsAuthenticated( + inspection, combinedDependencies.toString(), + languageVersion); String expectedBexCz = stringAfter( baseline, "\"bex\"", "\"czTomlSha256\""); String baselineBexCommit = stringAfter( baseline, "\"bex\"", "\"migrationBaselineCommit\""); - String expectedLanguageCz = stringAfter( - baseline, "\"language\"", "\"czTomlSha256\""); File bexCzFile = new File(bex, ".cz.toml"); String actualBexCzText = read(bexCzFile); String baselineBexCzText = gitText( @@ -184,7 +175,6 @@ && hasInventory(generatedApiClassification, String actualBexCz = sha256(bexCzFile); String baselineBexCz = sha256( baselineBexCzText.getBytes(StandardCharsets.UTF_8)); - String actualLanguageCz = sha256(new File(language, ".cz.toml")); CommitizenVersionCheck.Result bexVersion = CommitizenVersionCheck.evaluate( actualBexCzText, @@ -192,8 +182,7 @@ && hasInventory(generatedApiClassification, getProjectVersion().get()); boolean baselineBexCzVerified = baselineBexCz.equals(expectedBexCz); boolean versionAutomationValid = baselineBexCzVerified - && bexVersion.passed - && actualLanguageCz.equals(expectedLanguageCz); + && bexVersion.passed; LegacyTotals legacy = legacyTotals(getProductionSources()); int legacyBeforeLines = integerAfter( @@ -232,7 +221,6 @@ && sectionPassed(conformance, String publishedStatus = hasStatus(published, "passed") ? "passed" : "not-executed"; boolean workingReady = !bexState.dirty - && languageCodeEquivalent && versionAutomationValid && dependenciesPassed && tests.executed > 0 @@ -247,17 +235,18 @@ && sectionPassed(conformance, && reproducible; String json = "{\n" - + " \"schema\": \"blue-bex-latest-language-working/1.0\",\n" + + " \"schema\": \"blue-bex-latest-language-working/2.0\",\n" + " \"bex\": {\"commit\":" + quote(bexState.head) + ",\"dirty\":" + bexState.dirty + ",\"statusSha256\":" + quote(bexState.statusSha256) + "},\n" - + " \"language\": {\"exactCommit\":" - + quote(languageState.head) + ",\"dirty\":" - + languageState.dirty + ",\"verifiedImplementationCommit\":" - + quote(getVerifiedImplementationCommit().get()) - + ",\"codeEquivalent\":" + languageCodeEquivalent - + ",\"deltaPaths\":" + jsonStrings(actualDelta) + "},\n" + + " \"language\": {\"selection\":" + + quote("published-maven-central") + + ",\"coordinate\":" + quote(languageCoordinate) + + ",\"sourceCommit\":" + quote(languageCommit) + + ",\"sourceTag\":" + quote(languageTag) + + ",\"focusedArtifactsAuthenticated\":" + + dependenciesPassed + "},\n" + " \"languageModuleBaseline\": " + jsonOrEmpty(baseline) + ",\n" + " \"versionAutomation\": {\"status\":" @@ -275,10 +264,7 @@ && sectionPassed(conformance, + bexVersion.matchesProjectVersion + ",\"bexNonVersionConfigMatchesBaseline\":" + bexVersion.nonVersionConfigMatchesBaseline - + ",\"languageCzTomlSha256\":" - + quote(actualLanguageCz) - + ",\"languageMatchesBaseline\":" - + actualLanguageCz.equals(expectedLanguageCz) + "},\n" + + "},\n" + " \"dependencyEvidence\": " + jsonObjects(dependencyJson) + ",\n" + " \"legacyImports\": {\"before\":{\"lines\":" @@ -316,9 +302,8 @@ && sectionPassed(conformance, + " \"reproducibility\": {\"status\":" + quote(reproducible ? "passed" : "failed") + ",\"replicas\":" + fileEvidenceJson(replicas) + "},\n" - + " \"localComposite\": {\"command\":" - + quote(getLocalCompositeCommand().get()) - + ",\"outcome\":" + + " \"publishedDependency\": {\"coordinate\":" + + quote(languageCoordinate) + ",\"outcome\":" + quote(workingReady ? "passed" : "failed") + "},\n" + " \"workingReady\": " + workingReady + ",\n" + " \"publishedModeStatus\": " @@ -344,6 +329,54 @@ private static List regularFiles(ConfigurableFileCollection files) { .collect(Collectors.toList()); } + private static Properties properties(String text) throws IOException { + Properties result = new Properties(); + try (java.io.StringReader reader = new java.io.StringReader(text)) { + result.load(reader); + } + return result; + } + + private static String coordinateVersion(String coordinate) { + int separator = coordinate.lastIndexOf(':'); + return separator >= 0 && separator + 1 < coordinate.length() + ? coordinate.substring(separator + 1) : ""; + } + + private static boolean focusedArtifactsAuthenticated( + Properties inspection, String dependencyEvidence, String version) { + List modules = propertyList( + inspection, "release.resolvedRuntimeArtifacts"); + if (modules.isEmpty()) { + return false; + } + for (String module : modules) { + String hash = inspection.getProperty( + "artifact." + module + ".sha256", "").trim(); + if (!hash.matches("[0-9a-f]{64}") + || !dependencyEvidence.contains( + module + "-" + version + ".jar") + || !dependencyEvidence.contains(hash)) { + return false; + } + } + return true; + } + + private static List propertyList( + Properties properties, String key) { + String value = properties.getProperty(key, "").trim(); + if (value.isEmpty()) { + return java.util.Collections.emptyList(); + } + List result = Arrays.stream(value.split(",", -1)) + .map(String::trim) + .collect(Collectors.toList()); + return result.stream().anyMatch(String::isEmpty) + || result.stream().distinct().count() != result.size() + ? java.util.Collections.emptyList() : result; + } + private static List describe(ConfigurableFileCollection files) throws Exception { List result = new ArrayList<>(); diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/ReleaseEvidenceJson.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/ReleaseEvidenceJson.java index 9e032e7..599a11c 100644 --- a/build-logic/src/main/java/blue/bex/buildlogic/tasks/ReleaseEvidenceJson.java +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/ReleaseEvidenceJson.java @@ -60,13 +60,15 @@ static boolean conformanceReleasePassed( static boolean publishedLanguagePassed(Map evidence) { if (!hasString(evidence, "schema", - "blue-bex-published-language/2.0") + "blue-bex-published-language/3.0") || !hasString(evidence, "status", "passed") || !hasBoolean(evidence, "configuredAssertionsMatch", true) || !hasBoolean(evidence, "apiInspectionPassed", true) + || !hasBoolean(evidence, + "focusedArtifactHashesPassed", true) || !hasString(evidence, - "differentialStatus", "passed") + "repeatabilityStatus", "passed") || !matches(evidence, "artifactSha256", SHA_256) || !matches(evidence, "sourceCommit", COMMIT) || !coordinate(stringOrEmpty(evidence, "coordinate"))) { @@ -100,22 +102,22 @@ static boolean publishedLanguagePassed(Map evidence) { } } - static boolean differentialPassed( + static boolean publishedRepeatabilityPassed( Map evidence, String expectedCommit) { if (!hasString(evidence, "schema", - "blue-bex-local-published-differential/1.0") + "blue-bex-published-conformance-repeatability/1.0") || !hasString(evidence, "status", "passed") || !hasString(evidence, - "localMode", "local-composite") - || !hasString(evidence, - "publishedMode", "standalone-published") + "dependencyPolicy", "published-only") + || !hasInteger(evidence, "runCount", 4) || !hasBoolean(evidence, "sourceBound", true) - || !hasBoolean(evidence, "dependenciesDistinct", true) + || !hasBoolean(evidence, + "dependencyIdentityRepeated", true) || !hasString(evidence, - "semanticAndGasParity", "passed") + "semanticAndGasRepeatability", "passed") || !hasString(evidence, - "exactGasTraceParity", "passed")) { + "exactGasTraceRepeatability", "passed")) { return false; } String commit = stringOrEmpty(evidence, "bexCommit"); @@ -125,10 +127,13 @@ static boolean differentialPassed( return false; } try { - return digestPairMatches(StrictJson.object( - evidence, "semanticEvidenceSha256")) - && digestPairMatches(StrictJson.object( - evidence, "gasEvidenceSha256")); + List modes = StrictJson.array(evidence, "modes"); + return modes.size() == 4 + && modes.stream().allMatch( + mode -> "standalone-published".equals(mode)) + && repeatedDigest(evidence, "semanticEvidenceSha256") + && repeatedDigest(evidence, "gasEvidenceSha256") + && repeatedDigest(evidence, "dependencyArtifactSha256"); } catch (IllegalArgumentException invalid) { return false; } @@ -138,8 +143,10 @@ static boolean independentBuildsPassed( Map evidence, String expectedCommit) { if (!hasString(evidence, "schema", - "blue-bex-independent-clean-builds/2.1") + "blue-bex-independent-clean-builds/3.0") || !hasString(evidence, "status", "passed") + || !hasString(evidence, + "dependencyPolicy", "published-only") || !hasBoolean(evidence, "distinctCheckoutRoots", true) || !hasBoolean(evidence, @@ -173,7 +180,8 @@ static boolean independentBuildsPassed( gradleHomes, manifests) && buildPairPassed( - StrictJson.object(evidence, "localComposite"), + StrictJson.object( + evidence, "standalonePublishedReplica"), commit, checkoutRoots, gitDirectories, @@ -197,6 +205,7 @@ private static boolean buildPairPassed( Set gradleHomes, Set manifests) throws Exception { if (!hasString(pair, "status", "passed") + || !hasString(pair, "mode", "standalone-published") || !hasBoolean(pair, "exactManifestBytesMatch", true) || !hasBoolean(pair, "exactArtifactBytesMatch", true) || !hasBoolean(pair, "artifactPathSetMatch", true) @@ -344,10 +353,16 @@ private static LiveBuild liveBuild( manifestBytes); } - private static boolean digestPairMatches(Map pair) { - String local = stringOrEmpty(pair, "local"); - String published = stringOrEmpty(pair, "published"); - return local.matches(SHA_256) && local.equals(published); + private static boolean repeatedDigest( + Map evidence, String field) { + List values = StrictJson.array(evidence, field); + if (values.size() != 4) { + return false; + } + String expected = values.get(0) instanceof String + ? (String) values.get(0) : ""; + return expected.matches(SHA_256) + && values.stream().allMatch(expected::equals); } private static boolean safeArtifactPath(String path) { diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyPublishedLanguageTask.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyPublishedLanguageTask.java index a899481..d3bd9a3 100644 --- a/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyPublishedLanguageTask.java +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifyPublishedLanguageTask.java @@ -11,9 +11,11 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.Enumeration; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.stream.Collectors; @@ -36,8 +38,8 @@ * *

Caller-supplied coordinates and digests are assertions, not evidence. * This task only passes when they agree with the source-controlled inspection, - * the resolved artifact bytes, and a same-source local/published differential - * report. Missing publication inputs remain explicitly {@code not-executed}. + * the resolved artifact bytes, and same-source published-mode repeatability + * evidence. Missing publication inputs remain explicitly {@code not-executed}. */ public abstract class VerifyPublishedLanguageTask extends DefaultTask { private static final String COMPATIBLE_STATUS = @@ -66,7 +68,7 @@ public abstract class VerifyPublishedLanguageTask extends DefaultTask { @InputFile @Optional @PathSensitive(PathSensitivity.NONE) - public abstract RegularFileProperty getDifferentialReport(); + public abstract RegularFileProperty getRepeatabilityReport(); @OutputFile public abstract RegularFileProperty getOutputFile(); @@ -112,7 +114,8 @@ public void verify() { "[^:]+:[^:]+:[^:]+") && reviewedSha.matches("[0-9a-f]{64}") && sourceCommit.matches("[0-9a-f]{40}") - && sourceTag.matches("v?[A-Za-z0-9][A-Za-z0-9._-]*"); + && sourceTag.equals("v" + coordinateVersion( + reviewedCoordinate)); if (!reviewedIdentityComplete) { reasons.add("reviewed coordinate, artifact hash, or source identity " + "is incomplete"); @@ -136,37 +139,41 @@ public void verify() { boolean reviewedApiClaimsPass = reviewedApiClaimsPass( inspection, artifacts, reasons); - String differential = getDifferentialReport().isPresent() - && getDifferentialReport().get().getAsFile().isFile() - ? read(getDifferentialReport().get().getAsFile()) : ""; - Map differentialEvidence = - ReleaseEvidenceJson.parseOrEmpty(differential); - boolean differentialPassed = - ReleaseEvidenceJson.differentialPassed( - differentialEvidence, null); - if (!differential.isEmpty() && !differentialPassed) { - reasons.add("local/published semantic and exact-gas differential " - + "did not pass"); + boolean focusedArtifactHashesPass = focusedArtifactHashesPass( + inspection, artifactEvidence, reasons); + + String repeatability = getRepeatabilityReport().isPresent() + && getRepeatabilityReport().get().getAsFile().isFile() + ? read(getRepeatabilityReport().get().getAsFile()) : ""; + Map repeatabilityEvidence = + ReleaseEvidenceJson.parseOrEmpty(repeatability); + boolean repeatabilityPassed = + ReleaseEvidenceJson.publishedRepeatabilityPassed( + repeatabilityEvidence, null); + if (!repeatability.isEmpty() && !repeatabilityPassed) { + reasons.add("published-mode semantic and exact-gas " + + "repeatability did not pass"); } boolean inputsPresent = configured && !artifacts.isEmpty() - && !differential.isEmpty(); + && !repeatability.isEmpty(); boolean passed = reviewedCompatible && reviewedIdentityComplete && assertionsMatch && artifactHashMatches - && reviewedApiClaimsPass && differentialPassed; + && reviewedApiClaimsPass && focusedArtifactHashesPass + && repeatabilityPassed; String status; if (!reviewedCompatible) { status = "incompatible"; } else if (!inputsPresent) { status = "not-executed"; - reasons.add("resolved artifacts and same-run differential evidence " - + "are required"); + reasons.add("resolved artifacts and same-run published-mode " + + "repeatability evidence are required"); } else { status = passed ? "passed" : "failed"; } String json = "{\n" - + " \"schema\": \"blue-bex-published-language/2.0\",\n" + + " \"schema\": \"blue-bex-published-language/3.0\",\n" + " \"status\": " + quote(status) + ",\n" + " \"coordinate\": " + quote(reviewedCoordinate) + ",\n" + " \"artifactSha256\": " + quote(reviewedSha) + ",\n" @@ -182,8 +189,10 @@ && getDifferentialReport().get().getAsFile().isFile() + artifactsJson(artifactEvidence) + ",\n" + " \"apiInspectionPassed\": " + reviewedApiClaimsPass + ",\n" - + " \"differentialStatus\": " - + quote(differentialPassed ? "passed" : "not-executed") + + " \"focusedArtifactHashesPassed\": " + + focusedArtifactHashesPass + ",\n" + + " \"repeatabilityStatus\": " + + quote(repeatabilityPassed ? "passed" : "not-executed") + ",\n" + " \"blockers\": " + jsonStrings(reasons) + "\n" + "}\n"; @@ -231,6 +240,72 @@ private static boolean reviewedApiClaimsPass(Properties inspection, return passed; } + private static boolean focusedArtifactHashesPass( + Properties inspection, + List artifacts, + List reasons) { + String coordinate = property(inspection, "coordinate"); + String version = coordinateVersion(coordinate); + List modules = propertyList( + inspection, "release.requiredArtifacts"); + String[] coordinateParts = coordinate.split(":", -1); + String coordinateArtifact = coordinateParts.length == 3 + ? coordinateParts[1] : ""; + boolean passed = true; + Set expectedNames = new HashSet<>(); + if (modules.isEmpty() || !modules.contains(coordinateArtifact)) { + passed = false; + reasons.add("reviewed required Language artifact list is invalid"); + } + for (String module : modules) { + String expectedName = module + "-" + version + ".jar"; + expectedNames.add(expectedName); + String expected = property( + inspection, "artifact." + module + ".sha256"); + boolean validExpected = expected.matches("[0-9a-f]{64}"); + long matching = artifacts.stream() + .filter(item -> item.file.getName().equals(expectedName) + && expected.equals(item.sha256)) + .count(); + boolean resolved = validExpected && matching == 1; + if (!resolved) { + passed = false; + reasons.add("reviewed hash did not authenticate resolved " + + module + " artifact"); + } + } + Set actualNames = artifacts.stream() + .map(item -> item.file.getName()) + .collect(Collectors.toSet()); + if (artifacts.size() != expectedNames.size() + || !actualNames.equals(expectedNames)) { + passed = false; + reasons.add("resolved Language artifact set differs from the " + + "required set reviewed for publication"); + } + return passed; + } + + private static List propertyList( + Properties properties, String key) { + String value = property(properties, key); + if (value.isEmpty()) { + return java.util.Collections.emptyList(); + } + List result = java.util.Arrays.stream(value.split(",", -1)) + .map(String::trim) + .collect(Collectors.toList()); + return result.stream().anyMatch(String::isEmpty) + || result.stream().distinct().count() != result.size() + ? java.util.Collections.emptyList() : result; + } + + private static String coordinateVersion(String coordinate) { + int separator = coordinate.lastIndexOf(':'); + return separator >= 0 && separator + 1 < coordinate.length() + ? coordinate.substring(separator + 1) : ""; + } + private static boolean containsJarEntry(List files, String name) throws IOException { for (File file : files) { diff --git a/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifySdkStageReportTask.java b/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifySdkStageReportTask.java new file mode 100644 index 0000000..6e7c79d --- /dev/null +++ b/build-logic/src/main/java/blue/bex/buildlogic/tasks/VerifySdkStageReportTask.java @@ -0,0 +1,187 @@ +package blue.bex.buildlogic.tasks; + +import groovy.json.JsonOutput; +import groovy.json.JsonSlurper; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.TaskAction; + +/** Fails closed unless the isolated SDK-stage conformance evidence is exact. */ +public abstract class VerifySdkStageReportTask extends DefaultTask { + @InputFile + @PathSensitive(PathSensitivity.NONE) + public abstract RegularFileProperty getConformanceReport(); + + @InputFile + @PathSensitive(PathSensitivity.RELATIVE) + public abstract RegularFileProperty getCandidateBaseline(); + + @Input + public abstract Property getProjectVersion(); + + @OutputFile + public abstract RegularFileProperty getOutputFile(); + + @TaskAction + public void verify() { + File reportFile = getConformanceReport().get().getAsFile(); + File baselineFile = getCandidateBaseline().get().getAsFile(); + Map report = object(new JsonSlurper().parse(reportFile)); + Map baseline = object(new JsonSlurper().parse(baselineFile)); + Map language = object(baseline.get("language")); + Map bex = object(baseline.get("bex")); + Map dependency = object(report.get("dependency")); + Map resolution = object(dependency.get("resolution")); + Map provenance = object(resolution.get("provenance")); + Map versionAutomation = object(report.get("versionAutomation")); + Map languageIdentity = object( + report.get("languageReleaseIdentity")); + Map publishedInspection = object( + report.get("publishedHostApiInspection")); + + String languageVersion = string(language.get("candidateVersion")); + String languageCoordinate = + "blue.language:blue-language-java:" + languageVersion; + String candidateVersion = string(bex.get("candidateVersion")); + String projectVersion = getProjectVersion().get(); + List blockers = new ArrayList<>(); + require(blockers, + "blue-bex-sdk-stage-baseline/1.0".equals( + baseline.get("schema")), + "unknown-candidate-baseline-schema"); + require(blockers, + "candidate-source-lock".equals(baseline.get("status")), + "candidate-baseline-not-source-locked"); + require(blockers, + string(language.get("sourceCommit")) + .matches("[0-9a-f]{40}"), + "language-source-commit-not-exact"); + require(blockers, + candidateVersion.equals(projectVersion), + "bex-candidate-version-mismatch"); + require(blockers, + "blue-bex-hosted-release-report/2.0".equals( + report.get("schema")), + "unknown-conformance-report-schema"); + require(blockers, + projectVersion.equals(report.get("projectVersion")), + "conformance-project-version-mismatch"); + require(blockers, + "staged-repository".equals(dependency.get("mode")), + "dependency-mode-is-not-staged-repository"); + require(blockers, + languageCoordinate.equals( + dependency.get("declaredCoordinate")), + "language-candidate-coordinate-mismatch"); + require(blockers, + "passed".equals(resolution.get("status")), + "staged-language-resolution-not-passing"); + require(blockers, + "isolated-staged-repository".equals( + provenance.get("kind")), + "staged-language-provenance-kind-mismatch"); + require(blockers, + "explicit-staged-repository-before-maven-central".equals( + provenance.get("repositoryPolicy")), + "staged-language-repository-policy-mismatch"); + require(blockers, + Boolean.TRUE.equals(provenance.get( + "stagedRepositoryArtifactsMatchResolved")), + "staged-language-artifact-hash-mismatch"); + require(blockers, + Boolean.TRUE.equals( + versionAutomation.get("matchesProjectVersion")) + && "explicit-staged-candidate".equals( + versionAutomation.get("selectionKind")), + "explicit-bex-candidate-version-not-accepted"); + require(blockers, + string(bex.get("historicalReleaseVersion")).equals( + versionAutomation.get("historicalConfiguredVersion")), + "historical-bex-release-version-mismatch"); + require(blockers, + ("blue.language:blue-language-java:" + + language.get("historicalPublishedVersion")) + .equals(publishedInspection.get("coordinate")), + "historical-published-language-evidence-mismatch"); + require(blockers, + Boolean.TRUE.equals( + languageIdentity.get("exactSelectedArtifactProven")) + && "staged-repository-candidate".equals( + languageIdentity.get("selectionKind")), + "exact-staged-language-artifact-not-proven"); + Object currentFailures = report.get("currentModeFailures"); + require(blockers, + currentFailures instanceof List + && ((List) currentFailures).isEmpty(), + "staged-conformance-blockers-present"); + + Map receipt = new LinkedHashMap<>(); + receipt.put("schema", "blue-bex-sdk-stage-verification/1.0"); + receipt.put("status", blockers.isEmpty() ? "passed" : "failed"); + receipt.put("blockers", blockers); + receipt.put("bexCandidateVersion", projectVersion); + receipt.put("historicalBexReleaseVersion", + bex.get("historicalReleaseVersion")); + receipt.put("languageCandidateVersion", languageVersion); + receipt.put("languageSourceCommit", language.get("sourceCommit")); + receipt.put("languageCoordinate", languageCoordinate); + receipt.put("historicalPublishedLanguageVersion", + language.get("historicalPublishedVersion")); + receipt.put("stagedRepository", provenance.get("recordedRepository")); + receipt.put("resolvedLanguageArtifacts", + resolution.get("artifacts") instanceof List + ? resolution.get("artifacts") + : Collections.emptyList()); + receipt.put("conformanceReport", reportFile.getAbsolutePath()); + + try { + File output = getOutputFile().get().getAsFile(); + Files.createDirectories(output.toPath().getParent()); + Files.write( + output.toPath(), + (JsonOutput.prettyPrint(JsonOutput.toJson(receipt)) + "\n") + .getBytes(StandardCharsets.UTF_8)); + } catch (Exception exception) { + throw new GradleException( + "Cannot write BEX SDK-stage verification receipt", + exception); + } + if (!blockers.isEmpty()) { + throw new GradleException( + "BEX SDK staging is blocked: " + + String.join(", ", blockers)); + } + } + + private static Map object(Object value) { + return value instanceof Map + ? (Map) value + : Collections.emptyMap(); + } + + private static String string(Object value) { + return value instanceof String ? (String) value : ""; + } + + private static void require( + List blockers, boolean condition, String blocker) { + if (!condition) { + blockers.add(blocker); + } + } +} diff --git a/build-logic/src/test/java/blue/bex/buildlogic/tasks/VerifySdkStageReportTaskTest.java b/build-logic/src/test/java/blue/bex/buildlogic/tasks/VerifySdkStageReportTaskTest.java new file mode 100644 index 0000000..ca8b016 --- /dev/null +++ b/build-logic/src/test/java/blue/bex/buildlogic/tasks/VerifySdkStageReportTaskTest.java @@ -0,0 +1,111 @@ +package blue.bex.buildlogic.tasks; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.gradle.api.GradleException; +import org.gradle.api.Project; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +final class VerifySdkStageReportTaskTest { + @TempDir + Path temporaryDirectory; + + @Test + void passesOnlyExactBlockerFreeStagedEvidence() throws Exception { + VerifySdkStageReportTask task = task(); + task.verify(); + + String receipt = Files.readString( + task.getOutputFile().get().getAsFile().toPath()); + assertTrue(receipt.contains("\"status\": \"passed\"")); + assertTrue(receipt.contains( + "e0dfc897ea7d158895325fae2bf84e103b8c1989")); + } + + @Test + void writesFailureReceiptBeforeRejectingConformanceBlockers() + throws Exception { + VerifySdkStageReportTask task = task(); + Path report = task.getConformanceReport().get().getAsFile().toPath(); + Files.writeString( + report, + Files.readString(report).replace( + "\"currentModeFailures\": []", + "\"currentModeFailures\": [\"failed-test\"]")); + + assertThrows(GradleException.class, task::verify); + assertTrue(Files.readString( + task.getOutputFile().get().getAsFile().toPath()) + .contains("\"status\": \"failed\"")); + } + + private VerifySdkStageReportTask task() throws Exception { + Project project = ProjectBuilder.builder() + .withProjectDir(temporaryDirectory.toFile()) + .build(); + VerifySdkStageReportTask task = project.getTasks().create( + "verifySdkStage", + VerifySdkStageReportTask.class); + Path baseline = temporaryDirectory.resolve("baseline.json"); + Files.writeString(baseline, """ + { + "schema": "blue-bex-sdk-stage-baseline/1.0", + "status": "candidate-source-lock", + "language": { + "candidateVersion": "3.1.0-rc.21", + "sourceCommit": "e0dfc897ea7d158895325fae2bf84e103b8c1989", + "historicalPublishedVersion": "3.1.0-rc.20" + }, + "bex": { + "candidateVersion": "1.1.0-rc.4", + "historicalReleaseVersion": "1.1.0-rc.3" + } + } + """); + Path report = temporaryDirectory.resolve("report.json"); + Files.writeString(report, """ + { + "schema": "blue-bex-hosted-release-report/2.0", + "projectVersion": "1.1.0-rc.4", + "currentModeFailures": [], + "dependency": { + "mode": "staged-repository", + "declaredCoordinate": "blue.language:blue-language-java:3.1.0-rc.21", + "resolution": { + "status": "passed", + "artifacts": [], + "provenance": { + "kind": "isolated-staged-repository", + "repositoryPolicy": "explicit-staged-repository-before-maven-central", + "stagedRepositoryArtifactsMatchResolved": true, + "recordedRepository": "/stage" + } + } + }, + "versionAutomation": { + "matchesProjectVersion": true, + "selectionKind": "explicit-staged-candidate", + "historicalConfiguredVersion": "1.1.0-rc.3" + }, + "languageReleaseIdentity": { + "exactSelectedArtifactProven": true, + "selectionKind": "staged-repository-candidate" + }, + "publishedHostApiInspection": { + "coordinate": "blue.language:blue-language-java:3.1.0-rc.20" + } + } + """); + task.getCandidateBaseline().fileValue(baseline.toFile()); + task.getConformanceReport().fileValue(report.toFile()); + task.getProjectVersion().set("1.1.0-rc.4"); + task.getOutputFile().fileValue( + temporaryDirectory.resolve("receipt.json").toFile()); + return task; + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 8893929..599cd50 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,9 @@ plugins { } group = "blue.bex" -version = configuredVersion() +version = configuredVersion( + providers.gradleProperty("bexLocalStageVersion").orNull +) allprojects { group = rootProject.group @@ -51,7 +53,15 @@ if (System.getenv("CI") != null) { } } -fun configuredVersion(): String { +fun configuredVersion(localStageVersion: String?): String { + if (!localStageVersion.isNullOrBlank()) { + val selected = localStageVersion.trim() + require(Regex("""\d+\.\d+\.\d+(?:-rc\.\d+)?""") + .matches(selected)) { + "bexLocalStageVersion must be a release or RC version" + } + return selected + } val configured = Regex("""version\s*=\s*"([^"]+)"""") .find(file(".cz.toml").readText()) ?.groupValues diff --git a/docs/BEX_CONFORMANCE.md b/docs/BEX_CONFORMANCE.md index b89d4ff..966a06c 100644 --- a/docs/BEX_CONFORMANCE.md +++ b/docs/BEX_CONFORMANCE.md @@ -29,11 +29,10 @@ never converted into an invented execution count. ## Run locally -Use the explicit verified Language composite: +Use the exact reviewed Language release from Maven Central: ```bash -./gradlew --no-daemon clean bexWorkingVerification \ - -PblueLanguageCompositePath=/absolute/path/to/blue-language-java +./gradlew --no-daemon clean bexWorkingVerification ``` The working gate creates: @@ -56,8 +55,7 @@ module and source artifacts; and byte-identical BEX-owned archive replicas. Run the longer modernization gate separately: ```bash -./gradlew --no-daemon bexModernizationVerification \ - -PblueLanguageCompositePath=/absolute/path/to/blue-language-java +./gradlew --no-daemon bexModernizationVerification ``` It adds architecture/source metrics, concurrency and property tests, fourteen @@ -71,6 +69,17 @@ blue-bex-conformance/build/reports/jmh/results.json blue-bex-conformance/build/reports/jmh/environment.json ``` +An explicit Language composite remains available only for developer integration +with an unpublished Language checkout: + +```bash +./gradlew --no-daemon bexCheck \ + -PblueLanguageCompositePath=/absolute/path/to/blue-language-java +``` + +That optional developer mode is not release evidence and is never used by the +final-publication gate. + ## Reproducibility claims `verifyReproducibleArchives` and @@ -79,18 +88,19 @@ module/source/Javadoc/source-release archives from the same compiled inputs. That is the BEX-owned working claim; it is not mislabeled as two clean builds. Public release additionally uses four isolated BEX checkouts and four isolated -Gradle homes: two standalone-published builds and two local-composite builds. -`.github/scripts/run-final-publication-gates.sh` records exact artifact -manifests for each pair and compares local versus published conformance fields -for semantic and exact-gas equality. +Gradle homes, all in `standalone-published` mode. The final-publication script +records exact artifact manifests and conformance receipts for every build, +requires byte-identical BEX artifacts, and compares the published semantic, +fixture, operator, and exact-gas evidence for repeatability. ## Fail-closed publication `bexPublishedLanguageVerification` authenticates resolved artifact bytes against `published-api-inspection.properties`, requires every reviewed API -claim and a same-run local/published differential, and cannot pass from a CLI -coordinate/hash alone. `bexReleaseVerify` then requires modernization, both -independent clean-build pairs, clean exact-tagged BEX source, and writes: +claim and same-run published repeatability evidence, and cannot pass from a CLI +coordinate/hash alone. `bexReleaseVerify` then requires modernization, all four +independent standalone-published builds, clean exact-tagged BEX source, and +writes: ```text build/reports/bex-release/final.json @@ -98,5 +108,4 @@ build/reports/bex-release/final.md ``` Unavailable or incompatible published Language modules remain visibly -`not-executed` or `incompatible`; they never make local-composite evidence red -and never become a public-release pass. +`not-executed` or `incompatible` and never become a public-release pass. diff --git a/docs/LATEST_LANGUAGE_API_MIGRATION.md b/docs/LATEST_LANGUAGE_API_MIGRATION.md index d1765e3..b322318 100644 --- a/docs/LATEST_LANGUAGE_API_MIGRATION.md +++ b/docs/LATEST_LANGUAGE_API_MIGRATION.md @@ -12,12 +12,12 @@ It is the human-readable companion to | BEX baseline | `395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8` | | Working compatibility checkpoint | `169e589` | | BEX migration | Modernization delta rooted at the working checkpoint; the commit containing this ledger is the final target revision | -| Language target | `505a654699b86b42bf0e282ddf94560a91529bcf` (`v3.1.0-rc.20`) | -| Language verified implementation | `505a654699b86b42bf0e282ddf94560a91529bcf` | +| Language target | `5c4e5c88fa75d6cbc52b2e8772f14f2ac5246f52` (`v3.1.0-rc.21`) | +| Language verified implementation | `5c4e5c88fa75d6cbc52b2e8772f14f2ac5246f52` | | Language target delta | None; the target and verified implementation commits are identical | | Previous API manifest SHA-256 | `830caa187023079ba53fa76d2932e6e12cb8c93be3f90ac887ad374d6642b315` | | Working-checkpoint API manifest SHA-256 | `43aea6ae9de6f39729f93c146ff5453887da9a2303f6f4f33573ba47f37d5be0` | -| Final modular API manifest SHA-256 | `5acb4712e3e03c5ba9a58d87b4a40bed4e1dcca76ed58ef355a77f0efc9d3c92` | +| Final modular API manifest SHA-256 | `a330de486f74df49ddf45b3bf896753f6a326e08b7e6f84a8c9aef3bc46cbd43` | The migration target cannot truthfully name its eventual BEX commit while that commit is being assembled. The Git commit containing this ledger is the target @@ -26,7 +26,7 @@ entry was audited. ## Exact descriptor changes -The subsequent modular modernization contains 254 removed and 484 added exact +The subsequent modular modernization contains 254 removed and 492 added exact owner-qualified descriptors relative to commit `169e589`. Both compared manifests, both classifications, and the complete sorted addition/removal sets are source-controlled under `gradle/verification/api/`. The `binaryApiCheck` task @@ -54,6 +54,9 @@ presented as the exhaustive machine delta. | Added | internal implementation | `blue.bex.type.BexBlueTypeMatcher::(blue.language.runtime.BlueLanguage)` | Supported modular matcher/runtime boundary. | Additive alone; migration target for the removed constructor. | | Removed | host SPI | `method public static referenceBacked(blue.bex.value.BexValue,blue.language.Blue):blue.bex.value.BexValue` | Replaced by the overload using the modular graph-capable runtime. | Binary and source breaking for direct host callers. | | Added | host SPI | `method public static referenceBacked(blue.bex.value.BexValue,blue.language.runtime.BlueLanguage):blue.bex.value.BexValue` | Verified, demand-driven reference materialization through `BlueLanguage`. | Additive alone; migration target for host integrations. | +| Added | host SPI | `class public abstract interface blue.bex.output.BexExactValueCapability` | Carries an already verified exact Blue value across BEX output admission. | Compatible additive host capability. | +| Added | host SPI | `class public final blue.bex.contracts.ProcessorExactBlueValueCapability implements blue.bex.output.BexExactValueCapability` | Adapts the Contracts `ExactBlueValue` capability without reserialization. | Compatible additive Contracts adapter. | +| Added | host SPI | `method public carryExactIdentity(java.lang.String,blue.language.snapshot.FrozenNode):blue.bex.output.BexEstablishedIdentity` on `BexSemanticIdentityBoundary` | Preserves an established ordinary BlueId and frozen value through semantic output admission. | Compatible default-method addition. | The compiler-package acyclicity pass contributes these reviewed descriptors: @@ -128,14 +131,14 @@ isolated because diagnostics cannot alter compile/cache/execution/gas success. ## Public API classification and deterministic inventory [`public-api-classification.json`](public-api-classification.json) classifies -all 101 public production types as stable API, host SPI, intrinsic SPI, internal -implementation, or conformance-only. The exact 1,028 class/member descriptors are +all 103 public production types as stable API, host SPI, intrinsic SPI, internal +implementation, or conformance-only. The exact 1,036 class/member descriptors are source-controlled in `src/test/resources/hosted-release/required-public-api.txt`; that file is the machine-comparable inventory, while the JSON file supplies intent metadata. At this audited state the required inventory is byte-for-byte identical to `blue-bex-conformance/build/reports/bex-release/public-api.txt`, and both have -SHA-256 `5acb4712e3e03c5ba9a58d87b4a40bed4e1dcca76ed58ef355a77f0efc9d3c92`. +SHA-256 `a330de486f74df49ddf45b3bf896753f6a326e08b7e6f84a8c9aef3bc46cbd43`. Build wiring generates the latter from compiled classes and fails on any diff from the reviewed source-controlled baseline. diff --git a/docs/latest-language-api-migration.json b/docs/latest-language-api-migration.json index 9802d03..b8345fe 100644 --- a/docs/latest-language-api-migration.json +++ b/docs/latest-language-api-migration.json @@ -5,34 +5,34 @@ "bexBaselineCommit": "395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8", "bexWorkingCheckpointCommit": "169e589", "bexMigrationState": "modernization delta rooted at bexWorkingCheckpointCommit; the containing commit is the final target revision", - "languageExactCommit": "505a654699b86b42bf0e282ddf94560a91529bcf", - "languageVerifiedImplementationCommit": "505a654699b86b42bf0e282ddf94560a91529bcf", + "languageExactCommit": "5c4e5c88fa75d6cbc52b2e8772f14f2ac5246f52", + "languageVerifiedImplementationCommit": "5c4e5c88fa75d6cbc52b2e8772f14f2ac5246f52", "languageDeltaClassification": "none", "languageDeltaPaths": [] }, "manifests": { "beforeSha256": "830caa187023079ba53fa76d2932e6e12cb8c93be3f90ac887ad374d6642b315", "workingCheckpointSha256": "43aea6ae9de6f39729f93c146ff5453887da9a2303f6f4f33573ba47f37d5be0", - "afterSha256": "5acb4712e3e03c5ba9a58d87b4a40bed4e1dcca76ed58ef355a77f0efc9d3c92", + "afterSha256": "a330de486f74df49ddf45b3bf896753f6a326e08b7e6f84a8c9aef3bc46cbd43", "workingCheckpointPath": "gradle/verification/api/working-checkpoint-public-api.txt", "requiredPath": "src/test/resources/hosted-release/required-public-api.txt", "generatedPath": "blue-bex-conformance/build/reports/bex-release/public-api.txt", "workingCheckpointClassificationPath": "gradle/verification/api/working-checkpoint-public-api-classification.json", - "workingCheckpointClassificationSha256": "0c3486c3bc2e7f4426e8e3973e74a2319457ccb911bb6da5ab03e6ea23f4a742", + "workingCheckpointClassificationSha256": "b27f400c6bac238e916f75f5ef66337371bea44783fb01f8931eab2835005001", "afterClassificationPath": "docs/public-api-classification.json", - "afterClassificationSha256": "69ffe62280a4feb1eafb93cb4d26ccd730713d2ddf7f132e426016db51e92753", - "publicTypeCount": 101, - "publicDescriptorCount": 1028 + "afterClassificationSha256": "e8ae8fb201704b860b6691310cc86726d94bd1242c1d301e0b6beec22b38ec16", + "publicTypeCount": 103, + "publicDescriptorCount": 1036 }, "modernizationDelta": { "baselineCommit": "169e589", "baselineManifestSha256": "43aea6ae9de6f39729f93c146ff5453887da9a2303f6f4f33573ba47f37d5be0", "removedDescriptorLines": 254, - "addedDescriptorLines": 484, + "addedDescriptorLines": 492, "removedDescriptorsPath": "gradle/verification/api/modernization-removed-descriptors.txt", "removedDescriptorsSha256": "7d3ba5a5e69ddf8e2fee0d92c924cae3c4dd36d42e465dc9931c550a99db2d99", "addedDescriptorsPath": "gradle/verification/api/modernization-added-descriptors.txt", - "addedDescriptorsSha256": "5cb3e8a2ef872d5f2334e9251feca3c083f5c887a76549ce3dd60e2b40a03987", + "addedDescriptorsSha256": "0219d20091f06783915b2a4898297e466e60c8b76944bf9c66bc514fc642a431", "comparison": "complete bytewise set difference after qualifying every member descriptor with its owning class; :blue-bex-conformance:binaryApiCheck recomputes and compares every line", "completeMachineAuditableDelta": true, "packageMoves": [ @@ -88,7 +88,8 @@ "boundaryChanges": [ "BexCompiledProgram executes through blue.bex.compile.BexExecutionMachine rather than concrete BexRuntime.", "BexRuntime implements BexExecutionMachine and consumes BexRuntimeContext/BexRuntimeIntrinsics rather than API-owned concrete host types.", - "Contracts evidence and failure translation are exposed only from blue-bex-contracts." + "Contracts evidence and failure translation are exposed only from blue-bex-contracts.", + "Exact values retain their verified Blue identity and frozen capability through BexExactValueCapability; hosted output admission does not reconstruct or rehash them." ] }, "reviewedHighlights": [ diff --git a/docs/migrating-to-modular-blue-language.md b/docs/migrating-to-modular-blue-language.md index 81b5fc4..39e79bf 100644 --- a/docs/migrating-to-modular-blue-language.md +++ b/docs/migrating-to-modular-blue-language.md @@ -81,11 +81,12 @@ both manifests, both classifications, and the complete additions/removals under Published mode resolves exact module coordinates from the controlled public repository configuration. `mavenLocal()` or an uncontrolled same-GAV repository must not masquerade as release evidence. The strict gate inspects coordinates, -origin, hashes, API, and a local/published differential run. +origin, hashes, API, and four isolated published-mode repeatability runs. -If matching modular Language artifacts are unavailable, local composite work can -still be complete and committed, but `bexReleaseVerify` must remain red or -`not-executed`. See [Release](release.md). +If matching modular Language artifacts are unavailable, optional local-composite +developer work can still proceed, but it is never accepted by +`bexReleaseVerify`, which must remain red or `not-executed`. See +[Release](release.md). ## Consumer migration diff --git a/docs/public-api-classification.json b/docs/public-api-classification.json index 2d44283..bf645f6 100644 --- a/docs/public-api-classification.json +++ b/docs/public-api-classification.json @@ -2,10 +2,10 @@ "schema": "blue-bex-public-api-classification/2.0", "inventory": { "path": "src/test/resources/hosted-release/required-public-api.txt", - "sha256": "5acb4712e3e03c5ba9a58d87b4a40bed4e1dcca76ed58ef355a77f0efc9d3c92", + "sha256": "a330de486f74df49ddf45b3bf896753f6a326e08b7e6f84a8c9aef3bc46cbd43", "manifestSchema": "blue-bex-binary-api-manifest/1.0", - "publicTypeCount": 101, - "publicDescriptorCount": 1028 + "publicTypeCount": 103, + "publicDescriptorCount": 1036 }, "classifications": { "stable API": [ @@ -53,6 +53,7 @@ "blue.bex.api.FrozenBexDocumentView", "blue.bex.contracts.BexContractsExecutionContext", "blue.bex.contracts.BexContractsFailureBoundary", + "blue.bex.contracts.ProcessorExactBlueValueCapability", "blue.bex.contracts.ProcessorExecutionContextBexDocumentView", "blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost", "blue.bex.contracts.ProcessorExecutionContextBexSemanticIdentityBoundary", @@ -62,6 +63,7 @@ "blue.bex.gas.BexSharedGasBudget", "blue.bex.output.BexAdmittedValue", "blue.bex.output.BexEstablishedIdentity", + "blue.bex.output.BexExactValueCapability", "blue.bex.output.BexFailurePolicy", "blue.bex.output.BexOutputAdmission", "blue.bex.output.BexOutputKind", diff --git a/docs/release.md b/docs/release.md index f69bdd0..d413fa9 100644 --- a/docs/release.md +++ b/docs/release.md @@ -1,25 +1,38 @@ # Release -BEX has separate working and public-release gates. Keeping them separate allows -downstream local integration to proceed without pretending that unpublished -Language artifacts have public provenance. +BEX has separate working and public-release gates. Keeping them separate +distinguishes same-checkout verification from the isolated repeatability proof +required for publication. -## Working local gate +## Working published gate ```bash -./gradlew --no-daemon clean bexWorkingVerification \ - -PblueLanguageCompositePath=/absolute/path/to/blue-language-java +./gradlew --no-daemon clean bexWorkingVerification ``` -This mandatory gate uses the exact local modular Language checkout and requires +This mandatory gate resolves the exact reviewed modular Language release from +Maven Central and requires all ordinary/conformance/hosted tests, 60 vectors, 105 behavior fixtures, 30 gas microfixtures, 86 operator checks, Java 8 bytecode, API reports, runtime smoke, artifact construction, BEX-owned archive determinism, and current dependency and source evidence. Success requires zero failed, skipped, or unclassified evidence and -`workingReady = true`. A green, reviewable working commit is a usable local -artifact checkpoint even when published Language modules do not yet exist. +`workingReady = true`. An included-build Language checkout is not a supported +input to this gate. + +## Retired SDK candidate stage + +The isolated SDK stage was used before Language `3.1.0-rc.21` was published. +Its source lock remains as historical evidence in +`gradle/verification/sdk-stage-language-baseline.json`, but its status is +`retired-after-publication` and it is not accepted by a public BEX gate. + +The ledger truthfully records that the staged candidate used Language commit +`e0dfc897ea7d158895325fae2bf84e103b8c1989` while `3.1.0-rc.20` was the +published release. The final `3.1.0-rc.21` tag instead resolves to commit +`5c4e5c88fa75d6cbc52b2e8772f14f2ac5246f52`. Current release evidence is kept +only in the latest-Language baseline and published API inspection. ## Strict public gate @@ -32,20 +45,25 @@ The strict gate additionally requires: - exact compatible published Language module coordinates from controlled repositories (never an ambiguous `mavenLocal` substitute); - published artifact API and SHA-256 inspection; -- semantic and exact gas differential proof between authenticated local and - published dependency modes; -- two independent clean builds with isolated Gradle homes and matching artifact - hashes; +- same-run published conformance receipts from authenticated artifacts; +- four independent clean standalone-published builds with isolated Gradle homes + and matching artifact hashes and conformance receipts; - a clean tagged BEX source state and commit-bound evidence; - `releaseReady = true` in the final report. -The script is the supported publication entry point. It checks out the exact -reviewed Language tag, runs the local working and modernization gates, builds -two clean standalone and two clean local-composite BEX checkouts with isolated -Gradle homes, compares their artifact manifests, derives a local/published -semantic and exact-gas differential, authenticates the resolved Language JARs, +The script is the supported publication entry point. It runs the published +working and modernization gates, executes four clean standalone-published BEX +builds as two independently isolated build pairs, compares their artifact +manifests and conformance receipts, authenticates the resolved Language JARs, and finally invokes `bexReleaseVerify`. +On success, the script retains its four isolated checkouts, manifests, and +Gradle homes under the ephemeral runner temporary directory. The later +`publish` and JReleaser invocations re-open that live evidence rather than +trusting copied JSON alone, and each receives its own empty Gradle home. Failed +gates remove the temporary evidence immediately; hosted runners discard it at +the end of the job. + If matching published modules or any independent evidence is absent, the gate is truthfully red or `not-executed`. It must not be described as passing and must not be weakened to unblock local work. @@ -57,16 +75,18 @@ JAR, Javadoc JAR, source archive, API descriptors and migration ledger, dependency lock/evidence, working report, and strict release report. All BEX-owned archives must be reproducible. -Reports record source commit and dirty state, Language exact/code-equivalent -commits, dependency graph and JAR hashes, registry/gas/fixture identities, -test/fixture/operator totals, semantic and gas parity, hosted boundaries, Java 8 -verification, artifact hashes, and the exact local/published mode status. +Reports record source commit and dirty state, exact published Language source +and artifact identities, dependency graph and JAR hashes, +registry/gas/fixture identities, test/fixture/operator totals, semantic and gas +parity, hosted boundaries, Java 8 verification, artifact hashes, and the exact +standalone-published mode and repeatability status. The strict decision is written to `build/reports/bex-release/final.json` and `final.md` even when the Gradle task fails closed. -Do not publish from a dirty tree, change `.cz.toml` as part of this work, embed a -local checkout path in published metadata, or stage downloaded archives. Version -automation and tags remain the repository's existing release process. +Do not publish from a dirty tree, rewrite `.cz.toml` merely to select a +dependency version, embed a local checkout path in published metadata, or stage +downloaded archives. Version automation and tags remain the repository's +existing release process. ## Benchmark reporting @@ -74,7 +94,7 @@ JMH sources belong to the benchmark suite, but a benchmark claim is valid only when a report records JVM/CPU details, forks, warmups, measurements, allocation data, confidence intervals, and result/gas identity checks. Merely compiling a benchmark is not a performance result. Long benchmark campaigns do not block the -first local compatibility checkpoint, but release/modernization reports must +first working compatibility checkpoint, but release/modernization reports must label unexecuted campaigns honestly. `bexModernizationVerification` consumes the serious `results.json` plus the recorded JVM/OS/CPU/campaign environment; the bounded `jmhSmoke` result belongs only to the working compatibility gate. diff --git a/gradle/verification/api/modernization-added-descriptors.txt b/gradle/verification/api/modernization-added-descriptors.txt index dcf7cbb..1cfffc2 100644 --- a/gradle/verification/api/modernization-added-descriptors.txt +++ b/gradle/verification/api/modernization-added-descriptors.txt @@ -76,11 +76,13 @@ class public abstract interface blue.bex.gas.BexSharedGasBudget class public abstract interface blue.bex.gas.BexSharedGasBudget :: method public abstract admittedGas():long class public abstract interface blue.bex.gas.BexSharedGasBudget :: method public abstract maximumGas():long class public abstract interface blue.bex.gas.BexSharedGasBudget :: method public abstract remainingGas():long +class public abstract interface blue.bex.output.BexExactValueCapability class public abstract interface blue.bex.output.BexFailurePolicy class public abstract interface blue.bex.output.BexFailurePolicy :: field public static final STANDALONE:blue.bex.output.BexFailurePolicy class public abstract interface blue.bex.output.BexFailurePolicy :: method public abstract evidenceUnavailable(java.lang.Throwable):boolean class public abstract interface blue.bex.output.BexFailurePolicy :: method public preserveOrWrap(java.lang.String,java.lang.RuntimeException):java.lang.RuntimeException class public abstract interface blue.bex.output.BexFailurePolicy :: method public translate(java.lang.RuntimeException):java.lang.RuntimeException +class public abstract interface blue.bex.output.BexSemanticIdentityBoundary :: method public carryExactIdentity(java.lang.String,blue.language.snapshot.FrozenNode):blue.bex.output.BexEstablishedIdentity class public abstract interface blue.bex.runtime.BexRuntimeContext class public abstract interface blue.bex.runtime.BexRuntimeContext :: method public abstract binding(java.lang.String):blue.bex.value.BexValue class public abstract interface blue.bex.runtime.BexRuntimeContext :: method public abstract currentContract():blue.bex.value.BexValue @@ -236,6 +238,8 @@ class public final blue.bex.contracts.BexContractsFailureBoundary implements blu class public final blue.bex.contracts.BexContractsFailureBoundary implements blue.bex.api.BexFailureBoundary :: field public static final INSTANCE:blue.bex.contracts.BexContractsFailureBoundary class public final blue.bex.contracts.BexContractsFailureBoundary implements blue.bex.api.BexFailureBoundary :: method public classify(java.lang.Throwable):blue.bex.api.BexFailureBoundary$Classification class public final blue.bex.contracts.BexContractsFailureBoundary implements blue.bex.api.BexFailureBoundary :: method public translate(java.lang.RuntimeException):java.lang.RuntimeException +class public final blue.bex.contracts.ProcessorExactBlueValueCapability implements blue.bex.output.BexExactValueCapability +class public final blue.bex.contracts.ProcessorExactBlueValueCapability implements blue.bex.output.BexExactValueCapability :: method public exactValue():blue.language.processor.ExactBlueValue class public final blue.bex.contracts.ProcessorExecutionContextBexDocumentView implements blue.bex.api.BexDocumentView class public final blue.bex.contracts.ProcessorExecutionContextBexDocumentView implements blue.bex.api.BexDocumentView :: constructor public (blue.language.processor.ProcessorExecutionContext) class public final blue.bex.contracts.ProcessorExecutionContextBexDocumentView implements blue.bex.api.BexDocumentView :: method public canonicalAt(java.lang.String):blue.bex.value.BexValue @@ -259,6 +263,7 @@ class public final blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost class public final blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost implements blue.bex.api.BexGasLedgerHost :: method public submit(blue.bex.gas.BexGasLedgerCapability):void class public final blue.bex.contracts.ProcessorExecutionContextBexSemanticIdentityBoundary implements blue.bex.output.BexSemanticIdentityBoundary class public final blue.bex.contracts.ProcessorExecutionContextBexSemanticIdentityBoundary implements blue.bex.output.BexSemanticIdentityBoundary :: constructor public (blue.language.processor.ProcessorExecutionContext) +class public final blue.bex.contracts.ProcessorExecutionContextBexSemanticIdentityBoundary implements blue.bex.output.BexSemanticIdentityBoundary :: method public carryExactIdentity(java.lang.String,blue.language.snapshot.FrozenNode):blue.bex.output.BexEstablishedIdentity class public final blue.bex.contracts.ProcessorExecutionContextBexSemanticIdentityBoundary implements blue.bex.output.BexSemanticIdentityBoundary :: method public establishIdentity(blue.language.model.Node):blue.bex.output.BexEstablishedIdentity class public final blue.bex.gas.BexGasChargeContext class public final blue.bex.gas.BexGasChargeContext :: method public contractKey():java.lang.String @@ -281,6 +286,9 @@ class public final blue.bex.gas.BexHostGasExhaustion extends java.lang.RuntimeEx class public final blue.bex.gas.BexHostGasExhaustion extends java.lang.RuntimeException :: method public namespace():java.lang.String class public final blue.bex.gas.BexHostGasExhaustion extends java.lang.RuntimeException :: method public quantity():long class public final blue.bex.gas.BexHostGasExhaustion extends java.lang.RuntimeException :: method public weight():long +class public final blue.bex.output.BexAdmittedValue :: method public exactCapability():blue.bex.output.BexExactValueCapability +class public final blue.bex.output.BexEstablishedIdentity :: constructor public (java.lang.String,blue.language.snapshot.FrozenNode,blue.bex.output.BexExactValueCapability) +class public final blue.bex.output.BexEstablishedIdentity :: method public exactCapability():blue.bex.output.BexExactValueCapability class public final blue.bex.output.BexOutputAdmission :: constructor public (blue.bex.gas.BexGasMeter,blue.bex.output.BexSemanticIdentityBoundary,blue.bex.output.BexFailurePolicy) class public final blue.bex.pointer.BexPointerCache :: method public synchronized get(java.lang.String,blue.bex.result.BexMetricsRecorder):blue.bex.pointer.BexPointer class public final blue.bex.result.BexChangeset implements blue.bex.value.BexChangesetValueView diff --git a/gradle/verification/api/working-checkpoint-public-api-classification.json b/gradle/verification/api/working-checkpoint-public-api-classification.json index 8bbe5bf..34f7093 100644 --- a/gradle/verification/api/working-checkpoint-public-api-classification.json +++ b/gradle/verification/api/working-checkpoint-public-api-classification.json @@ -10,8 +10,8 @@ "sourceState": { "bexBaselineCommit": "395c484111f8c4e9e0e98d2db7f1c5b0777bd5a8", "bexMigrationState": "working-tree delta rooted at bexBaselineCommit", - "languageExactCommit": "505a654699b86b42bf0e282ddf94560a91529bcf", - "languageVerifiedImplementationCommit": "505a654699b86b42bf0e282ddf94560a91529bcf" + "languageExactCommit": "5c4e5c88fa75d6cbc52b2e8772f14f2ac5246f52", + "languageVerifiedImplementationCommit": "5c4e5c88fa75d6cbc52b2e8772f14f2ac5246f52" }, "classifications": { "stable API": [ diff --git a/gradle/verification/latest-language-baseline.json b/gradle/verification/latest-language-baseline.json index d8c616a..02802e8 100644 --- a/gradle/verification/latest-language-baseline.json +++ b/gradle/verification/latest-language-baseline.json @@ -78,38 +78,56 @@ } }, "language": { - "exactHead": "505a654699b86b42bf0e282ddf94560a91529bcf", - "verifiedImplementationCommit": "505a654699b86b42bf0e282ddf94560a91529bcf", - "verifiedReleaseVersion": "3.1.0-rc.20", - "localCompositeProjectVersion": "3.1.0-rc.20", - "bexDeclaredPublishedCandidateVersion": "3.1.0-rc.20", - "publishedCandidateStatus": "passed", + "exactHead": "5c4e5c88fa75d6cbc52b2e8772f14f2ac5246f52", + "verifiedImplementationCommit": "5c4e5c88fa75d6cbc52b2e8772f14f2ac5246f52", + "sourceTag": "v3.1.0-rc.21", + "sourceTagObject": "447b4bc440cc720a5fdd09fe50b9c9a280645a4f", + "verifiedReleaseVersion": "3.1.0-rc.21", + "bexDeclaredPublishedVersion": "3.1.0-rc.21", + "publishedReleaseStatus": "passed", + "publishedRepository": "https://repo1.maven.org/maven2", + "publishedMetadataLastUpdated": "20260820080329", "documentationOnlyDiffPaths": [], - "czTomlSha256": "32b6457085126ec8b2701840c3a8da6a4c6714509c867ecfb4cace3632cf13f6", + "czTomlSha256": "9d041e324ef28255fc905095cd29322dd8765009fd3890f48b4c0afa4208a8da", "focusedModules": [ { - "declaredPublishedCoordinate": "blue.language:blue-language-model:3.1.0-rc.20", - "projectPath": ":blue-language-model", - "localArtifact": "blue-language-model-3.1.0-rc.20.jar", - "verifiedLocalArtifactSha256": "ef55be8331147442b858474add4782489d993568effe30202a9c4a8b014d5bd8" + "declaredPublishedCoordinate": "blue.language:blue-language-model:3.1.0-rc.21", + "artifactId": "blue-language-model", + "publishedArtifact": "blue-language-model-3.1.0-rc.21.jar", + "verifiedPublishedArtifactSha256": "ef55be8331147442b858474add4782489d993568effe30202a9c4a8b014d5bd8" }, { - "declaredPublishedCoordinate": "blue.language:blue-language-core:3.1.0-rc.20", - "projectPath": ":blue-language-core", - "localArtifact": "blue-language-core-3.1.0-rc.20.jar", - "verifiedLocalArtifactSha256": "916d5e6315f34d25ad4a2ddbc5587a209506871ea70dd2daa7aa69dbdbe1263d" + "declaredPublishedCoordinate": "blue.language:blue-language-core:3.1.0-rc.21", + "artifactId": "blue-language-core", + "publishedArtifact": "blue-language-core-3.1.0-rc.21.jar", + "verifiedPublishedArtifactSha256": "8d7167254a39132e7a494561ed966748918c138c08f0967ccc3e841edba0b1f0" }, { - "declaredPublishedCoordinate": "blue.language:blue-language-mapping:3.1.0-rc.20", - "projectPath": ":blue-language-mapping", - "localArtifact": "blue-language-mapping-3.1.0-rc.20.jar", - "verifiedLocalArtifactSha256": "d9141d5c611bde7eb6a21bce3dc4bc0df7d8167f013eeaef2a365dd0a6af329b" + "declaredPublishedCoordinate": "blue.language:blue-language-mapping:3.1.0-rc.21", + "artifactId": "blue-language-mapping", + "publishedArtifact": "blue-language-mapping-3.1.0-rc.21.jar", + "verifiedPublishedArtifactSha256": "d9141d5c611bde7eb6a21bce3dc4bc0df7d8167f013eeaef2a365dd0a6af329b" }, { - "declaredPublishedCoordinate": "blue.language:blue-contracts-core:3.1.0-rc.20", - "projectPath": ":blue-contracts-core", - "localArtifact": "blue-contracts-core-3.1.0-rc.20.jar", - "verifiedLocalArtifactSha256": "5845c6bead274dffd8d22afcb323f7cdf6e53b5656e0070bd241a1a660516280" + "declaredPublishedCoordinate": "blue.language:blue-contracts-core:3.1.0-rc.21", + "artifactId": "blue-contracts-core", + "publishedArtifact": "blue-contracts-core-3.1.0-rc.21.jar", + "verifiedPublishedArtifactSha256": "66ce3f0ba7e76118ddc95b40a3105bbfa5ab49fd56b3c57b7c4a1ff93d72b432" + } + ], + "publishedAggregate": { + "coordinate": "blue.language:blue-language-java:3.1.0-rc.21", + "artifact": "blue-language-java-3.1.0-rc.21.jar", + "artifactSha256": "0de1584be094515ddd27938819464dc024a993c7eb06e4145cac129ad5bbfed0" + }, + "additionalPublishedComponents": [ + { + "coordinate": "blue.language:blue-conformance:3.1.0-rc.21", + "artifactSha256": "db1a398958d02c8b80d04cba0f3997d72f14c5965c106a356d7043d8b92b9e10" + }, + { + "coordinate": "blue.language:blue-language-ipfs:3.1.0-rc.21", + "artifactSha256": "bec7355f39a109c4fe6dfc5f9970232dc0a75cd8e5b4ab055abc311314d24c8e" } ], "hostingPackageIdentities": { diff --git a/gradle/verification/sdk-stage-language-baseline.json b/gradle/verification/sdk-stage-language-baseline.json new file mode 100644 index 0000000..108798e --- /dev/null +++ b/gradle/verification/sdk-stage-language-baseline.json @@ -0,0 +1,34 @@ +{ + "schema": "blue-bex-sdk-stage-baseline/1.0", + "status": "retired-after-publication", + "language": { + "candidateVersion": "3.1.0-rc.21", + "sourceCommit": "e0dfc897ea7d158895325fae2bf84e103b8c1989", + "sourceBranch": "feature/cyclic-topology", + "releaseVersionSelection": "-PreleaseVersion=3.1.0-rc.21", + "historicalPublishedVersion": "3.1.0-rc.20", + "historicalPublishedEvidence": [], + "publishedRelease": { + "version": "3.1.0-rc.21", + "sourceCommit": "5c4e5c88fa75d6cbc52b2e8772f14f2ac5246f52", + "sourceTag": "v3.1.0-rc.21", + "sourceTagObject": "447b4bc440cc720a5fdd09fe50b9c9a280645a4f", + "currentEvidence": [ + "src/test/resources/hosted-release/published-api-inspection.properties", + "gradle/verification/latest-language-baseline.json" + ] + } + }, + "bex": { + "candidateVersion": "1.1.0-rc.4", + "candidateVersionSelection": "-PbexLocalStageVersion=1.1.0-rc.4", + "historicalReleaseVersion": "1.1.0-rc.3", + "historicalReleaseMetadata": ".cz.toml" + }, + "policy": { + "dependencyMode": "retired-staged-repository", + "publishedEvidenceComparison": "superseded-by-authenticated-standalone-published-release", + "artifactAcceptance": "historical candidate evidence is not accepted for publication", + "publication": "public gates consume authenticated Maven Central artifacts only" + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 4d52e79..9e7ad70 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,6 +24,39 @@ val compositePath = providers.gradleProperty("blueLanguageCompositePath") .orNull ?.trim() ?.takeIf(String::isNotEmpty) +val stagedRepositoryPath = providers.gradleProperty("blueLanguageRepository") + .orNull + ?.trim() + ?.takeIf(String::isNotEmpty) +val publishedOnlyTasks = setOf( + "bexPublishedDependencyVerification", + "bexPublishedLanguageVerification", + "bexWorkingVerification", + "generateBexWorkingReport", + "bexModernizationVerification", + "generateBexModernizationReport", + "bexReleaseVerify", + "generateBexReleaseReport", + "bexReleaseEvidence", + "publish", + "jreleaserFullRelease" +) +val requestedPublishedOnlyTask = gradle.startParameter.taskNames + .map { it.substringAfterLast(':') } + .firstOrNull { + it in publishedOnlyTasks || it.startsWith("publish") || + it.startsWith("jreleaser") + } +if (requestedPublishedOnlyTask != null) { + require(compositePath == null) { + "$requestedPublishedOnlyTask is published-only and forbids " + + "blueLanguageCompositePath" + } + require(stagedRepositoryPath == null) { + "$requestedPublishedOnlyTask is published-only and forbids " + + "blueLanguageRepository" + } +} if (compositePath != null) { val checkout = file(compositePath) diff --git a/src/test/java/blue/bex/BexExactReferenceDocumentTest.java b/src/test/java/blue/bex/BexExactReferenceDocumentTest.java index 230bf69..5748301 100644 --- a/src/test/java/blue/bex/BexExactReferenceDocumentTest.java +++ b/src/test/java/blue/bex/BexExactReferenceDocumentTest.java @@ -6,6 +6,7 @@ import blue.bex.api.FrozenBexDocumentView; import blue.bex.value.BexValue; import blue.bex.value.BexValues; +import blue.bex.value.BexFrozenWriter; import blue.bex.test.TestBlue; import blue.language.provider.NodeProvider; import blue.language.model.Node; @@ -72,6 +73,33 @@ void semanticAccessMaterializesVerifiedReferenceButExactIdentityDoesNot() { } } + @Test + void verifiedOrdinaryReferenceRetainsItsCanonicalBodyForOutputAdmission() { + Node content = obj( + "channel", obj("timeline", "orders"), + "operation", "reconfigure"); + String blueId = calculateBlueId(content); + NodeProvider provider = requestedBlueId -> blueId.equals( + requestedBlueId) + ? Collections.singletonList(content.clone()) + : Collections.emptyList(); + + try (TestBlue blue = new TestBlue(provider)) { + BexValue reference = BexValues.referenceBacked( + BexValues.frozen(FrozenNode.fromNode( + new Node().blueId(blueId))), + blue.runtime()); + + assertEquals("object", BexValues.kind(reference)); + FrozenNode retained = BexFrozenWriter.toFrozen(reference); + + assertTrue(retained.isStrictCanonical()); + assertEquals(blueId, retained.blueId()); + assertTrue(retained.sameResolvedStructure( + FrozenNode.fromNode(content))); + } + } + @Test void unavailableReferenceEvidencePropagatesInsteadOfBecomingUndefined() { Node unavailableContent = obj("a", 1); diff --git a/src/test/java/blue/bex/conformance/BexConformanceReportMain.java b/src/test/java/blue/bex/conformance/BexConformanceReportMain.java index f0bde05..64d4e5b 100644 --- a/src/test/java/blue/bex/conformance/BexConformanceReportMain.java +++ b/src/test/java/blue/bex/conformance/BexConformanceReportMain.java @@ -104,7 +104,10 @@ public static void main(String[] args) throws Exception { specificationEvidence(projectDir, baseline); Map versionAutomation = versionAutomationEvidence( - projectDir, projectVersion, baseline); + projectDir, + projectVersion, + baseline, + dependencyMode); Map namedEvidence = namedReleaseEvidence(tests); Map gasExhaustionTraceExamples = @@ -176,25 +179,30 @@ && modeRunCanPersistEvidence( namedEvidence, publishedApiInspection); } - Map buildModes = buildModeMatrix( - persistentEvidenceRoot, - declaredDependency, - projectVersion, - sourceState, - compositePath, - publishedApiInspection); - boolean exactFinalArtifactProven = - bindLanguageReleaseIdentityToModes( + boolean stagedRepositoryMode = "staged-repository".equals( + dependencyMode); + Map buildModes = stagedRepositoryMode + ? publicReleaseMatrixNotSelected(persistentEvidenceRoot) + : buildModeMatrix( + persistentEvidenceRoot, + declaredDependency, + projectVersion, + sourceState, + compositePath, + publishedApiInspection); + boolean exactFinalArtifactProven = !stagedRepositoryMode + && bindLanguageReleaseIdentityToModes( languageReleaseIdentity, buildModes); releaseGates.put( "cleanDependencyCacheAcceptance", cleanDependencyCacheAcceptance(buildModes)); - boolean bothModesPassed = - Boolean.TRUE.equals(buildModes.get("allRequiredModesPassed")); + boolean publishedEvidencePassed = + Boolean.TRUE.equals(buildModes.get( + "allRequiredPublishedEvidencePassed")); boolean releaseReady = currentModeFailures.isEmpty() - && bothModesPassed + && publishedEvidencePassed && exactFinalArtifactProven; Map report = new LinkedHashMap(); @@ -218,7 +226,9 @@ && modeRunCanPersistEvidence( "declaredCoordinate", declaredDependency, "resolution", dependencyResolution, "localComposite", - compositeDependencyEvidence(compositePath))); + map( + "status", + "not-applicable-to-published-only-release"))); report.put("hostedStandaloneMatrix", buildModes); report.put("publishedHostApiInspection", evidenceMap(publishedApiInspection)); @@ -332,7 +342,7 @@ && modeRunCanPersistEvidence( ? "all-required-evidence-passed" : readinessReason( currentModeFailures, - bothModesPassed))); + publishedEvidencePassed))); System.out.println("BEX conformance report: " + output); System.out.println("BEX conformance report: " + markdown); } @@ -507,6 +517,13 @@ private static Map dependencyResolutionEvidence( String languageCheckoutState = jsonString( evidence, "languageCheckoutState"); boolean standalone = "standalone-published".equals(expectedMode); + boolean staged = "staged-repository".equals(expectedMode); + boolean stagedRepositoryEvidenceApplicable = jsonBoolean( + evidence, "stagedRepositoryEvidenceApplicable"); + String stagedRepositoryPath = jsonString( + evidence, "stagedRepositoryPath"); + boolean stagedRepositoryArtifactsMatchResolved = jsonBoolean( + evidence, "stagedRepositoryArtifactsMatchResolved"); boolean cleanCacheInitiallyAbsent = jsonBoolean( evidence, "exactVersionCacheInitiallyAbsent"); List> artifacts = @@ -543,8 +560,12 @@ private static Map dependencyResolutionEvidence( && aggregateArtifact != null && publishedInspection.get("artifact.sha256").equals( aggregateArtifact.get("sha256")) - : languageCommit.matches("[0-9a-f]{40}") - && "clean".equals(languageCheckoutState); + : staged + ? stagedRepositoryEvidenceApplicable + && !stagedRepositoryPath.isEmpty() + && stagedRepositoryArtifactsMatchResolved + : languageCommit.matches("[0-9a-f]{40}") + && "clean".equals(languageCheckoutState); boolean cleanCacheAccepted = !standalone || cleanCacheInitiallyAbsent; boolean passed = "passed".equals(jsonString(evidence, "status")) @@ -562,7 +583,7 @@ private static Map dependencyResolutionEvidence( "receiptSha256", sha256(evidencePath), "mode", mode, "declaredCoordinate", declaredDependency, - "effectiveComponent", standalone + "effectiveComponent", standalone || staged ? declaredDependency : "project :blue-language-java", "effectiveCoordinate", declaredDependency, "declaredLanguageVersion", declaredVersion, @@ -578,20 +599,38 @@ private static Map dependencyResolutionEvidence( ? "passed" : "failed", "kind", standalone ? "reviewed-published-focused-modules" - : "exact-clean-local-composite", + : staged + ? "isolated-staged-repository" + : "exact-clean-local-composite", "publishedReviewStatus", publishedInspection.get("status"), - "repositoryPolicy", "maven-central-only", + "repositoryPolicy", standalone + ? "maven-central-only" + : staged + ? "explicit-staged-repository-before-maven-central" + : "included-build-substitution", "recordedRepository", - publishedInspection.get("repository"), + staged + ? stagedRepositoryPath + : publishedInspection.get("repository"), "recordedCoordinate", - publishedInspection.get("coordinate"), + staged + ? declaredDependency + : publishedInspection.get("coordinate"), "recordedSha256", - publishedInspection.get("artifact.sha256"), + staged && aggregateArtifact != null + ? aggregateArtifact.get("sha256") + : publishedInspection.get("artifact.sha256"), "resolvedHashMatchesRecordedMavenCentralHash", standalone && sourceProvenanceValid, + "stagedRepositoryArtifactsMatchResolved", + staged && stagedRepositoryArtifactsMatchResolved, "networkFetchObservation", - standalone ? "isolated-resolution" : "not-applicable"), + standalone + ? "isolated-resolution" + : staged + ? "local-staged-resolution" + : "not-applicable"), "cleanDependencyCacheAcceptance", map( "status", cleanCacheAccepted ? "passed" : "failed", @@ -601,8 +640,18 @@ private static Map dependencyResolutionEvidence( "moduleVersionInitiallyAbsentAtProjectConfiguration", cleanCacheInitiallyAbsent, "reason", standalone - ? "exact focused-module version cache must be absent before isolated resolution" - : "fresh module cache proof is not required for local composite mode"), + ? "exact focused-module version cache must be " + + "absent before isolated resolution" + : staged + ? "explicit staged repository hashes " + + "are verified instead of a " + + "remote cache miss" + : "fresh module cache proof is not " + + "required for local " + + "composite mode"), + "stagedRepositoryPath", stagedRepositoryPath, + "stagedRepositoryArtifactsMatchResolved", + staged && stagedRepositoryArtifactsMatchResolved, "compositePath", compositePath != null ? compositePath.toString() : ""); } @@ -610,6 +659,9 @@ private static Map dependencyResolutionEvidence( static boolean modeRunCanPersistEvidence( String dependencyMode, Map dependencyResolution) { + if ("local-composite".equals(dependencyMode)) { + return false; + } if (!"standalone-published".equals(dependencyMode)) { return true; } @@ -677,7 +729,8 @@ private static Map specificationEvidence( static Map versionAutomationEvidence( Path projectDir, String projectVersion, - Map baseline) throws IOException { + Map baseline, + String dependencyMode) throws IOException { Path czToml = projectDir.resolve(".cz.toml"); String actual = Files.isRegularFile(czToml) ? sha256(czToml) @@ -690,6 +743,11 @@ static Map versionAutomationEvidence( projectVersion.length() - "-SNAPSHOT".length()) : projectVersion; + boolean stagedCandidate = "staged-repository".equals( + dependencyMode); + boolean explicitStageCandidate = stagedCandidate + && projectVersion.matches( + "[0-9]+\\.[0-9]+\\.[0-9]+-rc\\.[0-9]+"); return map( "path", ".cz.toml", "sha256", actual, @@ -697,8 +755,15 @@ static Map versionAutomationEvidence( "matchesHistoricalBaseline", actual.equals(expected), "configuredVersion", configuredVersion, "projectVersion", projectVersion, + "selectionKind", stagedCandidate + ? "explicit-staged-candidate" + : "commitizen-release-version", + "historicalConfiguredVersion", configuredVersion, + "candidateOverrideAccepted", explicitStageCandidate, "matchesProjectVersion", - configuredVersion.equals(expectedVersion)); + stagedCandidate + ? explicitStageCandidate + : configuredVersion.equals(expectedVersion)); } private static String readCommitizenVersion(Path czToml) @@ -1047,6 +1112,8 @@ private static List currentModeFailures( Map languageReleaseIdentity, Map representationMatrixResult) { List failures = new ArrayList(); + boolean stagedRepository = "staged-repository".equals( + dependencyResolution.get("mode")); require( failures, "passed".equals(tests.overallStatus()), @@ -1109,12 +1176,14 @@ private static List currentModeFailures( failures, gatePassed(releaseGates, "deterministicArchives"), "archive-packaging-determinism-gate-not-passing"); - require( - failures, - gatePassed( - releaseGates, - "independentCleanBuilds"), - "independent-clean-build-reproducibility-gate-not-passing"); + if (!stagedRepository) { + require( + failures, + gatePassed( + releaseGates, + "independentCleanBuilds"), + "independent-clean-build-reproducibility-gate-not-passing"); + } require( failures, gatePassed(releaseGates, "binaryApi"), @@ -1167,8 +1236,12 @@ private static List currentModeFailures( failures, Boolean.TRUE.equals( languageReleaseIdentity.get( - "exactFinalArtifactProven")), - "exact-final-language-artifact-not-proven"); + stagedRepository + ? "exactSelectedArtifactProven" + : "exactFinalArtifactProven")), + stagedRepository + ? "exact-staged-language-artifact-not-proven" + : "exact-final-language-artifact-not-proven"); require( failures, "passed".equals( @@ -1314,60 +1387,56 @@ private static Map languageReleaseIdentity( declaredDependency.equals( publishedApiInspection.get("coordinate")); - Map localSource = - Collections.emptyMap(); - boolean localMatchesPublished = compositePath == null; - if (compositePath != null - && Files.isDirectory(compositePath)) { - SourceState state = sourceState(compositePath); - List tagsAtHead = - gitTagsAtHead(compositePath); - localMatchesPublished = - !state.worktreeDirty - && state.completeWorkspace() - && publishedSourceIdentityMatches( - declaredDependency, - publishedApiInspection, - state.commit, - tagsAtHead); - localSource = new LinkedHashMap( - state.report()); - localSource.put("tagsAtHead", tagsAtHead); - localSource.put( - "matchesPublishedIdentity", - localMatchesPublished); - } + boolean standalone = + "standalone-published".equals( + dependencyResolution.get("mode")); + boolean staged = + "staged-repository".equals( + dependencyResolution.get("mode")); Map resolvedArtifact = castMap(dependencyResolution.get("artifact")); boolean dependencyResolved = "passed".equals( dependencyResolution.get("status")); - boolean standalone = - "standalone-published".equals( - dependencyResolution.get("mode")); + Map dependencyProvenance = castMap( + dependencyResolution.get("provenance")); boolean resolvedArtifactHashMatchesPublished = standalone && publishedHash != null && publishedHash.equals( resolvedArtifact.get("sha256")); boolean resolvedArtifactIdentitySatisfied = - !standalone - || resolvedArtifactHashMatchesPublished; - boolean exactFinalArtifactProven = - commitIdentified - && hashIdentified - && compatible - && coordinateMatches - && localMatchesPublished - && dependencyResolved - && resolvedArtifactIdentitySatisfied; + standalone && resolvedArtifactHashMatchesPublished; + boolean exactStagedArtifactProven = staged + && dependencyResolved + && "passed".equals(dependencyProvenance.get("status")) + && Boolean.TRUE.equals(dependencyProvenance.get( + "stagedRepositoryArtifactsMatchResolved")) + && String.valueOf(resolvedArtifact.get("sha256")) + .matches("[0-9a-f]{64}"); + boolean exactFinalArtifactProven = standalone + && commitIdentified + && hashIdentified + && compatible + && coordinateMatches + && dependencyResolved + && resolvedArtifactIdentitySatisfied; return map( "schema", - "blue-bex-language-release-identity/1.1", + "blue-bex-language-release-identity/1.2", "exactFinalArtifactProven", exactFinalArtifactProven, + "exactSelectedArtifactProven", + staged + ? exactStagedArtifactProven + : exactFinalArtifactProven, "currentDependencyExactFinalArtifactProven", exactFinalArtifactProven, + "selectionKind", staged + ? "staged-repository-candidate" + : standalone + ? "published-coordinate" + : "local-composite", "declaredCoordinate", declaredDependency, "publishedCoordinate", publishedApiInspection.get("coordinate"), @@ -1386,12 +1455,13 @@ && publishedSourceIdentityMatches( resolvedArtifactHashMatchesPublished, "resolvedArtifactIdentitySatisfied", resolvedArtifactIdentitySatisfied, + "stagedRepositoryArtifactsMatchResolved", + staged && Boolean.TRUE.equals(dependencyProvenance.get( + "stagedRepositoryArtifactsMatchResolved")), "resolvedArtifact", resolvedArtifact, - "localCompositeSource", localSource, - "localCompositeMatchesPublishedCommit", - localMatchesPublished, - "localCompositeMatchesPublishedIdentity", - localMatchesPublished, + "releaseDependencyPolicy", "published-only", + "localCompositeStatus", + "not-applicable-to-published-only-release", "failures", exactFinalArtifactProven ? Collections.emptyList() @@ -1414,12 +1484,17 @@ && publishedSourceIdentityMatches( resolvedArtifactIdentitySatisfied ? null : "resolved-artifact-hash-mismatch", - localMatchesPublished + standalone ? null - : "local-composite-not-clean-exact-published-commit-and-version-tag") + : "published-only-release-requires-standalone-published-dependency") .stream() .filter(Objects::nonNull) - .collect(Collectors.toList())); + .collect(Collectors.toList()), + "selectedArtifactFailures", + staged && !exactStagedArtifactProven + ? Collections.singletonList( + "staged-artifact-provenance-not-passing") + : Collections.emptyList()); } private static Map representationMatrixResult( @@ -1705,6 +1780,9 @@ private static void persistModeEvidence( static String modeEvidenceProvenanceStatus( String mode, Map provenance) { + if ("local-composite".equals(mode)) { + return "not-applicable-to-published-only-release"; + } String status = String.valueOf(provenance.get("status")); if (!"standalone-published".equals(mode)) { return status; @@ -1719,6 +1797,9 @@ static String modeEvidenceProvenanceStatus( static String modeEvidenceCacheScope( String mode, Map cacheAcceptance) { + if ("local-composite".equals(mode)) { + return "not-applicable-to-published-only-release"; + } return "standalone-published".equals(mode) ? "standalone-published-blue-language-module-version-cache" : String.valueOf(cacheAcceptance.get("scope")); @@ -1764,28 +1845,19 @@ private static Map buildModeMatrix( sourceState, activeCompositePath, publishedApiInspection); - Map local = validateModeEvidence( - root, - "local-composite", - declaredDependency, - projectVersion, - sourceState, - activeCompositePath, - publishedApiInspection); - boolean bothPassed = - "passed".equals(standalone.get("status")) - && "passed".equals(local.get("status")); - boolean artifactsEquivalent = bothPassed - && artifactHashes(standalone).equals( - artifactHashes(local)); + Map local = map( + "mode", "local-composite", + "status", "not-applicable-to-published-only-release", + "evidenceRead", false, + "effectiveCoordinate", "not-applicable"); boolean allRequired = - bothPassed && artifactsEquivalent; + "passed".equals(standalone.get("status")); return map( + "releaseDependencyPolicy", "published-only", "standalonePublished", standalone, "localComposite", local, - "artifactsBehaviorallyEquivalent", - artifactsEquivalent, - "allRequiredModesPassed", allRequired, + "localCompositeRequired", false, + "allRequiredPublishedEvidencePassed", allRequired, "standaloneBlocker", "passed".equals(standalone.get("status")) ? Collections.emptyList() @@ -1793,6 +1865,31 @@ && artifactHashes(standalone).equals( publishedApiInspection)); } + static Map publicReleaseMatrixNotSelected(Path root) { + Map standalone = map( + "mode", "standalone-published", + "status", "not-applicable-to-staged-candidate", + "evidenceRead", false, + "evidencePath", root.resolve("modes") + .resolve("standalone-published") + .resolve("mode.properties").toString()); + Map local = map( + "mode", "local-composite", + "status", "not-applicable-to-published-only-release", + "evidenceRead", false, + "effectiveCoordinate", "not-applicable"); + return map( + "releaseDependencyPolicy", "published-only", + "standalonePublished", standalone, + "localComposite", local, + "localCompositeRequired", false, + "allRequiredPublishedEvidencePassed", false, + "standaloneBlocker", Collections.emptyList(), + "reason", + "public release evidence is retained but not compared to " + + "an isolated staged candidate"); + } + private static Map validateModeEvidence( Path root, String expectedMode, @@ -2153,42 +2250,40 @@ static boolean publishedSourceIdentityMatches( && tagsAtHead.contains(publishedTag); } - private static boolean localModeMatchesPublishedIdentity( + private static boolean standaloneModeAuthenticatesPublishedIdentity( Map buildModes) { - Map local = - castMap(buildModes.get("localComposite")); - Map compositeSource = - castMap(local.get("compositeSource")); - return "passed".equals(local.get("status")) - && Boolean.TRUE.equals( - compositeSource.get( - "matchesPublishedIdentity")); + Map standalone = + castMap(buildModes.get("standalonePublished")); + return "passed".equals(standalone.get("status")); } static boolean bindLanguageReleaseIdentityToModes( Map identity, Map buildModes) { - boolean localMatches = - localModeMatchesPublishedIdentity(buildModes); + boolean standaloneAuthenticated = + standaloneModeAuthenticatesPublishedIdentity(buildModes); boolean currentDependencyExact = Boolean.TRUE.equals( identity.get( "currentDependencyExactFinalArtifactProven")); - boolean exact = currentDependencyExact && localMatches; + boolean exact = currentDependencyExact && standaloneAuthenticated; identity.put( - "localCompositeMatchesPublishedCommit", - localMatches); + "releaseDependencyPolicy", + "published-only"); identity.put( - "localCompositeMatchesPublishedIdentity", - localMatches); + "validatedStandalonePublishedMode", + standaloneAuthenticated); identity.put( - "validatedLocalCompositeModeMatchesPublishedIdentity", - localMatches); + "validatedStandalonePublishedModeAuthenticatesArtifact", + standaloneAuthenticated); identity.put( - "validatedLocalCompositeModeFailure", - localMatches + "validatedStandalonePublishedModeFailure", + standaloneAuthenticated ? null - : "validated-local-composite-mode-not-bound-to-published-language-identity"); + : "validated-standalone-published-mode-not-authenticated"); + identity.put( + "localCompositeStatus", + "not-applicable-to-published-only-release"); Set failures = new LinkedHashSet(); Object existingFailures = identity.get("failures"); if (existingFailures instanceof Collection) { @@ -2199,9 +2294,9 @@ static boolean bindLanguageReleaseIdentityToModes( } } } - if (!localMatches) { + if (!standaloneAuthenticated) { failures.add( - "validated-local-composite-mode-not-bound-to-published-language-identity"); + "validated-standalone-published-mode-not-authenticated"); } identity.put( "failures", @@ -2596,7 +2691,8 @@ private static List knownLimitations( "resolution", "Commit and publish the final Language kernel, " + "record its exact coordinate, commit, and " - + "artifact hash, then rerun both modes.")); + + "artifact hash, then rerun the published-only " + + "release gate.")); } if (!sourceState.uncommittedReleasePaths.isEmpty()) { limitations.add(map( @@ -2630,7 +2726,8 @@ private static List knownLimitations( "Publish the current generic runtime-work-session " + "and semantic-output-boundary APIs from " + "blue-language-java, then update the " - + "declared coordinate and rerun both modes.")); + + "declared coordinate and rerun the published-only " + + "release gate.")); } return limitations; } @@ -3114,12 +3211,12 @@ private static void appendNamedEvidence( private static String readinessReason( List currentModeFailures, - boolean bothModesPassed) { + boolean publishedEvidencePassed) { List reasons = new ArrayList(currentModeFailures); - if (!bothModesPassed) { + if (!publishedEvidencePassed) { reasons.add( - "standalone-and-local-composite-matrix-incomplete"); + "standalone-published-evidence-incomplete"); } return String.join(";", reasons); } @@ -3465,27 +3562,36 @@ private static Map releaseGateEvidence( Path independentProperties = persistentEvidenceRoot.resolve( "independent-clean-builds-" + dependencyMode + ".properties"); - Map independentCleanBuilds = - Files.isRegularFile(independentProperties) - ? independentCleanBuildEvidence( - projectDir, - buildDir, - independentProperties, - projectVersion, - sourceCommit, - dependencyMode, - declaredDependency, - dependencyResolution, - compositePath) - : independentCleanBuildJsonEvidence( - projectDir, - projectDir.resolve("build") - .resolve("reports") - .resolve("bex-release") - .resolve("inputs") - .resolve("independent-clean-builds.json"), - sourceCommit, - dependencyMode); + Map independentCleanBuilds; + if ("staged-repository".equals(dependencyMode)) { + independentCleanBuilds = map( + "status", "not-applicable-to-staged-candidate", + "evidenceRead", false, + "reason", + "independent public-release pairs are not " + + "candidate-stage evidence"); + } else if (Files.isRegularFile(independentProperties)) { + independentCleanBuilds = independentCleanBuildEvidence( + projectDir, + buildDir, + independentProperties, + projectVersion, + sourceCommit, + dependencyMode, + declaredDependency, + dependencyResolution, + compositePath); + } else { + independentCleanBuilds = independentCleanBuildJsonEvidence( + projectDir, + projectDir.resolve("build") + .resolve("reports") + .resolve("bex-release") + .resolve("inputs") + .resolve("independent-clean-builds.json"), + sourceCommit, + dependencyMode); + } return map( "deterministicArchives", deterministicArchiveEvidence( @@ -3536,9 +3642,10 @@ private static Map independentCleanBuildJsonEvidence( "evidencePath", evidencePath.toString(), "parseStatus", "invalid-json"); } - String sectionName = "local-composite".equals(dependencyMode) - ? "localComposite" : "standalonePublished"; + String sectionName = "standalonePublished"; Map pair = castMap(evidence.get(sectionName)); + Map replicaPair = castMap( + evidence.get("standalonePublishedReplica")); Map firstBuild = castMap(pair.get("firstBuild")); Map secondBuild = castMap(pair.get("secondBuild")); Map first = castMap(firstBuild.get("manifest")); @@ -3601,6 +3708,7 @@ && manifestEvidenceMatches( boolean rolesPresent = corePresent && contractsPresent && aggregatePresent && sourceReleasePresent; boolean pairValid = "passed".equals(pair.get("status")) + && "standalone-published".equals(pair.get("mode")) && Boolean.TRUE.equals(pair.get("exactManifestBytesMatch")) && Boolean.TRUE.equals(pair.get("exactArtifactBytesMatch")) && Boolean.TRUE.equals(pair.get("artifactPathSetMatch")) @@ -3613,9 +3721,53 @@ && manifestEvidenceMatches( && sourceCommit.equals(firstBuild.get("head")) && sourceCommit.equals(secondBuild.get("head")) && manifestsValid && artifactsValid && rolesPresent; - boolean passed = "blue-bex-independent-clean-builds/2.1".equals( + + Map replicaFirstBuild = castMap( + replicaPair.get("firstBuild")); + Map replicaSecondBuild = castMap( + replicaPair.get("secondBuild")); + List replicaFirstArtifacts = objectList( + replicaFirstBuild.get("artifacts")); + List replicaSecondArtifacts = objectList( + replicaSecondBuild.get("artifacts")); + long replicaArtifactCount = longValue( + replicaPair.get("artifactCount")); + boolean replicaManifestsValid = manifestEvidenceMatches( + castMap(replicaFirstBuild.get("manifest")), + canonicalHash, + canonicalBytes.length, + artifactCount) + && manifestEvidenceMatches( + castMap(replicaSecondBuild.get("manifest")), + canonicalHash, + canonicalBytes.length, + artifactCount); + boolean replicaPairValid = "passed".equals( + replicaPair.get("status")) + && "standalone-published".equals( + replicaPair.get("mode")) + && Boolean.TRUE.equals(replicaPair.get( + "exactManifestBytesMatch")) + && Boolean.TRUE.equals(replicaPair.get( + "exactArtifactBytesMatch")) + && Boolean.TRUE.equals(replicaPair.get( + "artifactPathSetMatch")) + && Boolean.TRUE.equals(replicaPair.get( + "requiredArtifactRolesPresent")) + && replicaArtifactCount == artifactCount + && recordedArtifacts.equals(replicaFirstArtifacts) + && recordedArtifacts.equals(replicaSecondArtifacts) + && Boolean.TRUE.equals(replicaFirstBuild.get("clean")) + && Boolean.TRUE.equals(replicaSecondBuild.get("clean")) + && sourceCommit.equals(replicaFirstBuild.get("head")) + && sourceCommit.equals(replicaSecondBuild.get("head")) + && replicaManifestsValid; + boolean passed = "blue-bex-independent-clean-builds/3.0".equals( evidence.get("schema")) && "passed".equals(evidence.get("status")) + && "published-only".equals( + evidence.get("dependencyPolicy")) + && "standalone-published".equals(dependencyMode) && sourceCommit.equals(evidence.get("bexCommit")) && longValue(evidence.get("checkoutCount")) == 4L && longValue(evidence.get("gitDirectoryCount")) == 4L @@ -3629,7 +3781,8 @@ && longValue(evidence.get("inputManifestCount")) == 4L "distinctGradleHomes")) && Boolean.TRUE.equals(evidence.get( "distinctInputManifestFiles")) - && pairValid; + && pairValid + && replicaPairValid; return map( "status", passed ? "passed" : "stale-or-failed", "evidencePresent", true, @@ -3638,12 +3791,17 @@ && longValue(evidence.get("inputManifestCount")) == 4L "schema", evidence.get("schema"), "commit", evidence.get("bexCommit"), "dependencyMode", dependencyMode, - "validatedSection", sectionName, + "validatedSections", java.util.Arrays.asList( + sectionName, + "standalonePublishedReplica"), "distinctInputManifestFiles", evidence.get("distinctInputManifestFiles"), "artifactCount", recordedArtifacts.size(), "manifestSha256", canonicalHash, "manifestsValid", manifestsValid, + "replicaManifestsValid", replicaManifestsValid, + "standalonePublishedPairValid", pairValid, + "standalonePublishedReplicaPairValid", replicaPairValid, "requiredArtifactRolesPresent", rolesPresent, "currentArtifactsMatch", artifactsValid, "currentArtifacts", currentArtifacts); diff --git a/src/test/java/blue/bex/conformance/BexConformanceReportTruthfulnessTest.java b/src/test/java/blue/bex/conformance/BexConformanceReportTruthfulnessTest.java index 57d5e11..d8942e9 100644 --- a/src/test/java/blue/bex/conformance/BexConformanceReportTruthfulnessTest.java +++ b/src/test/java/blue/bex/conformance/BexConformanceReportTruthfulnessTest.java @@ -267,24 +267,41 @@ void rotatingRcVersionIsCheckedSemantically( BexConformanceReportMain.versionAutomationEvidence( temporaryDirectory, "2.0.0-rc.7", - Collections.emptyMap()); - Map local = + Collections.emptyMap(), + "standalone-published"); + Map snapshot = BexConformanceReportMain.versionAutomationEvidence( temporaryDirectory, "2.0.0-rc.7-SNAPSHOT", - Collections.emptyMap()); + Collections.emptyMap(), + "standalone-published"); Map mismatched = BexConformanceReportMain.versionAutomationEvidence( temporaryDirectory, "2.0.0-rc.8", - Collections.emptyMap()); + Collections.emptyMap(), + "standalone-published"); + Map staged = + BexConformanceReportMain.versionAutomationEvidence( + temporaryDirectory, + "2.0.0-rc.8", + Collections.emptyMap(), + "staged-repository"); assertTrue(Boolean.TRUE.equals( release.get("matchesProjectVersion"))); assertTrue(Boolean.TRUE.equals( - local.get("matchesProjectVersion"))); + snapshot.get("matchesProjectVersion"))); assertFalse(Boolean.TRUE.equals( mismatched.get("matchesProjectVersion"))); + assertTrue(Boolean.TRUE.equals( + staged.get("matchesProjectVersion"))); + assertEquals( + "2.0.0-rc.7", + staged.get("historicalConfiguredVersion")); + assertEquals( + "explicit-staged-candidate", + staged.get("selectionKind")); } @Test @@ -312,11 +329,33 @@ void onlyFreshStandaloneRunCanReplaceModeEvidence() { BexConformanceReportMain.modeRunCanPersistEvidence( "standalone-published", dependency)); - assertTrue( + assertFalse( BexConformanceReportMain.modeRunCanPersistEvidence( "local-composite", dependency)); } + @Test + void stagedCandidateDoesNotReadHistoricalPublicReleaseModes( + @TempDir Path temporaryDirectory) { + Map modes = + BexConformanceReportMain.publicReleaseMatrixNotSelected( + temporaryDirectory); + + Map standalone = (Map) modes.get("standalonePublished"); + Map local = (Map) modes.get("localComposite"); + assertEquals( + "not-applicable-to-staged-candidate", + standalone.get("status")); + assertEquals(Boolean.FALSE, standalone.get("evidenceRead")); + assertEquals( + "not-applicable-to-published-only-release", + local.get("status")); + assertEquals(Boolean.FALSE, local.get("evidenceRead")); + assertEquals(Boolean.FALSE, modes.get("localCompositeRequired")); + assertFalse(Boolean.TRUE.equals( + modes.get("allRequiredPublishedEvidencePassed"))); + } + @Test void standaloneModeReceiptUsesTheStrictValidationVocabulary() { Map provenance = @@ -338,11 +377,11 @@ void standaloneModeReceiptUsesTheStrictValidationVocabulary() { BexConformanceReportMain.modeEvidenceCacheScope( "standalone-published", cache)); assertEquals( - "passed", + "not-applicable-to-published-only-release", BexConformanceReportMain.modeEvidenceProvenanceStatus( "local-composite", provenance)); assertEquals( - "all focused and aggregate Language modules", + "not-applicable-to-published-only-release", BexConformanceReportMain.modeEvidenceCacheScope( "local-composite", cache)); @@ -356,16 +395,16 @@ void standaloneModeReceiptUsesTheStrictValidationVocabulary() { } @Test - void localCompositeIdentityMustMatchPublishedCommitAndVersionTag() { + void publishedSourceIdentityRequiresExactRc21CommitAndVersionTag() { String coordinate = - "blue.language:blue-language-java:3.1.0-rc.20"; + "blue.language:blue-language-java:3.1.0-rc.21"; String commit = "0123456789abcdef0123456789abcdef01234567"; Map inspection = new LinkedHashMap(); inspection.put("coordinate", coordinate); inspection.put("source.commit", commit); - inspection.put("source.tag", "v3.1.0-rc.20"); + inspection.put("source.tag", "v3.1.0-rc.21"); assertTrue( BexConformanceReportMain @@ -374,7 +413,7 @@ void localCompositeIdentityMustMatchPublishedCommitAndVersionTag() { inspection, commit, Collections.singleton( - "v3.1.0-rc.20"))); + "v3.1.0-rc.21"))); assertFalse( BexConformanceReportMain .publishedSourceIdentityMatches( @@ -382,7 +421,7 @@ void localCompositeIdentityMustMatchPublishedCommitAndVersionTag() { inspection, "1123456789abcdef0123456789abcdef01234567", Collections.singleton( - "v3.1.0-rc.20"))); + "v3.1.0-rc.21"))); assertFalse( BexConformanceReportMain .publishedSourceIdentityMatches( @@ -390,9 +429,9 @@ void localCompositeIdentityMustMatchPublishedCommitAndVersionTag() { inspection, commit, Collections.singleton( - "v3.1.0-rc.19"))); + "v3.1.0-rc.21-invalid"))); - inspection.put("source.tag", "release-3.1.0-rc.20"); + inspection.put("source.tag", "release-3.1.0-rc.21"); assertFalse( BexConformanceReportMain .publishedSourceIdentityMatches( @@ -400,39 +439,28 @@ void localCompositeIdentityMustMatchPublishedCommitAndVersionTag() { inspection, commit, Collections.singleton( - "release-3.1.0-rc.20"))); + "release-3.1.0-rc.21"))); } @Test - void finalIdentityCannotClaimAnUnvalidatedLocalMode() { + void finalIdentityRequiresValidatedStandalonePublishedMode() { Map identity = new LinkedHashMap(); identity.put( "currentDependencyExactFinalArtifactProven", Boolean.TRUE); identity.put("exactFinalArtifactProven", Boolean.TRUE); - identity.put( - "localCompositeMatchesPublishedCommit", - Boolean.TRUE); - identity.put( - "localCompositeMatchesPublishedIdentity", - Boolean.TRUE); identity.put( "failures", Collections.emptyList()); - Map local = + Map standalone = new LinkedHashMap(); - local.put("status", "stale-or-failed"); - local.put( - "compositeSource", - Collections.singletonMap( - "matchesPublishedIdentity", - Boolean.FALSE)); + standalone.put("status", "stale-or-failed"); Map modes = Collections.singletonMap( - "localComposite", - local); + "standalonePublished", + standalone); assertFalse( BexConformanceReportMain @@ -446,14 +474,38 @@ void finalIdentityCannotClaimAnUnvalidatedLocalMode() { assertEquals( Boolean.FALSE, identity.get( - "localCompositeMatchesPublishedCommit")); + "validatedStandalonePublishedMode")); assertEquals( Boolean.FALSE, identity.get( - "localCompositeMatchesPublishedIdentity")); + "validatedStandalonePublishedModeAuthenticatesArtifact")); + assertEquals( + "not-applicable-to-published-only-release", + identity.get("localCompositeStatus")); assertTrue( String.valueOf(identity.get("failures")) .contains( - "validated-local-composite-mode-not-bound-to-published-language-identity")); + "validated-standalone-published-mode-not-authenticated")); + + Map authenticatedIdentity = + new LinkedHashMap(); + authenticatedIdentity.put( + "currentDependencyExactFinalArtifactProven", + Boolean.TRUE); + authenticatedIdentity.put("failures", Collections.emptyList()); + standalone.put("status", "passed"); + + assertTrue( + BexConformanceReportMain + .bindLanguageReleaseIdentityToModes( + authenticatedIdentity, + modes)); + assertEquals( + Boolean.TRUE, + authenticatedIdentity.get("exactFinalArtifactProven")); + assertEquals( + Boolean.TRUE, + authenticatedIdentity.get( + "validatedStandalonePublishedModeAuthenticatesArtifact")); } } diff --git a/src/test/java/blue/bex/value/BexValuesIdentityValidationTest.java b/src/test/java/blue/bex/value/BexValuesIdentityValidationTest.java index a7b3a82..721e634 100644 --- a/src/test/java/blue/bex/value/BexValuesIdentityValidationTest.java +++ b/src/test/java/blue/bex/value/BexValuesIdentityValidationTest.java @@ -69,6 +69,22 @@ void admittedExactAlsoRejectsMalformedRetainedIdentity() { value, "malformed", BexValues.scalar("value"))); } + @Test + void frozenWriterPreservesHostAuthenticatedAdmittedRepresentation() { + FrozenNode established = FrozenNode.fromNode(new Node() + .properties("kind", new Node().value("established"))); + BexValue admitted = BexValues.admittedExact( + established, + established.blueId(), + BexValues.fromSimple(java.util.Collections.singletonMap( + "kind", "different-semantic-cursor"))); + + FrozenNode frozen = BexFrozenWriter.toFrozen(admitted); + + assertSame(established, frozen); + assertSame(established.blueId(), frozen.blueId()); + } + private static FrozenNode frozen(String value) { return FrozenNode.fromResolvedNode(new Node().value(value)); } diff --git a/src/test/java/blue/language/processor/BexHostedRuntimeWorkSessionTest.java b/src/test/java/blue/language/processor/BexHostedRuntimeWorkSessionTest.java index 9699448..652b365 100644 --- a/src/test/java/blue/language/processor/BexHostedRuntimeWorkSessionTest.java +++ b/src/test/java/blue/language/processor/BexHostedRuntimeWorkSessionTest.java @@ -681,7 +681,8 @@ void hostRejectionPropagatesTheExactRecordedException() { exhausted.counter()); assertEquals(1L, exhausted.quantity()); assertEquals(2L, exhausted.weight()); - assertEquals(0L, exhausted.admittedGas()); + assertEquals(100L, exhausted.admittedGas(), + "the shared-cap receipt includes the competing live reservation"); assertEquals(100L, exhausted.effectiveBudget()); assertEquals(0, identityCalls.get()); assertEquals(0, host.submitCount); diff --git a/src/test/resources/hosted-release/published-api-inspection.properties b/src/test/resources/hosted-release/published-api-inspection.properties index 1d1686b..aefff96 100644 --- a/src/test/resources/hosted-release/published-api-inspection.properties +++ b/src/test/resources/hosted-release/published-api-inspection.properties @@ -1,11 +1,21 @@ schema=blue-bex-published-host-api-inspection/1.0 repository=https://repo1.maven.org/maven2 -metadata.lastUpdated=20260805010321 -metadata.latest=3.1.0-rc.20 -coordinate=blue.language:blue-language-java:3.1.0-rc.20 +metadata.lastUpdated=20260820080329 +metadata.latest=3.1.0-rc.21 +coordinate=blue.language:blue-language-java:3.1.0-rc.21 artifact.sha256=0de1584be094515ddd27938819464dc024a993c7eb06e4145cac129ad5bbfed0 -source.tag=v3.1.0-rc.20 -source.commit=505a654699b86b42bf0e282ddf94560a91529bcf +artifact.blue-conformance.sha256=db1a398958d02c8b80d04cba0f3997d72f14c5965c106a356d7043d8b92b9e10 +artifact.blue-contracts-core.sha256=66ce3f0ba7e76118ddc95b40a3105bbfa5ab49fd56b3c57b7c4a1ff93d72b432 +artifact.blue-language-core.sha256=8d7167254a39132e7a494561ed966748918c138c08f0967ccc3e841edba0b1f0 +artifact.blue-language-ipfs.sha256=bec7355f39a109c4fe6dfc5f9970232dc0a75cd8e5b4ab055abc311314d24c8e +artifact.blue-language-java.sha256=0de1584be094515ddd27938819464dc024a993c7eb06e4145cac129ad5bbfed0 +artifact.blue-language-mapping.sha256=d9141d5c611bde7eb6a21bce3dc4bc0df7d8167f013eeaef2a365dd0a6af329b +artifact.blue-language-model.sha256=ef55be8331147442b858474add4782489d993568effe30202a9c4a8b014d5bd8 +release.requiredArtifacts=blue-language-model,blue-language-core,blue-language-mapping,blue-contracts-core,blue-language-java +release.resolvedRuntimeArtifacts=blue-language-model,blue-language-core,blue-language-mapping,blue-contracts-core +source.tag=v3.1.0-rc.21 +source.tagObject=447b4bc440cc720a5fdd09fe50b9c9a280645a4f +source.commit=5c4e5c88fa75d6cbc52b2e8772f14f2ac5246f52 inspection=central-sha256-sidecar-jar-tf-and-javap standaloneCompile=passed class.blue.language.api.BlueOperationLimits=true diff --git a/src/test/resources/hosted-release/required-public-api.txt b/src/test/resources/hosted-release/required-public-api.txt index a456a76..611b068 100644 --- a/src/test/resources/hosted-release/required-public-api.txt +++ b/src/test/resources/hosted-release/required-public-api.txt @@ -296,6 +296,8 @@ class public final blue.bex.contracts.BexContractsFailureBoundary implements blu field public static final INSTANCE:blue.bex.contracts.BexContractsFailureBoundary method public classify(java.lang.Throwable):blue.bex.api.BexFailureBoundary$Classification method public translate(java.lang.RuntimeException):java.lang.RuntimeException +class public final blue.bex.contracts.ProcessorExactBlueValueCapability implements blue.bex.output.BexExactValueCapability + method public exactValue():blue.language.processor.ExactBlueValue class public final blue.bex.contracts.ProcessorExecutionContextBexDocumentView implements blue.bex.api.BexDocumentView constructor public (blue.language.processor.ProcessorExecutionContext) method public canonicalAt(java.lang.String):blue.bex.value.BexValue @@ -319,6 +321,7 @@ class public final blue.bex.contracts.ProcessorExecutionContextBexGasLedgerHost method public submit(blue.bex.gas.BexGasLedgerCapability):void class public final blue.bex.contracts.ProcessorExecutionContextBexSemanticIdentityBoundary implements blue.bex.output.BexSemanticIdentityBoundary constructor public (blue.language.processor.ProcessorExecutionContext) + method public carryExactIdentity(java.lang.String,blue.language.snapshot.FrozenNode):blue.bex.output.BexEstablishedIdentity method public establishIdentity(blue.language.model.Node):blue.bex.output.BexEstablishedIdentity class public final blue.bex.gas.BexGasCharge constructor public (long,blue.bex.gas.BexGasCounter,long,long,java.lang.String,java.lang.String,java.lang.String) @@ -566,6 +569,7 @@ class public abstract interface blue.bex.gas.BexSharedGasBudget method public abstract maximumGas():long method public abstract remainingGas():long class public final blue.bex.output.BexAdmittedValue + method public exactCapability():blue.bex.output.BexExactValueCapability method public node():blue.language.model.Node method public nodeBlueId():java.lang.String method public reconstructed():boolean @@ -573,8 +577,11 @@ class public final blue.bex.output.BexAdmittedValue method public value():blue.bex.value.BexValue class public final blue.bex.output.BexEstablishedIdentity constructor public (java.lang.String,blue.language.snapshot.FrozenNode) + constructor public (java.lang.String,blue.language.snapshot.FrozenNode,blue.bex.output.BexExactValueCapability) method public blueId():java.lang.String + method public exactCapability():blue.bex.output.BexExactValueCapability method public frozenValue():blue.language.snapshot.FrozenNode +class public abstract interface blue.bex.output.BexExactValueCapability class public abstract interface blue.bex.output.BexFailurePolicy field public static final STANDALONE:blue.bex.output.BexFailurePolicy method public abstract evidenceUnavailable(java.lang.Throwable):boolean @@ -597,6 +604,7 @@ class public final blue.bex.output.BexOutputKind extends java.lang.Enum class public abstract interface blue.bex.output.BexSemanticIdentityBoundary field public static final STANDALONE:blue.bex.output.BexSemanticIdentityBoundary method public abstract establishIdentity(blue.language.model.Node):blue.bex.output.BexEstablishedIdentity + method public carryExactIdentity(java.lang.String,blue.language.snapshot.FrozenNode):blue.bex.output.BexEstablishedIdentity class public final blue.bex.pointer.BexPointer method public descendant(java.util.List):blue.bex.pointer.BexPointer method public equals(java.lang.Object):boolean