Export conformance capability gaps

This commit is contained in:
Meta Vens 2026-07-09 02:02:27 +01:00
commit 18de835f79
6 changed files with 193 additions and 1 deletions

View file

@ -280,6 +280,13 @@ Exit criteria:
Recently cut:
- 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
blockers. The generic consumer can assert named gaps with
`--expect-capability-gap`; the default smoke now proves Direct2D capture and
EveUnity release ownership remain visible blockers instead of hiding in
Markdown prose.
- Conformance export now carries a first-class
`gamecult.eve.capability_matrix.v1` projection. The matrix summarizes pack
health, plugin ABI/capability claims, provider surfaces/commands/receipts,

View file

@ -96,6 +96,11 @@ surfaces/commands/receipts, runtime feature/capture/command coverage, split
readiness, and handoff counts. This is derived evidence for consumers, not a
new source of truth.
Capability gaps are exported as `capabilityGaps[]`. They are derived from
missing plugin/provider/runtime evidence, declared unsupported plugin
projection, capture gaps, and split blockers. Consumer smokes can assert named
gap substrings with `--expect-capability-gap`.
The conformance export includes runtime `capabilityManifestPath` and
`capabilityManifestErrors` fields so external consumers can distinguish a
generic projection runtime from a runtime that can project plugin-declared

View file

@ -66,6 +66,35 @@
},
"additionalProperties": true
},
"capabilityGaps": {
"type": "array",
"items": {
"type": "object",
"required": ["kind", "subjectId", "gap", "severity"],
"properties": {
"kind": {
"type": "string",
"minLength": 1
},
"ownerRepo": {
"type": "string"
},
"subjectId": {
"type": "string",
"minLength": 1
},
"gap": {
"type": "string",
"minLength": 1
},
"severity": {
"type": "string",
"minLength": 1
}
},
"additionalProperties": true
}
},
"plugins": {
"type": "array"
},

View file

@ -30,7 +30,11 @@ Copy-Item -LiteralPath $sourceExport -Destination $consumerExport -Recurse
Push-Location $projectRoot
try {
node .\tools\conformance\consume-export.mjs $consumerExport --expect-capability-matrix --expect-conformance-handoff
node .\tools\conformance\consume-export.mjs $consumerExport `
--expect-capability-matrix `
--expect-capability-gap runtime:Fensalir:direct2d:capture:missing `
--expect-capability-gap "split-target:EveUnity:EveUnity:proof:Tagged UPM release" `
--expect-conformance-handoff
if ($LASTEXITCODE -ne 0) {
throw "Conformance consumer smoke failed with exit code $LASTEXITCODE"
}

View file

@ -17,6 +17,7 @@ if (!exportDirectory) {
" --expect-scenario <id>",
" --expect-split-target <id>",
" --expect-capability-matrix",
" --expect-capability-gap <substring>",
" --expect-conformance-handoff",
" --expect-plugin-operation <pluginId:operation>",
" --expect-plugin-capability <pluginId:capability>",
@ -107,6 +108,7 @@ function validateIndex(index, directory, expectations, errors) {
const providers = Array.isArray(index.providers) ? index.providers : [];
const runtimes = mergeRuntimeRecords(Array.isArray(index.runtimes) ? index.runtimes : [], exportedRuntimeTargets);
const splitTargets = Array.isArray(index.splitTargets) ? index.splitTargets : [];
const capabilityGaps = Array.isArray(index.capabilityGaps) ? index.capabilityGaps : [];
if (expectations.conformanceHandoff && !index.conformanceHandoffPath) {
errors.push("conformanceHandoffPath:missing");
@ -117,6 +119,12 @@ function validateIndex(index, directory, expectations, errors) {
if (expectations.capabilityMatrix) {
validateCapabilityMatrix(index.capabilityMatrix, packs, plugins, providers, runtimes, splitTargets, errors);
}
validateCapabilityGaps(index.capabilityGaps, errors);
for (const expectedGap of expectations.capabilityGaps) {
if (!capabilityGaps.some(gap => capabilityGapText(gap).includes(expectedGap))) {
errors.push(`capabilityGaps:${expectedGap}:missing`);
}
}
for (const expectedFixture of expectations.fixtures) {
if (!fixtureIds.has(expectedFixture)) errors.push(`fixture:${expectedFixture}:missing`);
@ -368,6 +376,29 @@ function validateCapabilityMatrix(matrix, packs, plugins, providers, runtimes, s
}
}
function validateCapabilityGaps(gaps, errors) {
if (!Array.isArray(gaps)) {
errors.push("capabilityGaps:missing");
return;
}
for (const [index, gap] of gaps.entries()) {
for (const field of ["kind", "subjectId", "gap", "severity"]) {
if (!gap?.[field]) errors.push(`capabilityGaps:${index}:${field}:missing`);
}
}
}
function capabilityGapText(gap) {
return [
gap.kind,
gap.ownerRepo,
gap.subjectId,
gap.gap,
gap.severity,
gap.detail,
].filter(Boolean).join(":");
}
function parseArguments(args) {
const exportPath = args[0] ? path.resolve(args[0]) : "";
const expectations = {
@ -394,6 +425,7 @@ function parseArguments(args) {
splitTargetBlockers: [],
splitTargetProofs: [],
capabilityMatrix: false,
capabilityGaps: [],
conformanceHandoff: false,
};
const optionTargets = new Map([
@ -419,6 +451,7 @@ function parseArguments(args) {
["--expect-split-target-status", expectations.splitTargetStatuses],
["--expect-split-target-blocker", expectations.splitTargetBlockers],
["--expect-split-target-proof", expectations.splitTargetProofs],
["--expect-capability-gap", expectations.capabilityGaps],
]);
for (let index = 1; index < args.length; index += 1) {

View file

@ -1356,6 +1356,7 @@ function buildConformanceExport(report) {
conformanceHandoffExportPath: makeHandoffExportPath("conformance", "EveConformance", report.repoStrategy.conformanceHandoffPath || ""),
packs,
capabilityMatrix: buildCapabilityMatrix(report),
capabilityGaps: collectCapabilityGaps(report),
plugins: (report.plugins || []).map(plugin => ({
pluginId: plugin.pluginId,
status: plugin.status,
@ -1462,6 +1463,7 @@ function buildCapabilityMatrix(report) {
schema: "gamecult.eve.capability_matrix.v1",
generatedAt: report.generatedAt,
summary: report.summary || {},
capabilityGapCount: collectCapabilityGaps(report).length,
handoffs: {
conformance: report.repoStrategy.conformanceHandoffPath ? 1 : 0,
plugins: plugins.filter(plugin => plugin.handoffExportPath).length,
@ -1476,6 +1478,117 @@ function buildCapabilityMatrix(report) {
};
}
function collectCapabilityGaps(report) {
const gaps = [];
const addGap = ({ kind, ownerRepo, subjectId, gap, severity = "gap", detail = "" }) => {
if (!kind || !subjectId || !gap) return;
gaps.push({
kind,
ownerRepo: ownerRepo || "",
subjectId,
gap,
severity,
detail,
});
};
for (const plugin of report.plugins || []) {
for (const error of [
...(plugin.missingPaths || []).map(id => `path:${id}`),
...(plugin.missingRequiredFixtures || []).map(id => `fixture:${id}`),
...(plugin.missingIncubationFields || []).map(id => `metadata:${id}`),
...(plugin.schemaErrors || []).map(id => `schema:${id}`),
...(plugin.advertisementErrors || []).map(id => `advertisement:${id}`),
...(plugin.abiErrors || []).map(id => `abi:${id}`),
]) {
addGap({
kind: "plugin",
ownerRepo: plugin.ownerRepo,
subjectId: plugin.pluginId,
gap: error,
severity: "blocker",
});
}
}
for (const provider of report.providers || []) {
for (const error of [
...(provider.missingPaths || []).map(id => `path:${id}`),
...(provider.missingRequiredFixtures || []).map(id => `fixture:${id}`),
...(provider.missingIncubationFields || []).map(id => `metadata:${id}`),
...(provider.advertisementErrors || []).map(id => `advertisement:${id}`),
...(provider.scenarioErrors || []).map(id => `scenario:${id}`),
...(provider.missingSchemas || []).map(id => `schema:${id}`),
...(provider.missingSurfaces || []).map(id => `surface:${id}`),
...(provider.missingCommands || []).map(id => `command:${id}`),
...(provider.pluginRequirementErrors || []).map(id => `plugin:${id}`),
]) {
addGap({
kind: "provider",
ownerRepo: provider.ownerRepo,
subjectId: provider.providerId,
gap: error,
severity: "blocker",
});
}
}
for (const runtime of report.runtimes || []) {
if (runtime.capture?.status === "missing") {
addGap({
kind: "runtime",
ownerRepo: runtime.ownerRepo,
subjectId: runtime.id,
gap: "capture:missing",
severity: runtime.kind === "active" ? "blocker" : "activation-blocker",
});
}
for (const error of [
...(runtime.missingPaths || []).map(id => `path:${id}`),
...(runtime.missingExternalPaths || []).map(id => `external:${id}`),
...(runtime.missingSourceSymbols || []).map(id => `source:${id}`),
...(runtime.missingExternalSourceSymbols || []).map(id => `external-source:${id}`),
...(runtime.missingRequiredFixtures || []).map(id => `fixture:${id}`),
...(runtime.missingRequiredFeatures || []).map(id => `feature:${id}`),
...(runtime.commandTransportSmokeErrors || []).map(id => `command-smoke:${id}`),
...(runtime.capabilityManifestErrors || []).map(id => `runtime-capability:${id}`),
...(runtime.localProviderCatalogErrors || []).map(id => `local-provider-catalog:${id}`),
...(runtime.pluginCapabilityGaps || []).map(id => `plugin-capability:${id}`),
]) {
addGap({
kind: "runtime",
ownerRepo: runtime.ownerRepo,
subjectId: runtime.id,
gap: error,
severity: "blocker",
});
}
for (const note of runtime.unsupportedPluginNotes || []) {
addGap({
kind: "runtime",
ownerRepo: runtime.ownerRepo,
subjectId: runtime.id,
gap: `unsupported-plugin:${note}`,
severity: "declared-gap",
});
}
}
for (const target of report.splitTargets || []) {
for (const blocker of target.blockers || []) {
addGap({
kind: "split-target",
ownerRepo: target.ownerRepo,
subjectId: target.id,
gap: blocker,
severity: "blocker",
});
}
}
return gaps;
}
async function writeConformanceExport(conformanceExport, directory) {
await mkdir(path.join(directory, "packs"), { recursive: true });
await mkdir(path.join(directory, "handoffs"), { recursive: true });
@ -1536,6 +1649,7 @@ function renderConformanceExportMarkdown(conformanceExport) {
`Boundary rule: ${conformanceExport.boundaryRule || "not declared"}`,
`Conformance handoff: ${conformanceExport.conformanceHandoffExportPath || conformanceExport.conformanceHandoffPath || "not declared"}`,
`Capability matrix: ${conformanceExport.capabilityMatrix?.schema || "not declared"}`,
`Capability gaps: ${(conformanceExport.capabilityGaps || []).length}`,
"",
"## Packs",
"",