Enforce optional nested plugin requirements

This commit is contained in:
Meta Vens 2026-07-09 17:42:50 +01:00
commit b03ddc1083
5 changed files with 73 additions and 0 deletions

View file

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

View file

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

View file

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

View file

@ -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"));
}

View file

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