From 5b6002383207c6aefd077904a4a54c675ca9afe6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 9 Jul 2026 02:16:07 +0100 Subject: [PATCH] Define EveUnity release contract --- docs/eve-migration-roadmap.md | 8 ++++ docs/parity-testing-harness.md | 4 +- .../README.md | 5 +++ .../eve-runtime-capability.json | 9 ++++ ...cult.eve.runtime_capability.v1.schema.json | 43 +++++++++++++++++++ scripts/run-eveunity-lifecycle-smoke.ps1 | 31 +++++++++++++ ...ntime-owner-conformance-consumer-smoke.ps1 | 6 ++- tools/conformance/consume-export.mjs | 39 +++++++++++++++++ tools/parity/parity-manifest.json | 9 ++++ tools/parity/run-parity.mjs | 18 ++++++++ 10 files changed, 170 insertions(+), 2 deletions(-) diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index 03e6803..6a0569c 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -280,6 +280,14 @@ Exit criteria: Recently cut: +- EveUnity release lifecycle now carries a structured UPM release contract. + `packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json` + names the package root, version source, tag pattern, artifact kind, and + owner-side publish proof for `org.gamecult.eve.unity-uitoolkit`. The + lifecycle smoke validates that contract against the package manifest, parity + compares it against the ledger, and the runtime-owner consumer smoke asserts + the exported release fields without pretending the tagged EveUnity release + already exists. - Runtime-owner conformance consumption now asserts EveUnity lifecycle stage evidence directly. The generic consumer supports `--expect-runtime-lifecycle-status` and diff --git a/docs/parity-testing-harness.md b/docs/parity-testing-harness.md index 3654f1f..d54cbc4 100644 --- a/docs/parity-testing-harness.md +++ b/docs/parity-testing-harness.md @@ -117,7 +117,9 @@ consume status, supported features, plugin support, command schema, lifecycle, capture status, and missing evidence without reading Eve's local parity manifest. The runtime-owner consumer smoke asserts EveUnity lifecycle stage status and pending release/test/capture proofs from the exported runtime record, -so split blockers stay machine-readable instead of hiding in handoff prose. +plus release contract fields such as package name, artifact kind, tag pattern, +and package root, so split blockers stay machine-readable instead of hiding in +handoff prose. After generating the parity report, `run-parity-harness.ps1` copies `artifacts/conformance/latest` into diff --git a/packages/org.gamecult.eve.unity-uitoolkit/README.md b/packages/org.gamecult.eve.unity-uitoolkit/README.md index 5e7c575..ec25fc9 100644 --- a/packages/org.gamecult.eve.unity-uitoolkit/README.md +++ b/packages/org.gamecult.eve.unity-uitoolkit/README.md @@ -42,6 +42,11 @@ evidence: plus Aetheria consumer-build smoke through Unity's generated project; - capture: pending Unity editor or batchmode artifact. +The release stage also declares the UPM release contract: +`org.gamecult.eve.unity-uitoolkit` is released from this package root, reads +its version from `package.json`, and uses tag pattern +`eveunity-uitoolkit-v{version}` once the tag is cut from EveUnity. + Those lifecycle claims are validated by the parity harness. A missing evidence path is a runtime capability error, not a README footnote. diff --git a/packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json b/packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json index fa3db65..21715c4 100644 --- a/packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json +++ b/packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json @@ -52,6 +52,15 @@ "status": "incubating-upm-package", "packageName": "org.gamecult.eve.unity-uitoolkit", "packageManifest": "packages/org.gamecult.eve.unity-uitoolkit/package.json", + "releaseContract": { + "ownerRepo": "EveUnity", + "packageName": "org.gamecult.eve.unity-uitoolkit", + "packageRoot": "packages/org.gamecult.eve.unity-uitoolkit", + "versionSource": "packages/org.gamecult.eve.unity-uitoolkit/package.json", + "tagPattern": "eveunity-uitoolkit-v{version}", + "artifactKind": "upm-package", + "publishProof": "Tagged UPM release is created from the EveUnity repo and references this package root." + }, "evidencePaths": [ "packages/org.gamecult.eve.unity-uitoolkit/package.json", "packages/org.gamecult.eve.unity-uitoolkit/README.md", diff --git a/schemas/gamecult.eve.runtime_capability.v1.schema.json b/schemas/gamecult.eve.runtime_capability.v1.schema.json index 09a25b5..b1b05c4 100644 --- a/schemas/gamecult.eve.runtime_capability.v1.schema.json +++ b/schemas/gamecult.eve.runtime_capability.v1.schema.json @@ -151,6 +151,49 @@ "command": { "type": "string" }, + "releaseContract": { + "type": "object", + "required": [ + "ownerRepo", + "packageName", + "packageRoot", + "versionSource", + "tagPattern", + "artifactKind", + "publishProof" + ], + "properties": { + "ownerRepo": { + "type": "string", + "minLength": 1 + }, + "packageName": { + "type": "string", + "minLength": 1 + }, + "packageRoot": { + "type": "string", + "minLength": 1 + }, + "versionSource": { + "type": "string", + "minLength": 1 + }, + "tagPattern": { + "type": "string", + "minLength": 1 + }, + "artifactKind": { + "type": "string", + "minLength": 1 + }, + "publishProof": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": true + }, "evidencePaths": { "type": "array", "items": { diff --git a/scripts/run-eveunity-lifecycle-smoke.ps1 b/scripts/run-eveunity-lifecycle-smoke.ps1 index f54e575..b151215 100644 --- a/scripts/run-eveunity-lifecycle-smoke.ps1 +++ b/scripts/run-eveunity-lifecycle-smoke.ps1 @@ -87,6 +87,37 @@ foreach ($stage in @("release", "test", "capture")) { } } +$releaseContract = $manifest.lifecycle.release.releaseContract +if ($null -eq $releaseContract) { + throw "EveUnity release lifecycle missing releaseContract" +} +if ($releaseContract.ownerRepo -ne "EveUnity") { + throw "EveUnity release contract has unexpected owner: $($releaseContract.ownerRepo)" +} +if ($releaseContract.packageName -ne "org.gamecult.eve.unity-uitoolkit") { + throw "EveUnity release contract has unexpected package name: $($releaseContract.packageName)" +} +if ($releaseContract.artifactKind -ne "upm-package") { + throw "EveUnity release contract has unexpected artifact kind: $($releaseContract.artifactKind)" +} +if (-not ($releaseContract.tagPattern -match "\{version\}")) { + throw "EveUnity release contract tag pattern must include {version}: $($releaseContract.tagPattern)" +} +foreach ($pathProperty in @("packageRoot", "versionSource")) { + $relativePath = $releaseContract.$pathProperty + if (-not $relativePath) { + throw "EveUnity release contract missing $pathProperty" + } + $absolutePath = Join-Path $projectRoot $relativePath + if (-not (Test-Path -LiteralPath $absolutePath)) { + throw "EveUnity release contract $pathProperty does not exist: $relativePath" + } +} +$packageManifest = Get-Content -LiteralPath (Join-Path $projectRoot $releaseContract.versionSource) -Raw | ConvertFrom-Json +if ($packageManifest.name -ne $releaseContract.packageName) { + throw "EveUnity release contract package name does not match package manifest: $($releaseContract.packageName) vs $($packageManifest.name)" +} + & (Join-Path $projectRoot "scripts\run-aetheria-unity-package-smoke.ps1") if ($LASTEXITCODE -ne 0) { throw "Aetheria Unity package smoke failed with exit code $LASTEXITCODE" diff --git a/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 b/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 index a38cdc3..deaff6c 100644 --- a/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 +++ b/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 @@ -63,7 +63,11 @@ node $consumerScript $consumerExport ` --expect-runtime-lifecycle-status unity-uitoolkit:capture:pending-editor-capture ` --expect-runtime-lifecycle-pending "unity-uitoolkit:release:Tagged UPM release" ` --expect-runtime-lifecycle-pending "unity-uitoolkit:test:Unity batchmode EditMode runner" ` - --expect-runtime-lifecycle-pending "unity-uitoolkit:capture:Unity batchmode or editor capture artifact" + --expect-runtime-lifecycle-pending "unity-uitoolkit:capture:Unity batchmode or editor capture artifact" ` + --expect-runtime-lifecycle-field "unity-uitoolkit:release:releaseContract.packageName:org.gamecult.eve.unity-uitoolkit" ` + --expect-runtime-lifecycle-field "unity-uitoolkit:release:releaseContract.artifactKind:upm-package" ` + --expect-runtime-lifecycle-field "unity-uitoolkit:release:releaseContract.tagPattern:eveunity-uitoolkit-v{version}" ` + --expect-runtime-lifecycle-field "unity-uitoolkit:release:releaseContract.packageRoot:packages/org.gamecult.eve.unity-uitoolkit" if ($LASTEXITCODE -ne 0) { throw "Runtime owner conformance consumer smoke failed with exit code $LASTEXITCODE" diff --git a/tools/conformance/consume-export.mjs b/tools/conformance/consume-export.mjs index 1144e75..f61b537 100644 --- a/tools/conformance/consume-export.mjs +++ b/tools/conformance/consume-export.mjs @@ -37,6 +37,7 @@ if (!exportDirectory) { " --expect-runtime-capture-status ", " --expect-runtime-lifecycle-status ", " --expect-runtime-lifecycle-pending ", + " --expect-runtime-lifecycle-field ", " --expect-split-target-status ", " --expect-split-target-blocker ", " --expect-split-target-proof ", @@ -325,6 +326,17 @@ function validateIndex(index, directory, expectations, errors) { errors.push(`runtimes:${expectation.runtimeId}:lifecycle.${expectation.stage}.pendingProof:${expectation.pendingProof}:missing`); } } + for (const expectation of expectations.runtimeLifecycleFields) { + const runtime = runtimes.find(candidate => candidate.runtimeId === expectation.runtimeId); + if (!runtime) { + errors.push(`runtimes:${expectation.runtimeId}:missing`); + continue; + } + const actual = readNestedField(runtime.lifecycle?.[expectation.stage], expectation.fieldPath); + if (actual !== expectation.value) { + errors.push(`runtimes:${expectation.runtimeId}:lifecycle.${expectation.stage}.${expectation.fieldPath}:expected ${expectation.value} got ${actual || ""}`); + } + } for (const expectedRuntime of expectations.runtimeHandoffs) { const runtime = runtimes.find(candidate => candidate.runtimeId === expectedRuntime); if (!runtime) { @@ -500,6 +512,7 @@ function parseArguments(args) { runtimeCaptureStatuses: [], runtimeLifecycleStatuses: [], runtimeLifecyclePendingProofs: [], + runtimeLifecycleFields: [], scenarios: [], splitTargets: [], splitTargetStatuses: [], @@ -535,6 +548,7 @@ function parseArguments(args) { ["--expect-runtime-capture-status", expectations.runtimeCaptureStatuses], ["--expect-runtime-lifecycle-status", expectations.runtimeLifecycleStatuses], ["--expect-runtime-lifecycle-pending", expectations.runtimeLifecyclePendingProofs], + ["--expect-runtime-lifecycle-field", expectations.runtimeLifecycleFields], ["--expect-split-target-status", expectations.splitTargetStatuses], ["--expect-split-target-blocker", expectations.splitTargetBlockers], ["--expect-split-target-proof", expectations.splitTargetProofs], @@ -597,6 +611,8 @@ function parseArguments(args) { target.push(parseRuntimeLifecycleExpectation(value, "status")); } else if (option === "--expect-runtime-lifecycle-pending") { target.push(parseRuntimeLifecycleExpectation(value, "pendingProof")); + } else if (option === "--expect-runtime-lifecycle-field") { + target.push(parseRuntimeLifecycleFieldExpectation(value)); } else if (option === "--expect-split-target-status") { target.push(parseSplitTargetExpectation(value, "status")); } else if (option === "--expect-split-target-blocker") { @@ -661,6 +677,29 @@ function parseRuntimeExpectation(value, field) { }; } +function parseRuntimeLifecycleFieldExpectation(value) { + const parts = value.split(":"); + if (parts.length < 4 || !parts[0] || !parts[1] || !parts[2] || !parts.slice(3).join(":")) { + console.error(`Expected runtime lifecycle field in form, got: ${value}`); + process.exit(2); + } + return { + runtimeId: parts[0], + stage: parts[1], + fieldPath: parts[2], + value: parts.slice(3).join(":"), + }; +} + +function readNestedField(source, fieldPath) { + let value = source; + for (const segment of fieldPath.split(".")) { + if (!value || typeof value !== "object") return ""; + value = value[segment]; + } + return typeof value === "string" ? value : ""; +} + function parseRuntimeLifecycleExpectation(value, field) { const parts = value.split(":"); if (parts.length < 3 || !parts[0] || !parts[1] || !parts.slice(2).join(":")) { diff --git a/tools/parity/parity-manifest.json b/tools/parity/parity-manifest.json index cc02a74..0b95075 100644 --- a/tools/parity/parity-manifest.json +++ b/tools/parity/parity-manifest.json @@ -974,6 +974,15 @@ "release": { "ownerRepo": "EveUnity", "status": "incubating-upm-package", + "releaseContract": { + "ownerRepo": "EveUnity", + "packageName": "org.gamecult.eve.unity-uitoolkit", + "packageRoot": "packages/org.gamecult.eve.unity-uitoolkit", + "versionSource": "packages/org.gamecult.eve.unity-uitoolkit/package.json", + "tagPattern": "eveunity-uitoolkit-v{version}", + "artifactKind": "upm-package", + "publishProof": "Tagged UPM release is created from the EveUnity repo and references this package root." + }, "evidencePaths": [ "packages/org.gamecult.eve.unity-uitoolkit/package.json", "packages/org.gamecult.eve.unity-uitoolkit/README.md", diff --git a/tools/parity/run-parity.mjs b/tools/parity/run-parity.mjs index 9572d86..6db5c16 100644 --- a/tools/parity/run-parity.mjs +++ b/tools/parity/run-parity.mjs @@ -1138,6 +1138,7 @@ function compareRuntimeLifecycleClaims(expectedLifecycle, actualLifecycle, manif errors.push(...missingMembers(expected.evidencePaths || [], actual.evidencePaths || [], `${label}.evidencePaths`)); errors.push(...missingMembers(expected.pendingProofs || [], actual.pendingProofs || [], `${label}.pendingProofs`)); + errors.push(...compareReleaseContract(expected.releaseContract, actual.releaseContract, `${label}.releaseContract`)); for (const evidencePath of actual.evidencePaths || []) { if (!existsSync(path.join(repoRoot, evidencePath))) { @@ -1148,6 +1149,23 @@ function compareRuntimeLifecycleClaims(expectedLifecycle, actualLifecycle, manif return errors; } +function compareReleaseContract(expected, actual, label) { + const errors = []; + if (!expected) return errors; + if (!actual) return [`${label}:missing`]; + for (const key of ["ownerRepo", "packageName", "packageRoot", "versionSource", "tagPattern", "artifactKind", "publishProof"]) { + if ((expected[key] || "") !== (actual[key] || "")) { + errors.push(`${label}.${key}:expected ${expected[key] || ""} got ${actual[key] || ""}`); + } + } + for (const key of ["packageRoot", "versionSource"]) { + if (actual[key] && !existsSync(path.join(repoRoot, actual[key]))) { + errors.push(`${label}.${key}:${actual[key]}:missing`); + } + } + return errors; +} + function comparePluginCapabilityClaims(expectedPlugins, actualPlugins, label) { const errors = []; const actual = new Map(actualPlugins.map(plugin => [plugin.pluginId, new Set(plugin.capabilities || [])]));