Export conformance capability gaps
This commit is contained in:
parent
ecf7bc73fb
commit
18de835f79
6 changed files with 193 additions and 1 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
"",
|
||||
|
|
|
|||
Loading…
Reference in a new issue