From bc7d7eaf3072b07656da5996ab1c0b19416c5ca3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 9 Jul 2026 17:05:24 +0100 Subject: [PATCH] Assert independent nested plugin composition --- docs/eve-migration-roadmap.md | 7 +++ docs/parity-testing-harness.md | 5 ++ ...lugin-owner-conformance-consumer-smoke.ps1 | 2 + tools/conformance/consume-export.mjs | 59 +++++++++++++++++++ 4 files changed, 73 insertions(+) diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index 54e5614..1130bbb 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -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 diff --git a/docs/parity-testing-harness.md b/docs/parity-testing-harness.md index 013d59a..e0bccce 100644 --- a/docs/parity-testing-harness.md +++ b/docs/parity-testing-harness.md @@ -216,6 +216,11 @@ Consumer smokes can assert them with `--expect-provider-plugin-requirement `. 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 `; +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 diff --git a/scripts/run-plugin-owner-conformance-consumer-smoke.ps1 b/scripts/run-plugin-owner-conformance-consumer-smoke.ps1 index 2d97887..59339c3 100644 --- a/scripts/run-plugin-owner-conformance-consumer-smoke.ps1 +++ b/scripts/run-plugin-owner-conformance-consumer-smoke.ps1 @@ -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 ` diff --git a/tools/conformance/consume-export.mjs b/tools/conformance/consume-export.mjs index 45f2bce..7b2e81c 100644 --- a/tools/conformance/consume-export.mjs +++ b/tools/conformance/consume-export.mjs @@ -47,6 +47,7 @@ if (!exportDirectory) { " --expect-provider-surface-field ", " --expect-interactive-world-surface ", " --expect-provider-plugin-requirement ", + " --expect-independent-nested-plugin ", " --expect-provider-command ", " --expect-provider-receipt-state ", " --expect-provider-handoff ", @@ -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 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)) {