Assert independent nested plugin composition

This commit is contained in:
Meta Vens 2026-07-09 17:05:24 +01:00
commit bc7d7eaf30
4 changed files with 73 additions and 0 deletions

View file

@ -682,6 +682,13 @@ Recently cut:
agreement, checks sidecar operations against ABI fixture operations, exports
the runtime boundary, and the plugin-owner consumer smoke asserts the Sai and
Norn sidecar contracts from the conformance pack.
- The plugin-owner conformance consumer now asserts independent nested plugin
composition directly. `--expect-independent-nested-plugin` verifies that the
Sai VN provider surface requires `sai.vn` while `norn.graph` and `tex.math`
remain `optional-nested`, separately owned, sidecar-provided capabilities.
That keeps a Sai surface free to deploy with Norn or TeX when available
without turning either plugin into a Sai dependency or runtime-hosted
semantic authority.
- Conformance export now carries first-class `capabilityGaps[]` records derived
from plugin errors, provider advertisement/scenario errors, runtime missing
evidence, declared unsupported plugin projection, capture gaps, and split

View file

@ -216,6 +216,11 @@ Consumer smokes can assert them with
`--expect-provider-plugin-requirement <providerId:surfaceId:pluginId:status:pluginOwnerRepo[:availability]>`.
Plugin-owner smokes should use the optional availability field when proving that
Sai's required VN plugin is distinct from optional nested Norn or TeX surfaces.
For the parent/nested relationship itself, use
`--expect-independent-nested-plugin <providerId:surfaceId:parentPluginId:nestedPluginId:nestedOwnerRepo>`;
this checks that the parent plugin is required, the nested plugin is
`optional-nested`, the nested owner is separate, and nested capabilities remain
optional composition rather than parent-plugin dependency.
The runtime conformance pack is evidence-shaped, not fixture-shaped. Its
`packs/runtime.json` document includes `runtimeTargets` so runtime repos can

View file

@ -74,6 +74,8 @@ node $consumerScript $consumerExport `
--expect-plugin-abi-operation-coverage norn.graph:apply:contracted:Norn `
--expect-provider-plugin-requirement gamecult.home.vn:sai.visual_novel.surface:norn.graph:optional-satisfied:Norn:optional-nested `
--expect-provider-plugin-requirement gamecult.home.vn:sai.visual_novel.surface:tex.math:optional-satisfied:EvePlugins:optional-nested `
--expect-independent-nested-plugin gamecult.home.vn:sai.visual_novel.surface:sai.vn:norn.graph:Norn `
--expect-independent-nested-plugin gamecult.home.vn:sai.visual_novel.surface:sai.vn:tex.math:EvePlugins `
--expect-plugin-capability sai.vn:vn.stage `
--expect-plugin-capability sai.vn:story.choose `
--expect-plugin-capability sai.vn:story.continue `

View file

@ -47,6 +47,7 @@ if (!exportDirectory) {
" --expect-provider-surface-field <providerId:surfaceId:field.path:value>",
" --expect-interactive-world-surface <providerId:surfaceId:targetId:ownerRepo>",
" --expect-provider-plugin-requirement <providerId:surfaceId:pluginId:status:pluginOwnerRepo[:availability]>",
" --expect-independent-nested-plugin <providerId:surfaceId:parentPluginId:nestedPluginId:nestedOwnerRepo>",
" --expect-provider-command <providerId:command>",
" --expect-provider-receipt-state <providerId:state>",
" --expect-provider-handoff <providerId>",
@ -550,6 +551,45 @@ function validateIndex(index, directory, expectations, errors) {
errors.push(`providerPluginRequirementCoverage:${expectation.providerId}:${expectation.surfaceId}:${expectation.pluginId}:availability:expected ${expectation.availability} got ${requirement.availability || ""}`);
}
}
for (const expectation of expectations.independentNestedPlugins) {
const parentRequirement = providerPluginRequirementCoverage.find(candidate =>
candidate.providerId === expectation.providerId &&
candidate.surfaceId === expectation.surfaceId &&
candidate.pluginId === expectation.parentPluginId);
const nestedRequirement = providerPluginRequirementCoverage.find(candidate =>
candidate.providerId === expectation.providerId &&
candidate.surfaceId === expectation.surfaceId &&
candidate.pluginId === expectation.nestedPluginId);
if (!parentRequirement) {
errors.push(`independentNestedPlugin:${expectation.providerId}:${expectation.surfaceId}:${expectation.parentPluginId}:parent:missing`);
continue;
}
if (!nestedRequirement) {
errors.push(`independentNestedPlugin:${expectation.providerId}:${expectation.surfaceId}:${expectation.nestedPluginId}:nested:missing`);
continue;
}
if (parentRequirement.availability !== "required") {
errors.push(`independentNestedPlugin:${expectation.providerId}:${expectation.surfaceId}:${expectation.parentPluginId}:parent.availability:expected required got ${parentRequirement.availability || ""}`);
}
if (nestedRequirement.availability !== "optional-nested") {
errors.push(`independentNestedPlugin:${expectation.providerId}:${expectation.surfaceId}:${expectation.nestedPluginId}:nested.availability:expected optional-nested got ${nestedRequirement.availability || ""}`);
}
if (!["optional-satisfied", "optional-degraded"].includes(nestedRequirement.status)) {
errors.push(`independentNestedPlugin:${expectation.providerId}:${expectation.surfaceId}:${expectation.nestedPluginId}:nested.status:expected optional-satisfied or optional-degraded got ${nestedRequirement.status || ""}`);
}
if (nestedRequirement.pluginOwnerRepo !== expectation.nestedOwnerRepo) {
errors.push(`independentNestedPlugin:${expectation.providerId}:${expectation.surfaceId}:${expectation.nestedPluginId}:nested.pluginOwnerRepo:expected ${expectation.nestedOwnerRepo} got ${nestedRequirement.pluginOwnerRepo || ""}`);
}
if (nestedRequirement.pluginOwnerRepo === parentRequirement.pluginOwnerRepo) {
errors.push(`independentNestedPlugin:${expectation.providerId}:${expectation.surfaceId}:${expectation.nestedPluginId}:owner:expected owner distinct from parent ${parentRequirement.pluginOwnerRepo || ""}`);
}
if (Array.isArray(nestedRequirement.requiredCapabilities) && nestedRequirement.requiredCapabilities.length) {
errors.push(`independentNestedPlugin:${expectation.providerId}:${expectation.surfaceId}:${expectation.nestedPluginId}:requiredCapabilities:expected empty got ${nestedRequirement.requiredCapabilities.join(",")}`);
}
if (!Array.isArray(nestedRequirement.optionalCapabilities) || !nestedRequirement.optionalCapabilities.length) {
errors.push(`independentNestedPlugin:${expectation.providerId}:${expectation.surfaceId}:${expectation.nestedPluginId}:optionalCapabilities:missing`);
}
}
for (const expectation of expectations.providerCommands) {
const provider = providers.find(candidate => candidate.providerId === expectation.providerId);
if (!provider) {
@ -1270,6 +1310,7 @@ function parseArguments(args) {
providerSurfaceFields: [],
interactiveWorldSurfaces: [],
providerPluginRequirements: [],
independentNestedPlugins: [],
providerCommands: [],
providerReceiptStates: [],
providerHandoffs: [],
@ -1332,6 +1373,7 @@ function parseArguments(args) {
["--expect-provider-surface-field", expectations.providerSurfaceFields],
["--expect-interactive-world-surface", expectations.interactiveWorldSurfaces],
["--expect-provider-plugin-requirement", expectations.providerPluginRequirements],
["--expect-independent-nested-plugin", expectations.independentNestedPlugins],
["--expect-provider-command", expectations.providerCommands],
["--expect-provider-receipt-state", expectations.providerReceiptStates],
["--expect-provider-handoff", expectations.providerHandoffs],
@ -1416,6 +1458,8 @@ function parseArguments(args) {
target.push(parseInteractiveWorldSurfaceExpectation(value));
} else if (option === "--expect-provider-plugin-requirement") {
target.push(parseProviderPluginRequirementExpectation(value));
} else if (option === "--expect-independent-nested-plugin") {
target.push(parseIndependentNestedPluginExpectation(value));
} else if (option === "--expect-provider-command") {
target.push(parseProviderExpectation(value, "command"));
} else if (option === "--expect-provider-receipt-state") {
@ -1515,6 +1559,21 @@ function parseRuntimeCaptureProbeExpectation(value) {
};
}
function parseIndependentNestedPluginExpectation(value) {
const parts = value.split(":");
if (parts.length !== 5 || parts.some(part => !part)) {
console.error(`Expected independent nested plugin in <providerId:surfaceId:parentPluginId:nestedPluginId:nestedOwnerRepo> form, got: ${value}`);
process.exit(2);
}
return {
providerId: parts[0],
surfaceId: parts[1],
parentPluginId: parts[2],
nestedPluginId: parts[3],
nestedOwnerRepo: parts[4],
};
}
function parseLocalProviderCatalogExpectation(value) {
const parts = value.split(":");
if (parts.length !== 3 || parts.some(part => !part)) {