From b03ddc108300733c4c2b29fa1308e55ecfff908c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 9 Jul 2026 17:42:50 +0100 Subject: [PATCH] Enforce optional nested plugin requirements --- docs/eve-migration-roadmap.md | 6 +++ docs/provider-advertisement-contract.md | 3 ++ ....eve.provider_advertisement.v1.schema.json | 39 +++++++++++++++++++ tools/parity/run-parity.mjs | 23 +++++++++++ web/provider-advertisements.test.mjs | 2 + 5 files changed, 73 insertions(+) diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index 8feab20..61db3a2 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -1099,6 +1099,12 @@ Recently cut: `story.*`/`style.patch`; Aetheria proves the daemon command boundary; the web lowerer resolves authored `action.command` through the shared Eve command intent path. +- Provider plugin requirements now distinguish required parent semantics from + optional nested sidecar affordances at the contract layer. A Sai surface may + require `sai.vn`, but nested `norn.graph` and `tex.math` entries must keep + `requiredCapabilities` empty and advertise their nested affordances through + `optionalCapabilities`, preserving Norn and TeX as independent sidecar + plugins. - Provider advertisement validation now runs in the parity harness. - Repixelizer and Aetheria provider advertisements are checked against `gamecult.eve.provider_advertisement.v1`. diff --git a/docs/provider-advertisement-contract.md b/docs/provider-advertisement-contract.md index f6eb095..6e699ea 100644 --- a/docs/provider-advertisement-contract.md +++ b/docs/provider-advertisement-contract.md @@ -90,6 +90,9 @@ Required top-level meanings: interaction. Optional nested entries, such as a VN scene reserving space for independently-owned Norn or TeX surfaces, degrade when unavailable; they do not make Sai or any other parent plugin own the nested plugin's semantics. + Required entries put their gated capabilities in `requiredCapabilities`. + Optional nested entries leave `requiredCapabilities` empty and put their + nested affordance claims in `optionalCapabilities`. 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. diff --git a/schemas/gamecult.eve.provider_advertisement.v1.schema.json b/schemas/gamecult.eve.provider_advertisement.v1.schema.json index 9348b80..22b09d0 100644 --- a/schemas/gamecult.eve.provider_advertisement.v1.schema.json +++ b/schemas/gamecult.eve.provider_advertisement.v1.schema.json @@ -220,6 +220,45 @@ } } }, + "allOf": [ + { + "if": { + "properties": { + "availability": { + "const": "required" + } + } + }, + "then": { + "properties": { + "requiredCapabilities": { + "minItems": 1 + } + } + } + }, + { + "if": { + "required": ["availability"], + "properties": { + "availability": { + "const": "optional-nested" + } + } + }, + "then": { + "required": ["optionalCapabilities"], + "properties": { + "requiredCapabilities": { + "maxItems": 0 + }, + "optionalCapabilities": { + "minItems": 1 + } + } + } + } + ], "additionalProperties": true } } diff --git a/tools/parity/run-parity.mjs b/tools/parity/run-parity.mjs index 1877811..1d96187 100644 --- a/tools/parity/run-parity.mjs +++ b/tools/parity/run-parity.mjs @@ -971,6 +971,7 @@ function validateProviderPluginRequirements(provider, requirements, pluginResult const errors = []; const pluginById = new Map((pluginResults || []).map(plugin => [plugin.pluginId, plugin])); for (const requirement of requirements) { + errors.push(...validateProviderPluginRequirementShape(provider, requirement)); const plugin = pluginById.get(requirement.pluginId); const optional = requirement.availability && requirement.availability !== "required"; if (!plugin) { @@ -991,6 +992,28 @@ function validateProviderPluginRequirements(provider, requirements, pluginResult return errors; } +function validateProviderPluginRequirementShape(provider, requirement) { + const label = `${provider.advertisementPath}:${requirement.surfaceId}:plugin:${requirement.pluginId || "unknown"}`; + const errors = []; + const availability = requirement.availability || "required"; + if (!requirement.pluginId) errors.push(`${label}:pluginId:missing`); + + if (availability === "required" && !requirement.requiredCapabilities.length) { + errors.push(`${label}:requiredCapabilities:missing`); + } + + if (availability === "optional-nested") { + if (requirement.requiredCapabilities.length) { + errors.push(`${label}:optional-nested:requiredCapabilities:must-be-empty`); + } + if (!requirement.optionalCapabilities.length) { + errors.push(`${label}:optional-nested:optionalCapabilities:missing`); + } + } + + return errors; +} + async function readJsonDocument(documentPath) { return JSON.parse(await readFile(path.join(repoRoot, documentPath), "utf8")); } diff --git a/web/provider-advertisements.test.mjs b/web/provider-advertisements.test.mjs index 6751e5b..22fee4d 100644 --- a/web/provider-advertisements.test.mjs +++ b/web/provider-advertisements.test.mjs @@ -52,7 +52,9 @@ test("preserves Sai required and optional nested plugin requirements from advert "story.jump", ]); assert.equal(byPlugin.get("norn.graph").availability, "optional-nested"); + assert.deepEqual(byPlugin.get("norn.graph").requiredCapabilities, []); assert.deepEqual(byPlugin.get("norn.graph").optionalCapabilities, ["embed.norn"]); assert.equal(byPlugin.get("tex.math").availability, "optional-nested"); + assert.deepEqual(byPlugin.get("tex.math").requiredCapabilities, []); assert.deepEqual(byPlugin.get("tex.math").optionalCapabilities, ["embed.tex"]); });