From ca01596b35b1e1fa82fde205c690b3d98203c545 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 9 Jul 2026 03:22:12 +0100 Subject: [PATCH] Export runtime plugin projection coverage --- docs/eve-migration-roadmap.md | 14 +++- docs/parity-testing-harness.md | 17 +++- docs/plugin-architecture.md | 24 ++++-- docs/provider-advertisement-contract.md | 12 ++- ...cult.eve.conformance_export.v1.schema.json | 79 ++++++++++++++++++- ....eve.provider_advertisement.v1.schema.json | 4 + ...lugin-owner-conformance-consumer-smoke.ps1 | 4 +- ...ntime-owner-conformance-consumer-smoke.ps1 | 5 ++ tools/conformance/consume-export.mjs | 61 +++++++++++++- tools/parity/run-parity.mjs | 63 ++++++++++++++- .../sai-vn.provider-advertisement.json | 11 ++- 11 files changed, 265 insertions(+), 29 deletions(-) diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index dc65c34..a8333b5 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -331,10 +331,16 @@ Recently cut: owners can consume the coverage ledger without walking Eve's nested plugin records. - Provider/plugin requirement coverage is now exported as root - `providerPluginRequirementCoverage[]` records. The plugin-owner smoke asserts - the Sai VN provider surface requirements for `sai.vn`, `norn.graph`, and - `tex.math` as satisfied by their owning plugin repos, keeping provider - requirements separate from runtime projection support. + `providerPluginRequirementCoverage[]` records with availability. The + plugin-owner smoke asserts `sai.vn` as the required VN plugin and + `norn.graph` / `tex.math` as `optional-satisfied` nested plugins owned by + Norn and EvePlugins. A Sai surface can embed Norn or TeX when those plugins + are available, but Sai does not own their graph or math semantics. +- Runtime/plugin projection claims are now exported as root + `runtimePluginProjectionCoverage[]` records. Runtime-owner smokes assert + Unity UI Toolkit support for `sai.vn` and `norn.graph`, its declared TeX gap, + and the pending Unity scene/Electron unsupported Sai projection until their + generic lowerers exist. - Runtime/plugin projection gaps are now exported as `runtimePluginProjectionGaps[]`. The runtime-owner smoke asserts Unity and Electron plugin gaps as typed records, so missing Sai, Norn, or TeX projection diff --git a/docs/parity-testing-harness.md b/docs/parity-testing-harness.md index e6711e1..cd6aa7d 100644 --- a/docs/parity-testing-harness.md +++ b/docs/parity-testing-harness.md @@ -141,6 +141,13 @@ split target, plugin id, reason, severity, and fixture lists so EveUnity, EveElectron, Sai, Norn, and TeX owners can distinguish "runtime cannot project this plugin yet" from "plugin semantics belong in Eve core." Consumer smokes can assert them with `--expect-runtime-plugin-gap `. +The same claims are exported as `runtimePluginProjectionCoverage[]`, including +positive `supported` projection adapters and declared `unsupported` gaps. This +lets runtime and plugin owners consume one ledger for "Unity UI Toolkit supports +Sai/Norn projection" and "Unity scene/Electron do not have generic plugin +projection yet" instead of walking nested runtime records. Consumer smokes can +assert entries with +`--expect-runtime-plugin-projection `. The conformance export includes runtime `capabilityManifestPath` and `capabilityManifestErrors` fields so external consumers can distinguish a @@ -162,10 +169,12 @@ expect keys. Plugin-owner smokes can assert contracted operations with without walking nested plugin records. Provider-advertised plugin requirements are indexed as root `providerPluginRequirementCoverage[]` records. These join provider id, surface -id, plugin id, plugin owner, plugin status, required capabilities, and missing -capabilities so provider and plugin owners can consume requirement coverage -without reconstructing the provider/plugin join. Consumer smokes can assert -them with +id, plugin id, plugin owner, plugin status, availability, required +capabilities, optional capabilities, and missing capabilities so provider and +plugin owners can consume requirement coverage without reconstructing the +provider/plugin join. `availability: optional-nested` means the parent surface +can host that independent plugin when available; it is not a parent-plugin +dependency. Consumer smokes can assert them with `--expect-provider-plugin-requirement `. The runtime conformance pack is evidence-shaped, not fixture-shaped. Its diff --git a/docs/plugin-architecture.md b/docs/plugin-architecture.md index 34a5f32..7dabf1f 100644 --- a/docs/plugin-architecture.md +++ b/docs/plugin-architecture.md @@ -316,19 +316,23 @@ Recommended discovery path: 1. A plugin publishes a plugin advertisement through CultMesh/Odin or ships a local package manifest. -2. A provider advertisement names the plugins its surfaces require. +2. A provider advertisement names the plugins its surfaces require and the + optional nested plugins it can use when available. 3. Odin indexes providers and plugins separately. 4. A renderer asks Odin for the provider surface and the required plugin manifests. 5. The renderer checks local plugin support. 6. Unsupported required plugin capability becomes a visible capability gap. -7. Optional unsupported capability becomes a visible degraded lowering. +7. Unsupported optional nested plugin capability becomes a visible degraded + lowering, not a claim that the parent plugin owns that nested semantic. Eve's parity harness validates this boundary directly: provider-advertised `surfaces[].requiresPlugins[]` entries must name known plugin manifests, and -each `requiredCapabilities` item must be claimed by that plugin manifest. This -keeps plugin requirements in the provider advertisement instead of hiding them -in runtime lowerers or fixture-only metadata. +each `requiredCapabilities` item must be claimed by that plugin manifest. Entries +with `availability: optional-nested` are reported separately so a Sai surface can +embed Norn or TeX when those plugins are available without making them Sai +dependencies. This keeps plugin requirements in the provider advertisement +instead of hiding them in runtime lowerers or fixture-only metadata. Provider advertisement sketch: @@ -344,10 +348,20 @@ Provider advertisement sketch: { "pluginId": "sai.vn", "versionRange": "^0.1.0", + "availability": "required", "requiredCapabilities": [ "vn.stage", "story.choose" ] + }, + { + "pluginId": "norn.graph", + "versionRange": "^0.1.0", + "availability": "optional-nested", + "requiredCapabilities": [], + "optionalCapabilities": [ + "embed.norn" + ] } ] } diff --git a/docs/provider-advertisement-contract.md b/docs/provider-advertisement-contract.md index d13a2b3..f7f7a85 100644 --- a/docs/provider-advertisement-contract.md +++ b/docs/provider-advertisement-contract.md @@ -84,10 +84,14 @@ Required top-level meanings: and ownership rule. Runtimes use it to decide how to lower the surface and where command intents must return; they do not gain authority over world state because they can render it. -- `surfaces[].requiresPlugins`: sidecar plugin capabilities required to render - or interact with that surface. Runtimes compare these requirements with their - local projection support and report visible capability gaps; they must not - import provider or plugin internals to recover missing semantics. +- `surfaces[].requiresPlugins`: sidecar plugin capabilities used by the + surface. Entries may be `availability: required` or + `availability: optional-nested`. Required entries gate full surface + interaction. Optional nested entries, such as a Sai-authored scene slotting in + Norn or TeX, degrade when unavailable; they do not make the parent plugin own + the nested plugin's semantics. Runtimes compare these entries with local + projection support and report visible capability gaps; they must not import + provider or plugin internals to recover missing semantics. - `commands`: command boundaries the provider accepts. - `routes`: transport, store, or compatibility routes used to reach the semantic address. WebSocket and HTTP routes live here when they still exist. diff --git a/schemas/gamecult.eve.conformance_export.v1.schema.json b/schemas/gamecult.eve.conformance_export.v1.schema.json index 7a02095..30afc87 100644 --- a/schemas/gamecult.eve.conformance_export.v1.schema.json +++ b/schemas/gamecult.eve.conformance_export.v1.schema.json @@ -13,6 +13,7 @@ "runtimes", "pluginAbiOperationCoverage", "providerPluginRequirementCoverage", + "runtimePluginProjectionCoverage", "interactiveWorldSurfaces", "worldSurfaceLoweringCoverage", "commandBoundaryCoverage", @@ -154,6 +155,73 @@ "additionalProperties": true } }, + "runtimePluginProjectionCoverage": { + "type": "array", + "items": { + "type": "object", + "required": [ + "runtimeId", + "runtimeStatus", + "runtimeOwnerRepo", + "pluginId", + "pluginOwnerRepo", + "status", + "severity", + "capabilities", + "requiredFixtures", + "pluginFixtures" + ], + "properties": { + "runtimeId": { + "type": "string", + "minLength": 1 + }, + "runtimeStatus": { + "type": "string", + "minLength": 1 + }, + "runtimeOwnerRepo": { + "type": "string", + "minLength": 1 + }, + "splitTarget": { + "type": "string" + }, + "pluginId": { + "type": "string", + "minLength": 1 + }, + "pluginOwnerRepo": { + "type": "string", + "minLength": 1 + }, + "pluginStatus": { + "type": "string" + }, + "status": { + "type": "string", + "minLength": 1 + }, + "severity": { + "type": "string", + "minLength": 1 + }, + "capabilities": { + "type": "array" + }, + "reason": { + "type": "string" + }, + "requiredFixtures": { + "type": "array" + }, + "pluginFixtures": { + "type": "array" + } + }, + "additionalProperties": true + } + }, "splitTargetBlockers": { "type": "array", "items": { @@ -720,10 +788,12 @@ "pluginOwnerRepo", "pluginStatus", "versionRange", + "availability", "status", "requiredCapabilities", "optionalCapabilities", - "missingRequiredCapabilities" + "missingRequiredCapabilities", + "missingOptionalCapabilities" ], "properties": { "providerId": { @@ -752,6 +822,10 @@ "versionRange": { "type": "string" }, + "availability": { + "type": "string", + "minLength": 1 + }, "status": { "type": "string", "minLength": 1 @@ -764,6 +838,9 @@ }, "missingRequiredCapabilities": { "type": "array" + }, + "missingOptionalCapabilities": { + "type": "array" } }, "additionalProperties": true diff --git a/schemas/gamecult.eve.provider_advertisement.v1.schema.json b/schemas/gamecult.eve.provider_advertisement.v1.schema.json index aaedac1..9348b80 100644 --- a/schemas/gamecult.eve.provider_advertisement.v1.schema.json +++ b/schemas/gamecult.eve.provider_advertisement.v1.schema.json @@ -201,6 +201,10 @@ "versionRange": { "type": "string" }, + "availability": { + "type": "string", + "enum": ["required", "optional", "optional-nested"] + }, "requiredCapabilities": { "type": "array", "items": { diff --git a/scripts/run-plugin-owner-conformance-consumer-smoke.ps1 b/scripts/run-plugin-owner-conformance-consumer-smoke.ps1 index 08963a8..266d2dd 100644 --- a/scripts/run-plugin-owner-conformance-consumer-smoke.ps1 +++ b/scripts/run-plugin-owner-conformance-consumer-smoke.ps1 @@ -56,8 +56,8 @@ node $consumerScript $consumerExport ` --expect-plugin-abi-operation-coverage norn.graph:describe:contracted:Norn ` --expect-plugin-abi-operation-coverage norn.graph:lower:contracted:Norn ` --expect-plugin-abi-operation-coverage norn.graph:apply:contracted:Norn ` - --expect-provider-plugin-requirement gamecult.home.vn:sai.visual_novel.surface:norn.graph:satisfied:Norn ` - --expect-provider-plugin-requirement gamecult.home.vn:sai.visual_novel.surface:tex.math:satisfied:EvePlugins ` + --expect-provider-plugin-requirement gamecult.home.vn:sai.visual_novel.surface:norn.graph:optional-satisfied:Norn ` + --expect-provider-plugin-requirement gamecult.home.vn:sai.visual_novel.surface:tex.math:optional-satisfied:EvePlugins ` --expect-plugin-capability sai.vn:vn.stage ` --expect-plugin-capability sai.vn:story.choose ` --expect-plugin-capability sai.vn:story.continue ` diff --git a/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 b/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 index fa64317..878246d 100644 --- a/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 +++ b/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 @@ -54,6 +54,11 @@ node $consumerScript $consumerExport ` --expect-runtime-world-target unity-uitoolkit:unity-uitoolkit ` --expect-runtime-world-field unity-uitoolkit:unity-uitoolkit:supportLevel:ui-toolkit-semantic-command-surface ` --expect-runtime-world-field unity-uitoolkit:unity-uitoolkit:ownership:runtime-lowers-provider-world-surface-without-owning-world-state ` + --expect-runtime-plugin-projection unity-uitoolkit:sai.vn:supported:EveUnity ` + --expect-runtime-plugin-projection unity-uitoolkit:norn.graph:supported:EveUnity ` + --expect-runtime-plugin-projection unity-uitoolkit:tex.math:unsupported:EveUnity ` + --expect-runtime-plugin-projection unity-scene:sai.vn:unsupported:EveUnity ` + --expect-runtime-plugin-projection electron-shell:sai.vn:unsupported:EveElectron ` --expect-runtime-plugin-gap unity-uitoolkit:tex.math:EveUnity ` --expect-runtime-plugin-gap unity-scene:sai.vn:EveUnity ` --expect-runtime-plugin-gap unity-scene:norn.graph:EveUnity ` diff --git a/tools/conformance/consume-export.mjs b/tools/conformance/consume-export.mjs index e463a98..33fb707 100644 --- a/tools/conformance/consume-export.mjs +++ b/tools/conformance/consume-export.mjs @@ -18,6 +18,7 @@ if (!exportDirectory) { " --expect-split-target ", " --expect-capability-matrix", " --expect-capability-gap ", + " --expect-runtime-plugin-projection ", " --expect-runtime-plugin-gap ", " --expect-world-lowering-coverage ", " --expect-command-boundary-coverage ", @@ -132,6 +133,7 @@ function validateIndex(index, directory, expectations, errors) { const pluginAbiOperationCoverage = Array.isArray(index.pluginAbiOperationCoverage) ? index.pluginAbiOperationCoverage : []; const providerPluginRequirementCoverage = Array.isArray(index.providerPluginRequirementCoverage) ? index.providerPluginRequirementCoverage : []; const capabilityGaps = Array.isArray(index.capabilityGaps) ? index.capabilityGaps : []; + const runtimePluginProjectionCoverage = Array.isArray(index.runtimePluginProjectionCoverage) ? index.runtimePluginProjectionCoverage : []; const runtimePluginProjectionGaps = Array.isArray(index.runtimePluginProjectionGaps) ? index.runtimePluginProjectionGaps : []; const interactiveWorldSurfaces = Array.isArray(index.interactiveWorldSurfaces) ? index.interactiveWorldSurfaces : []; const worldSurfaceLoweringCoverage = Array.isArray(index.worldSurfaceLoweringCoverage) ? index.worldSurfaceLoweringCoverage : []; @@ -158,6 +160,7 @@ function validateIndex(index, directory, expectations, errors) { validateCapabilityMatrix(index.capabilityMatrix, packs, plugins, providers, runtimes, splitTargets, errors); } validateCapabilityGaps(index.capabilityGaps, errors); + validateRuntimePluginProjectionCoverage(index.runtimePluginProjectionCoverage, errors); validateRuntimePluginProjectionGaps(index.runtimePluginProjectionGaps, errors); validateSplitTargetBlockers(index.splitTargetBlockers, errors); validateSplitHandoffMoveCoverage(index.splitHandoffMoveCoverage, errors); @@ -169,6 +172,21 @@ function validateIndex(index, directory, expectations, errors) { errors.push(`capabilityGaps:${expectedGap}:missing`); } } + for (const expectation of expectations.runtimePluginProjectionCoverage) { + const record = runtimePluginProjectionCoverage.find(candidate => + candidate.runtimeId === expectation.runtimeId && + candidate.pluginId === expectation.pluginId); + if (!record) { + errors.push(`runtimePluginProjectionCoverage:${expectation.runtimeId}:${expectation.pluginId}:missing`); + continue; + } + if (record.status !== expectation.status) { + errors.push(`runtimePluginProjectionCoverage:${expectation.runtimeId}:${expectation.pluginId}:status:expected ${expectation.status} got ${record.status || ""}`); + } + if (record.runtimeOwnerRepo !== expectation.ownerRepo) { + errors.push(`runtimePluginProjectionCoverage:${expectation.runtimeId}:${expectation.pluginId}:runtimeOwnerRepo:expected ${expectation.ownerRepo} got ${record.runtimeOwnerRepo || ""}`); + } + } for (const expectation of expectations.runtimePluginGaps) { const gap = runtimePluginProjectionGaps.find(candidate => candidate.runtimeId === expectation.runtimeId && @@ -724,6 +742,27 @@ function validateCapabilityGaps(gaps, errors) { } } +function validateRuntimePluginProjectionCoverage(records, errors) { + if (!Array.isArray(records)) { + errors.push("runtimePluginProjectionCoverage:missing"); + return; + } + for (const [index, record] of records.entries()) { + for (const field of ["runtimeId", "runtimeStatus", "runtimeOwnerRepo", "pluginId", "pluginOwnerRepo", "status", "severity"]) { + if (!record?.[field]) errors.push(`runtimePluginProjectionCoverage:${index}:${field}:missing`); + } + for (const field of ["capabilities", "requiredFixtures", "pluginFixtures"]) { + if (!Array.isArray(record?.[field])) errors.push(`runtimePluginProjectionCoverage:${index}:${field}:expected array`); + } + if (record?.status === "supported" && !record.capabilities.length) { + errors.push(`runtimePluginProjectionCoverage:${index}:capabilities:missing`); + } + if (record?.status === "unsupported" && !record.reason) { + errors.push(`runtimePluginProjectionCoverage:${index}:reason:missing`); + } + } +} + function validateRuntimePluginProjectionGaps(gaps, errors) { if (!Array.isArray(gaps)) { errors.push("runtimePluginProjectionGaps:missing"); @@ -862,10 +901,10 @@ function validateProviderPluginRequirementCoverage(records, errors) { return; } for (const [index, record] of records.entries()) { - for (const field of ["providerId", "providerOwnerRepo", "surfaceId", "pluginId", "pluginStatus", "status"]) { + for (const field of ["providerId", "providerOwnerRepo", "surfaceId", "pluginId", "pluginStatus", "availability", "status"]) { if (!record?.[field]) errors.push(`providerPluginRequirementCoverage:${index}:${field}:missing`); } - for (const field of ["requiredCapabilities", "optionalCapabilities", "missingRequiredCapabilities"]) { + for (const field of ["requiredCapabilities", "optionalCapabilities", "missingRequiredCapabilities", "missingOptionalCapabilities"]) { if (!Array.isArray(record?.[field])) errors.push(`providerPluginRequirementCoverage:${index}:${field}:expected array`); } } @@ -965,6 +1004,7 @@ function parseArguments(args) { splitTargetProofs: [], capabilityMatrix: false, capabilityGaps: [], + runtimePluginProjectionCoverage: [], runtimePluginGaps: [], worldLoweringCoverage: [], commandBoundaryCoverage: [], @@ -1012,6 +1052,7 @@ function parseArguments(args) { ["--expect-split-handoff-move", expectations.splitHandoffMoves], ["--expect-split-target-proof", expectations.splitTargetProofs], ["--expect-capability-gap", expectations.capabilityGaps], + ["--expect-runtime-plugin-projection", expectations.runtimePluginProjectionCoverage], ["--expect-runtime-plugin-gap", expectations.runtimePluginGaps], ["--expect-world-lowering-coverage", expectations.worldLoweringCoverage], ["--expect-command-boundary-coverage", expectations.commandBoundaryCoverage], @@ -1110,6 +1151,8 @@ function parseArguments(args) { target.push(parseCommandBoundaryCoverageExpectation(value)); } else if (option === "--expect-runtime-plugin-gap") { target.push(parseRuntimePluginGapExpectation(value)); + } else if (option === "--expect-runtime-plugin-projection") { + target.push(parseRuntimePluginProjectionCoverageExpectation(value)); } else { target.push(value); } @@ -1179,6 +1222,20 @@ function parseRuntimePluginGapExpectation(value) { }; } +function parseRuntimePluginProjectionCoverageExpectation(value) { + const parts = value.split(":"); + if (parts.length !== 4 || parts.some(part => !part)) { + console.error(`Expected runtime plugin projection coverage in form, got: ${value}`); + process.exit(2); + } + return { + runtimeId: parts[0], + pluginId: parts[1], + status: parts[2], + ownerRepo: parts[3], + }; +} + function parseSplitTargetBlockerRecordExpectation(value) { const parts = value.split(":"); if (parts.length < 3 || !parts[0] || !parts[1] || !parts.slice(2).join(":")) { diff --git a/tools/parity/run-parity.mjs b/tools/parity/run-parity.mjs index 0d1cf7f..69e9adc 100644 --- a/tools/parity/run-parity.mjs +++ b/tools/parity/run-parity.mjs @@ -725,6 +725,7 @@ function collectProviderPluginRequirements(advertisement) { surfaceId: surface.surfaceId || "", pluginId: requirement.pluginId || "", versionRange: requirement.versionRange || "", + availability: requirement.availability || "required", requiredCapabilities: requirement.requiredCapabilities || [], optionalCapabilities: requirement.optionalCapabilities || [], }); @@ -775,8 +776,9 @@ function validateProviderPluginRequirements(provider, requirements, pluginResult const pluginById = new Map((pluginResults || []).map(plugin => [plugin.pluginId, plugin])); for (const requirement of requirements) { const plugin = pluginById.get(requirement.pluginId); + const optional = requirement.availability && requirement.availability !== "required"; if (!plugin) { - errors.push(`${provider.advertisementPath}:${requirement.surfaceId}:plugin:${requirement.pluginId}:missing`); + if (!optional) errors.push(`${provider.advertisementPath}:${requirement.surfaceId}:plugin:${requirement.pluginId}:missing`); continue; } if (plugin.status !== "incubating" && plugin.status !== "external-owner-planned") { @@ -1669,6 +1671,7 @@ function buildConformanceExport(report) { conformanceHandoffExportPath: makeHandoffExportPath("conformance", "EveConformance", report.repoStrategy.conformanceHandoffPath || ""), packs, capabilityMatrix: buildCapabilityMatrix(report), + runtimePluginProjectionCoverage: collectRuntimePluginProjectionCoverage(report), runtimePluginProjectionGaps: collectRuntimePluginProjectionGaps(report), interactiveWorldSurfaces: collectInteractiveWorldSurfaces(report), worldSurfaceLoweringCoverage: collectWorldSurfaceLoweringCoverage(report), @@ -1953,6 +1956,53 @@ function collectRuntimePluginProjectionGaps(report) { return gaps; } +function collectRuntimePluginProjectionCoverage(report) { + const pluginById = new Map((report.plugins || []).map(plugin => [plugin.pluginId, plugin])); + const coverage = []; + for (const runtime of report.runtimes || []) { + for (const plugin of runtime.supportedPlugins || []) { + if (!plugin.pluginId) continue; + const pluginRecord = pluginById.get(plugin.pluginId); + coverage.push({ + runtimeId: runtime.id, + runtimeStatus: runtime.status || "", + runtimeOwnerRepo: resolveRuntimeProjectionOwnerRepo(runtime), + splitTarget: runtime.splitTarget || "", + pluginId: plugin.pluginId, + pluginOwnerRepo: pluginRecord?.ownerRepo || "", + pluginStatus: pluginRecord?.status || "", + status: "supported", + severity: "ok", + capabilities: plugin.capabilities || [], + reason: "", + requiredFixtures: runtime.requiredFixtures || [], + pluginFixtures: runtime.pluginFixtures || [], + }); + } + for (const plugin of runtime.unsupportedPlugins || []) { + if (!plugin.pluginId) continue; + const pluginRecord = pluginById.get(plugin.pluginId); + coverage.push({ + runtimeId: runtime.id, + runtimeStatus: runtime.status || "", + runtimeOwnerRepo: resolveRuntimeProjectionOwnerRepo(runtime), + splitTarget: runtime.splitTarget || "", + pluginId: plugin.pluginId, + pluginOwnerRepo: pluginRecord?.ownerRepo || "", + pluginStatus: pluginRecord?.status || "", + status: "unsupported", + severity: "declared-gap", + capabilities: [], + reason: plugin.reason || "unsupported", + requiredFixtures: runtime.requiredFixtures || [], + pluginFixtures: runtime.pluginFixtures || [], + }); + } + } + return coverage.sort((left, right) => + `${left.runtimeId}:${left.pluginId}:${left.status}`.localeCompare(`${right.runtimeId}:${right.pluginId}:${right.status}`)); +} + function collectPluginAbiOperationCoverage(report) { const requiredOperations = ["describe", "validate", "project", "lower", "measure", "apply"]; const coverage = []; @@ -1992,6 +2042,9 @@ function collectProviderPluginRequirementCoverage(report) { const pluginCapabilities = new Set(plugin?.capabilities || []); const missingRequiredCapabilities = (requirement.requiredCapabilities || []) .filter(capability => !pluginCapabilities.has(capability)); + const missingOptionalCapabilities = (requirement.optionalCapabilities || []) + .filter(capability => plugin && !pluginCapabilities.has(capability)); + const optional = requirement.availability && requirement.availability !== "required"; coverage.push({ providerId: provider.providerId, providerOwnerRepo: provider.ownerRepo || "", @@ -2000,14 +2053,16 @@ function collectProviderPluginRequirementCoverage(report) { pluginOwnerRepo: plugin?.ownerRepo || "", pluginStatus: plugin?.status || "missing", versionRange: requirement.versionRange || "", + availability: requirement.availability || "required", status: !plugin - ? "missing-plugin" + ? optional ? "optional-missing-plugin" : "missing-plugin" : missingRequiredCapabilities.length - ? "missing-capability" - : "satisfied", + ? optional ? "optional-missing-capability" : "missing-capability" + : optional ? "optional-satisfied" : "satisfied", requiredCapabilities: requirement.requiredCapabilities || [], optionalCapabilities: requirement.optionalCapabilities || [], missingRequiredCapabilities, + missingOptionalCapabilities, }); } } diff --git a/web/fixtures/sai-vn.provider-advertisement.json b/web/fixtures/sai-vn.provider-advertisement.json index a731428..5e42166 100644 --- a/web/fixtures/sai-vn.provider-advertisement.json +++ b/web/fixtures/sai-vn.provider-advertisement.json @@ -44,11 +44,12 @@ "status": "available", "audience": "player", "mode": "interactive", - "summary": "Visual novel surface requiring sidecar plugin capabilities for VN stage, Norn graph embedding, and TeX embedding.", + "summary": "Visual novel surface requiring Sai VN semantics, with optional independently-owned Norn graph and TeX nested surfaces when those plugins are available.", "requiresPlugins": [ { "pluginId": "sai.vn", "versionRange": "^0.1.0", + "availability": "required", "requiredCapabilities": [ "vn.stage", "story.choose", @@ -59,14 +60,18 @@ { "pluginId": "norn.graph", "versionRange": "^0.1.0", - "requiredCapabilities": [ + "availability": "optional-nested", + "requiredCapabilities": [], + "optionalCapabilities": [ "embed.norn" ] }, { "pluginId": "tex.math", "versionRange": "^0.1.0", - "requiredCapabilities": [ + "availability": "optional-nested", + "requiredCapabilities": [], + "optionalCapabilities": [ "embed.tex" ] }