From c0785ca9aadf1f26cda763280537980bc5bd754b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 9 Jul 2026 02:57:14 +0100 Subject: [PATCH] Export typed split target blockers --- docs/eve-migration-roadmap.md | 5 ++ docs/parity-testing-harness.md | 7 +- ...cult.eve.conformance_export.v1.schema.json | 75 +++++++++++++++- ...plit-target-conformance-consumer-smoke.ps1 | 6 ++ tools/conformance/consume-export.mjs | 43 +++++++++- tools/parity/run-parity.mjs | 85 +++++++++++++++++-- 6 files changed, 208 insertions(+), 13 deletions(-) diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index fc8837e..4e1f3bd 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -313,6 +313,11 @@ Recently cut: Electron plugin gaps as typed records, so missing Sai, Norn, or TeX projection support is owned by the runtime split target and does not imply those plugin semantics should move into Eve core or Aetheria product code. +- Split target blockers are now exported as `splitTargetBlockers[]` and as + per-target `blockerRecords[]` with blocker kind, subject, owner, and source + text. The split-target consumer smoke asserts Unity scene status/plugin + blockers, Electron feature blockers, and TUI command-transport blockers + without scraping the human `blockers[]` strings. - EveUnity now has a pending Unity scene handoff at `runtimes/incubating/eve-unity-scene/eveunity-scene-split-handoff.json`. The active `unity-uitoolkit` runtime remains a semantic UI surface lowerer; diff --git a/docs/parity-testing-harness.md b/docs/parity-testing-harness.md index 50bd59b..88c584c 100644 --- a/docs/parity-testing-harness.md +++ b/docs/parity-testing-harness.md @@ -144,8 +144,11 @@ plus release contract fields such as package name, artifact kind, tag pattern, and package root, plus Unity EditMode test runner contract fields such as runner script, package name, test assembly, and platform. It also asserts that web and Unity UI Toolkit claim their provider-advertised world-surface lowering -targets. Split blockers stay machine-readable instead of hiding in handoff -prose. +targets. Split blockers are exported both as human-readable `blockers[]` and as +typed `blockerRecords[]` / root `splitTargetBlockers[]` entries. Split-target +consumers can assert blocker kind and subject with +`--expect-split-target-blocker-record ` instead of +scraping prose from handoff notes. After generating the parity report, `run-parity-harness.ps1` copies `artifacts/conformance/latest` into diff --git a/schemas/gamecult.eve.conformance_export.v1.schema.json b/schemas/gamecult.eve.conformance_export.v1.schema.json index 5ea91ff..9ef2d17 100644 --- a/schemas/gamecult.eve.conformance_export.v1.schema.json +++ b/schemas/gamecult.eve.conformance_export.v1.schema.json @@ -11,6 +11,7 @@ "plugins", "providers", "runtimes", + "splitTargetBlockers", "splitTargets" ], "properties": { @@ -147,6 +148,74 @@ "additionalProperties": true } }, + "splitTargetBlockers": { + "type": "array", + "items": { + "type": "object", + "required": [ + "targetId", + "ownerRepo", + "kind", + "severity", + "subject", + "text" + ], + "properties": { + "targetId": { + "type": "string", + "minLength": 1 + }, + "ownerRepo": { + "type": "string", + "minLength": 1 + }, + "kind": { + "type": "string", + "minLength": 1 + }, + "severity": { + "type": "string", + "minLength": 1 + }, + "subject": { + "type": "string", + "minLength": 1 + }, + "runtimeId": { + "type": "string" + }, + "pluginId": { + "type": "string" + }, + "feature": { + "type": "string" + }, + "capability": { + "type": "string" + }, + "proof": { + "type": "string" + }, + "evidencePath": { + "type": "string" + }, + "status": { + "type": "string" + }, + "requiredStatuses": { + "type": "array" + }, + "reason": { + "type": "string" + }, + "text": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": true + } + }, "worldSurfaceLoweringGaps": { "type": "array", "items": { @@ -445,7 +514,8 @@ "requiredPlugins", "proofs", "pendingProofs", - "blockers" + "blockers", + "blockerRecords" ], "properties": { "id": { @@ -489,6 +559,9 @@ }, "blockers": { "type": "array" + }, + "blockerRecords": { + "type": "array" } }, "additionalProperties": true diff --git a/scripts/run-split-target-conformance-consumer-smoke.ps1 b/scripts/run-split-target-conformance-consumer-smoke.ps1 index 68a954b..06e969e 100644 --- a/scripts/run-split-target-conformance-consumer-smoke.ps1 +++ b/scripts/run-split-target-conformance-consumer-smoke.ps1 @@ -42,6 +42,9 @@ try { --expect-split-target-proof "EveUnity:Unity lifecycle evidence is declared" ` --expect-split-target-proof "EveUnity:EveUnity split handoff is machine-readable" ` --expect-split-target-blocker "EveUnity:runtime:unity-scene:status:pending" ` + --expect-split-target-blocker-record EveUnity:runtime-status:unity-scene ` + --expect-split-target-blocker-record EveUnity:runtime-plugin-projection:unity-scene:sai.vn ` + --expect-split-target-blocker-record "EveUnity:pending-proof:Unity scene runtime body exists outside Aetheria product code." ` --expect-split-target-blocker "EveUnity:Tagged UPM release" ` --expect-split-target-blocker "EveUnity:Unity batchmode EditMode runner" ` --expect-split-target-blocker "EveUnity:Unity batchmode or editor capture artifact" ` @@ -50,11 +53,14 @@ try { --expect-split-target-status EveElectron:incubating ` --expect-split-target-proof "EveElectron:EveElectron split handoff is machine-readable" ` --expect-split-target-blocker "EveElectron:runtime:electron-shell:status:pending" ` + --expect-split-target-blocker-record EveElectron:runtime-status:electron-shell ` + --expect-split-target-blocker-record EveElectron:runtime-feature:electron-shell:providerAdvertisements ` --expect-split-target-blocker "EveElectron:Electron shell runtime body exists outside Aetheria product code" ` --expect-split-target EveTui ` --expect-split-target-status EveTui:incubating ` --expect-split-target-proof "EveTui:EveTui split handoff is machine-readable" ` --expect-split-target-blocker "EveTui:runtime:tui:status:pending" ` + --expect-split-target-blocker-record EveTui:runtime-feature:tui:commandTransport ` --expect-split-target-blocker "EveTui:TUI runtime body exists outside provider product code" if ($LASTEXITCODE -ne 0) { throw "Split target conformance consumer smoke failed with exit code $LASTEXITCODE" diff --git a/tools/conformance/consume-export.mjs b/tools/conformance/consume-export.mjs index f217174..b5fc93c 100644 --- a/tools/conformance/consume-export.mjs +++ b/tools/conformance/consume-export.mjs @@ -47,6 +47,7 @@ if (!exportDirectory) { " --expect-runtime-lifecycle-field ", " --expect-split-target-status ", " --expect-split-target-blocker ", + " --expect-split-target-blocker-record ", " --expect-split-target-proof ", ].join("\n")); process.exit(2); @@ -124,6 +125,7 @@ function validateIndex(index, directory, expectations, errors) { const splitTargets = Array.isArray(index.splitTargets) ? index.splitTargets : []; const capabilityGaps = Array.isArray(index.capabilityGaps) ? index.capabilityGaps : []; const runtimePluginProjectionGaps = Array.isArray(index.runtimePluginProjectionGaps) ? index.runtimePluginProjectionGaps : []; + const splitTargetBlockers = Array.isArray(index.splitTargetBlockers) ? index.splitTargetBlockers : []; const worldSurfaceLoweringGaps = Array.isArray(index.worldSurfaceLoweringGaps) ? index.worldSurfaceLoweringGaps : []; validatePluginRecords(plugins, errors); @@ -142,6 +144,7 @@ function validateIndex(index, directory, expectations, errors) { } validateCapabilityGaps(index.capabilityGaps, errors); validateRuntimePluginProjectionGaps(index.runtimePluginProjectionGaps, errors); + validateSplitTargetBlockers(index.splitTargetBlockers, errors); validateWorldSurfaceLoweringGaps(index.worldSurfaceLoweringGaps, errors); for (const expectedGap of expectations.capabilityGaps) { if (!capabilityGaps.some(gap => capabilityGapText(gap).includes(expectedGap))) { @@ -495,6 +498,15 @@ function validateIndex(index, directory, expectations, errors) { errors.push(`splitTargets:${expectation.targetId}:blocker:${expectation.blocker}:missing`); } } + for (const expectation of expectations.splitTargetBlockerRecords) { + const blocker = splitTargetBlockers.find(candidate => + candidate.targetId === expectation.targetId && + candidate.kind === expectation.kind && + candidate.subject === expectation.subject); + if (!blocker) { + errors.push(`splitTargetBlockers:${expectation.targetId}:${expectation.kind}:${expectation.subject}:missing`); + } + } for (const expectation of expectations.splitTargetProofs) { const target = splitTargets.find(candidate => candidate.id === expectation.targetId); if (!target) { @@ -610,6 +622,18 @@ function validateRuntimePluginProjectionGaps(gaps, errors) { } } +function validateSplitTargetBlockers(blockers, errors) { + if (!Array.isArray(blockers)) { + errors.push("splitTargetBlockers:missing"); + return; + } + for (const [index, blocker] of blockers.entries()) { + for (const field of ["targetId", "ownerRepo", "kind", "severity", "subject", "text"]) { + if (!blocker?.[field]) errors.push(`splitTargetBlockers:${index}:${field}:missing`); + } + } +} + function validateWorldSurfaceLoweringGaps(gaps, errors) { if (!Array.isArray(gaps)) { errors.push("worldSurfaceLoweringGaps:missing"); @@ -676,7 +700,7 @@ function validateSplitTargetRecords(splitTargets, errors) { for (const field of ["id", "ownerRepo", "status", "runtimeStatuses"]) { if (!target?.[field]) errors.push(`${label}:${field}:missing`); } - for (const field of ["runtimes", "requiredRuntimeStatuses", "requiredFeatures", "requiredPlugins", "proofs", "pendingProofs", "blockers"]) { + for (const field of ["runtimes", "requiredRuntimeStatuses", "requiredFeatures", "requiredPlugins", "proofs", "pendingProofs", "blockers", "blockerRecords"]) { if (!Array.isArray(target?.[field])) errors.push(`${label}:${field}:expected array`); } } @@ -729,6 +753,7 @@ function parseArguments(args) { splitTargets: [], splitTargetStatuses: [], splitTargetBlockers: [], + splitTargetBlockerRecords: [], splitTargetProofs: [], capabilityMatrix: false, capabilityGaps: [], @@ -770,6 +795,7 @@ function parseArguments(args) { ["--expect-runtime-lifecycle-field", expectations.runtimeLifecycleFields], ["--expect-split-target-status", expectations.splitTargetStatuses], ["--expect-split-target-blocker", expectations.splitTargetBlockers], + ["--expect-split-target-blocker-record", expectations.splitTargetBlockerRecords], ["--expect-split-target-proof", expectations.splitTargetProofs], ["--expect-capability-gap", expectations.capabilityGaps], ["--expect-runtime-plugin-gap", expectations.runtimePluginGaps], @@ -848,6 +874,8 @@ function parseArguments(args) { target.push(parseSplitTargetExpectation(value, "status")); } else if (option === "--expect-split-target-blocker") { target.push(parseSplitTargetExpectation(value, "blocker")); + } else if (option === "--expect-split-target-blocker-record") { + target.push(parseSplitTargetBlockerRecordExpectation(value)); } else if (option === "--expect-split-target-proof") { target.push(parseSplitTargetExpectation(value, "proof")); } else if (option === "--expect-world-lowering-gap") { @@ -891,6 +919,19 @@ function parseRuntimePluginGapExpectation(value) { }; } +function parseSplitTargetBlockerRecordExpectation(value) { + const parts = value.split(":"); + if (parts.length < 3 || !parts[0] || !parts[1] || !parts.slice(2).join(":")) { + console.error(`Expected split target blocker record in form, got: ${value}`); + process.exit(2); + } + return { + targetId: parts[0], + kind: parts[1], + subject: parts.slice(2).join(":"), + }; +} + function parsePluginExpectation(value, field) { const separator = value.indexOf(":"); if (separator <= 0 || separator === value.length - 1) { diff --git a/tools/parity/run-parity.mjs b/tools/parity/run-parity.mjs index 4ed8fda..a4aa007 100644 --- a/tools/parity/run-parity.mjs +++ b/tools/parity/run-parity.mjs @@ -1349,23 +1349,50 @@ function evaluateSplitTargets(splitTargets, runtimeResults) { const proofs = target.proofs || []; const runtimeIds = target.runtimes || []; const blockers = []; + const blockerRecords = []; const runtimeStatuses = {}; + const addBlocker = (text, record) => { + blockers.push(text); + blockerRecords.push({ + targetId: target.id, + ownerRepo: target.ownerRepo || "", + severity: "blocker", + subject: record.subject || "", + ...record, + text, + }); + }; for (const runtimeId of runtimeIds) { const runtime = runtimeById.get(runtimeId); if (!runtime) { - blockers.push(`runtime:${runtimeId}:missing`); + addBlocker(`runtime:${runtimeId}:missing`, { + kind: "runtime-missing", + subject: runtimeId, + runtimeId, + }); continue; } runtimeStatuses[runtimeId] = runtime.status; if (!requiredRuntimeStatuses.includes(runtime.status)) { - blockers.push(`runtime:${runtimeId}:status:${runtime.status}`); + addBlocker(`runtime:${runtimeId}:status:${runtime.status}`, { + kind: "runtime-status", + subject: runtimeId, + runtimeId, + status: runtime.status, + requiredStatuses: requiredRuntimeStatuses, + }); } for (const feature of requiredFeatures) { if (!runtime.supportedFeatures.includes(feature)) { - blockers.push(`runtime:${runtimeId}:feature:${feature}`); + addBlocker(`runtime:${runtimeId}:feature:${feature}`, { + kind: "runtime-feature", + subject: `${runtimeId}:${feature}`, + runtimeId, + feature, + }); } } @@ -1373,18 +1400,35 @@ function evaluateSplitTargets(splitTargets, runtimeResults) { const unsupportedPlugins = new Map((runtime.unsupportedPlugins || []).map(plugin => [plugin.pluginId, plugin.reason || "unsupported"])); for (const requirement of requiredPlugins) { if (unsupportedPlugins.has(requirement.pluginId)) { - blockers.push(`runtime:${runtimeId}:unsupported-plugin:${requirement.pluginId}:${unsupportedPlugins.get(requirement.pluginId)}`); + addBlocker(`runtime:${runtimeId}:unsupported-plugin:${requirement.pluginId}:${unsupportedPlugins.get(requirement.pluginId)}`, { + kind: "runtime-plugin-projection", + subject: `${runtimeId}:${requirement.pluginId}`, + runtimeId, + pluginId: requirement.pluginId, + reason: unsupportedPlugins.get(requirement.pluginId), + }); continue; } const capabilities = supportedPlugins.get(requirement.pluginId); if (!capabilities) { - blockers.push(`runtime:${runtimeId}:plugin:${requirement.pluginId}`); + addBlocker(`runtime:${runtimeId}:plugin:${requirement.pluginId}`, { + kind: "runtime-plugin-missing", + subject: `${runtimeId}:${requirement.pluginId}`, + runtimeId, + pluginId: requirement.pluginId, + }); continue; } for (const capability of requirement.capabilities || []) { if (!capabilities.has(capability)) { - blockers.push(`runtime:${runtimeId}:plugin:${requirement.pluginId}:${capability}`); + addBlocker(`runtime:${runtimeId}:plugin:${requirement.pluginId}:${capability}`, { + kind: "runtime-plugin-capability", + subject: `${runtimeId}:${requirement.pluginId}:${capability}`, + runtimeId, + pluginId: requirement.pluginId, + capability, + }); } } } @@ -1393,15 +1437,32 @@ function evaluateSplitTargets(splitTargets, runtimeResults) { for (const proof of proofs) { const missingEvidence = (proof.evidencePaths || []).filter(candidate => !existsSync(path.join(repoRoot, candidate))); if (proof.status !== "passed") { - blockers.push(`proof:${proof.description || "unnamed"}:status:${proof.status || "missing"}`); + const description = proof.description || "unnamed"; + addBlocker(`proof:${description}:status:${proof.status || "missing"}`, { + kind: "proof-status", + subject: description, + proof: description, + status: proof.status || "missing", + }); } for (const evidencePath of missingEvidence) { - blockers.push(`proof:${proof.description || "unnamed"}:evidence:${evidencePath}:missing`); + const description = proof.description || "unnamed"; + addBlocker(`proof:${description}:evidence:${evidencePath}:missing`, { + kind: "proof-evidence", + subject: `${description}:${evidencePath}`, + proof: description, + evidencePath, + status: "missing", + }); } } for (const proof of target.pendingProofs || []) { - blockers.push(`proof:${proof}`); + addBlocker(`proof:${proof}`, { + kind: "pending-proof", + subject: proof, + proof, + }); } return { @@ -1418,6 +1479,7 @@ function evaluateSplitTargets(splitTargets, runtimeResults) { proofs, pendingProofs: target.pendingProofs || [], blockers, + blockerRecords, }; }); } @@ -1609,6 +1671,7 @@ function buildConformanceExport(report) { capabilityMatrix: buildCapabilityMatrix(report), runtimePluginProjectionGaps: collectRuntimePluginProjectionGaps(report), worldSurfaceLoweringGaps: collectWorldSurfaceLoweringGaps(report), + splitTargetBlockers: collectSplitTargetBlockers(report), capabilityGaps: collectCapabilityGaps(report), plugins: (report.plugins || []).map(plugin => ({ pluginId: plugin.pluginId, @@ -1884,6 +1947,10 @@ function collectRuntimePluginProjectionGaps(report) { return gaps; } +function collectSplitTargetBlockers(report) { + return (report.splitTargets || []).flatMap(target => target.blockerRecords || []); +} + function resolveRuntimeProjectionOwnerRepo(runtime) { return runtime.lifecycle?.release?.ownerRepo || runtime.lifecycle?.test?.ownerRepo