Define EveUnity release contract

This commit is contained in:
Meta Vens 2026-07-09 02:16:07 +01:00
commit 5b60023832
10 changed files with 170 additions and 2 deletions

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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",

View file

@ -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": {

View file

@ -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"

View file

@ -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"

View file

@ -37,6 +37,7 @@ if (!exportDirectory) {
" --expect-runtime-capture-status <runtimeId:status>",
" --expect-runtime-lifecycle-status <runtimeId:stage:status>",
" --expect-runtime-lifecycle-pending <runtimeId:stage:pending-proof-substring>",
" --expect-runtime-lifecycle-field <runtimeId:stage:field.path:value>",
" --expect-split-target-status <targetId:status>",
" --expect-split-target-blocker <targetId:blocker-substring>",
" --expect-split-target-proof <targetId:proof-substring>",
@ -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 <runtimeId:stage:field.path:value> 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(":")) {

View file

@ -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",

View file

@ -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 || [])]));