Validate Eve plugin advertisement contracts
This commit is contained in:
parent
f873b6af4d
commit
db0ef80bb7
8 changed files with 453 additions and 14 deletions
|
|
@ -182,7 +182,9 @@ Work:
|
|||
`plugins/incubating/norn-graph.plugin.json`, and
|
||||
`plugins/incubating/tex-math.plugin.json` as temporary manifests.
|
||||
- Define `gamecult.eve.plugin.v1` and
|
||||
`gamecult.eve.plugin_advertisement.v1` as explicit schemas.
|
||||
`gamecult.eve.plugin_advertisement.v1` as explicit schemas. First-cut JSON
|
||||
schemas live under `schemas/` and the parity harness validates incubating
|
||||
plugin manifests and advertisements against them.
|
||||
- Add plugin ABI fixtures for `describe`, `validate`, `project`, `apply`,
|
||||
`lower`, and `measure` where relevant.
|
||||
- Move `sai.vn` to Sai when VN/Ink story state, visual manifest, story commands,
|
||||
|
|
@ -271,15 +273,14 @@ Exit criteria:
|
|||
|
||||
## Active Work Queue
|
||||
|
||||
1. Add JSON schemas for plugin manifest and plugin advertisement.
|
||||
2. Add provider advertisement checks to the parity harness.
|
||||
3. Add command descriptor checks to fixtures and runtime source-symbol
|
||||
1. Add provider advertisement checks to the parity harness.
|
||||
2. Add command descriptor checks to fixtures and runtime source-symbol
|
||||
expectations.
|
||||
4. Add authority/staleness witness fixture to browser reference.
|
||||
5. Add conformance-pack metadata files beside fixtures.
|
||||
6. Add a Direct2D adapter spike or explicitly demote Direct2D until Fensalir owns
|
||||
3. Add authority/staleness witness fixture to browser reference.
|
||||
4. Add conformance-pack metadata files beside fixtures.
|
||||
5. Add a Direct2D adapter spike or explicitly demote Direct2D until Fensalir owns
|
||||
the body.
|
||||
7. Add split-readiness reports for EveFlutter and EveUnity.
|
||||
6. Add split-readiness reports for EveFlutter and EveUnity.
|
||||
|
||||
## Stop Conditions
|
||||
|
||||
|
|
|
|||
22
plugins/incubating/norn-graph.plugin-advertisement.json
Normal file
22
plugins/incubating/norn-graph.plugin-advertisement.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"schema": "gamecult.eve.plugin_advertisement.v1",
|
||||
"pluginId": "norn.graph",
|
||||
"ownerService": "norn",
|
||||
"version": "0.1.0-incubating",
|
||||
"manifestAddress": "file://plugins/incubating/norn-graph.plugin.json",
|
||||
"schemas": [
|
||||
"norn.graph.document.v1",
|
||||
"norn.graph.layout_request.v1",
|
||||
"norn.graph.layout_result.v1"
|
||||
],
|
||||
"componentKinds": [
|
||||
"embed.norn"
|
||||
],
|
||||
"commands": [
|
||||
"graph.node.activate",
|
||||
"graph.focus"
|
||||
],
|
||||
"fixtures": [
|
||||
"sai-vn"
|
||||
]
|
||||
}
|
||||
25
plugins/incubating/sai-vn.plugin-advertisement.json
Normal file
25
plugins/incubating/sai-vn.plugin-advertisement.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"schema": "gamecult.eve.plugin_advertisement.v1",
|
||||
"pluginId": "sai.vn",
|
||||
"ownerService": "sai",
|
||||
"version": "0.1.0-incubating",
|
||||
"manifestAddress": "file://plugins/incubating/sai-vn.plugin.json",
|
||||
"schemas": [
|
||||
"sai.vn.story_session.v1",
|
||||
"sai.vn.visual_manifest.v1"
|
||||
],
|
||||
"componentKinds": [
|
||||
"vn.stage",
|
||||
"panel.dialogue",
|
||||
"text.dialogue",
|
||||
"rail.actions"
|
||||
],
|
||||
"commands": [
|
||||
"story.continue",
|
||||
"story.choose",
|
||||
"story.jump"
|
||||
],
|
||||
"fixtures": [
|
||||
"sai-vn"
|
||||
]
|
||||
}
|
||||
19
plugins/incubating/tex-math.plugin-advertisement.json
Normal file
19
plugins/incubating/tex-math.plugin-advertisement.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"schema": "gamecult.eve.plugin_advertisement.v1",
|
||||
"pluginId": "tex.math",
|
||||
"ownerService": "eveplugins",
|
||||
"version": "0.1.0-incubating",
|
||||
"manifestAddress": "file://plugins/incubating/tex-math.plugin.json",
|
||||
"schemas": [
|
||||
"tex.math.source.v1",
|
||||
"tex.math.render_request.v1",
|
||||
"tex.math.render_result.v1"
|
||||
],
|
||||
"componentKinds": [
|
||||
"embed.tex"
|
||||
],
|
||||
"commands": [],
|
||||
"fixtures": [
|
||||
"sai-vn"
|
||||
]
|
||||
}
|
||||
177
schemas/gamecult.eve.plugin.v1.schema.json
Normal file
177
schemas/gamecult.eve.plugin.v1.schema.json
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "gamecult.eve.plugin.v1",
|
||||
"title": "Eve Plugin Manifest",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"schema",
|
||||
"pluginId",
|
||||
"title",
|
||||
"owner",
|
||||
"incubation",
|
||||
"componentKinds",
|
||||
"commands",
|
||||
"stateSchemas",
|
||||
"fixtures",
|
||||
"capabilities"
|
||||
],
|
||||
"properties": {
|
||||
"schema": {
|
||||
"const": "gamecult.eve.plugin.v1"
|
||||
},
|
||||
"pluginId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"incubation": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ownerRepo",
|
||||
"currentHostRepo",
|
||||
"splitTarget",
|
||||
"graduationTrigger"
|
||||
],
|
||||
"properties": {
|
||||
"ownerRepo": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"currentHostRepo": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"splitTarget": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"graduationTrigger": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"componentKinds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"commands": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"command",
|
||||
"schema",
|
||||
"effect"
|
||||
],
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"effect": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"stateSchemas": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"fixtures": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"fixtureId",
|
||||
"schema",
|
||||
"path"
|
||||
],
|
||||
"properties": {
|
||||
"fixtureId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"requires"
|
||||
],
|
||||
"properties": {
|
||||
"requires": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"optionalCapabilities": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"optionalPlugins": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"pluginId",
|
||||
"capabilities"
|
||||
],
|
||||
"properties": {
|
||||
"pluginId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"capabilities": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
67
schemas/gamecult.eve.plugin_advertisement.v1.schema.json
Normal file
67
schemas/gamecult.eve.plugin_advertisement.v1.schema.json
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "gamecult.eve.plugin_advertisement.v1",
|
||||
"title": "Eve Plugin Advertisement",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"schema",
|
||||
"pluginId",
|
||||
"ownerService",
|
||||
"version",
|
||||
"manifestAddress",
|
||||
"schemas",
|
||||
"componentKinds",
|
||||
"commands",
|
||||
"fixtures"
|
||||
],
|
||||
"properties": {
|
||||
"schema": {
|
||||
"const": "gamecult.eve.plugin_advertisement.v1"
|
||||
},
|
||||
"pluginId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"ownerService": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"manifestAddress": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"schemas": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"componentKinds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"commands": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"fixtures": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
|
|
@ -5,6 +5,10 @@
|
|||
"incubationPolicy": "Eve may host runtimes, plugins, and conformance packs only while their contract is still forming.",
|
||||
"graduationDocument": "docs/repo-strategy.md"
|
||||
},
|
||||
"schemas": {
|
||||
"gamecult.eve.plugin.v1": "schemas/gamecult.eve.plugin.v1.schema.json",
|
||||
"gamecult.eve.plugin_advertisement.v1": "schemas/gamecult.eve.plugin_advertisement.v1.schema.json"
|
||||
},
|
||||
"conformancePacks": [
|
||||
{
|
||||
"id": "core",
|
||||
|
|
@ -223,7 +227,14 @@
|
|||
"capabilities": ["vn.stage", "story.choose", "story.continue", "story.jump"],
|
||||
"optionalPlugins": ["norn.graph", "tex.math"],
|
||||
"requiredFixtures": ["sai-vn"],
|
||||
"expectedPaths": ["plugins/incubating/sai-vn.plugin.json"]
|
||||
"schemaPath": "schemas/gamecult.eve.plugin.v1.schema.json",
|
||||
"manifestPath": "plugins/incubating/sai-vn.plugin.json",
|
||||
"advertisementSchemaPath": "schemas/gamecult.eve.plugin_advertisement.v1.schema.json",
|
||||
"advertisementPath": "plugins/incubating/sai-vn.plugin-advertisement.json",
|
||||
"expectedPaths": [
|
||||
"plugins/incubating/sai-vn.plugin.json",
|
||||
"plugins/incubating/sai-vn.plugin-advertisement.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"pluginId": "norn.graph",
|
||||
|
|
@ -235,7 +246,14 @@
|
|||
"graduationTrigger": "Split when embed.norn graph layout, interaction, fallback, and conformance fixtures are stable.",
|
||||
"capabilities": ["embed.norn", "graph.node.activate"],
|
||||
"requiredFixtures": ["sai-vn"],
|
||||
"expectedPaths": ["plugins/incubating/norn-graph.plugin.json"]
|
||||
"schemaPath": "schemas/gamecult.eve.plugin.v1.schema.json",
|
||||
"manifestPath": "plugins/incubating/norn-graph.plugin.json",
|
||||
"advertisementSchemaPath": "schemas/gamecult.eve.plugin_advertisement.v1.schema.json",
|
||||
"advertisementPath": "plugins/incubating/norn-graph.plugin-advertisement.json",
|
||||
"expectedPaths": [
|
||||
"plugins/incubating/norn-graph.plugin.json",
|
||||
"plugins/incubating/norn-graph.plugin-advertisement.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"pluginId": "tex.math",
|
||||
|
|
@ -247,7 +265,14 @@
|
|||
"graduationTrigger": "Split when TeX source, macro, baseline, cached render, and fallback fixtures are stable.",
|
||||
"capabilities": ["embed.tex", "tex.inline", "tex.block"],
|
||||
"requiredFixtures": ["sai-vn"],
|
||||
"expectedPaths": ["plugins/incubating/tex-math.plugin.json"]
|
||||
"schemaPath": "schemas/gamecult.eve.plugin.v1.schema.json",
|
||||
"manifestPath": "plugins/incubating/tex-math.plugin.json",
|
||||
"advertisementSchemaPath": "schemas/gamecult.eve.plugin_advertisement.v1.schema.json",
|
||||
"advertisementPath": "plugins/incubating/tex-math.plugin-advertisement.json",
|
||||
"expectedPaths": [
|
||||
"plugins/incubating/tex-math.plugin.json",
|
||||
"plugins/incubating/tex-math.plugin-advertisement.json"
|
||||
]
|
||||
}
|
||||
],
|
||||
"runtimes": [
|
||||
|
|
|
|||
|
|
@ -230,15 +230,27 @@ async function evaluatePlugin(plugin, fixtureResults) {
|
|||
const requiredFixtures = plugin.requiredFixtures || [];
|
||||
const missingRequiredFixtures = requiredFixtures.filter(id => !fixtureResults.some(fixture => fixture.id === id && fixture.status === "pass"));
|
||||
const missingIncubationFields = requiredIncubationFields(plugin).filter(field => !plugin[field]);
|
||||
const schemaErrors = await validateJsonDocument(plugin.schemaPath, plugin.manifestPath, {
|
||||
schema: "gamecult.eve.plugin.v1",
|
||||
pluginId: plugin.pluginId,
|
||||
});
|
||||
const advertisementErrors = await validateJsonDocument(plugin.advertisementSchemaPath, plugin.advertisementPath, {
|
||||
schema: "gamecult.eve.plugin_advertisement.v1",
|
||||
pluginId: plugin.pluginId,
|
||||
});
|
||||
const status = missingPaths.length
|
||||
? "missing-body"
|
||||
: missingRequiredFixtures.length
|
||||
? "missing-required-fixture"
|
||||
: missingIncubationFields.length
|
||||
? "missing-incubation-metadata"
|
||||
: plugin.kind === "incubating"
|
||||
? "incubating"
|
||||
: "external-owner-planned";
|
||||
: schemaErrors.length
|
||||
? "invalid-plugin-manifest"
|
||||
: advertisementErrors.length
|
||||
? "invalid-plugin-advertisement"
|
||||
: plugin.kind === "incubating"
|
||||
? "incubating"
|
||||
: "external-owner-planned";
|
||||
|
||||
return {
|
||||
pluginId: plugin.pluginId,
|
||||
|
|
@ -252,6 +264,12 @@ async function evaluatePlugin(plugin, fixtureResults) {
|
|||
optionalPlugins: plugin.optionalPlugins || [],
|
||||
requiredFixtures,
|
||||
missingRequiredFixtures,
|
||||
schemaPath: plugin.schemaPath || "",
|
||||
manifestPath: plugin.manifestPath || "",
|
||||
advertisementSchemaPath: plugin.advertisementSchemaPath || "",
|
||||
advertisementPath: plugin.advertisementPath || "",
|
||||
schemaErrors,
|
||||
advertisementErrors,
|
||||
expectedPaths,
|
||||
missingPaths,
|
||||
missingIncubationFields,
|
||||
|
|
@ -259,6 +277,89 @@ async function evaluatePlugin(plugin, fixtureResults) {
|
|||
};
|
||||
}
|
||||
|
||||
async function validateJsonDocument(schemaPath, documentPath, expected = {}) {
|
||||
const errors = [];
|
||||
if (!schemaPath) errors.push("schemaPath:missing");
|
||||
if (!documentPath) errors.push("documentPath:missing");
|
||||
if (!schemaPath || !documentPath) return errors;
|
||||
|
||||
const absoluteSchemaPath = path.join(repoRoot, schemaPath);
|
||||
const absoluteDocumentPath = path.join(repoRoot, documentPath);
|
||||
if (!existsSync(absoluteSchemaPath)) errors.push(`${schemaPath}:missing`);
|
||||
if (!existsSync(absoluteDocumentPath)) errors.push(`${documentPath}:missing`);
|
||||
if (errors.length) return errors;
|
||||
|
||||
try {
|
||||
const schema = JSON.parse(await readFile(absoluteSchemaPath, "utf8"));
|
||||
const document = JSON.parse(await readFile(absoluteDocumentPath, "utf8"));
|
||||
errors.push(...validateSchemaSubset(schema, document));
|
||||
for (const [key, value] of Object.entries(expected)) {
|
||||
if (document[key] !== value) errors.push(`${documentPath}:${key}:expected ${value} got ${document[key]}`);
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(`${documentPath}:invalid-json:${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateSchemaSubset(schema, value, pointer = "$") {
|
||||
const errors = [];
|
||||
if (!schema || typeof schema !== "object") return errors;
|
||||
|
||||
if (schema.const !== undefined && value !== schema.const) {
|
||||
errors.push(`${pointer}:const:${schema.const}`);
|
||||
}
|
||||
|
||||
if (schema.type && !matchesSchemaType(value, schema.type)) {
|
||||
errors.push(`${pointer}:type:${schema.type}`);
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (schema.minLength !== undefined && typeof value === "string" && value.length < schema.minLength) {
|
||||
errors.push(`${pointer}:minLength:${schema.minLength}`);
|
||||
}
|
||||
|
||||
if (schema.required && value && typeof value === "object" && !Array.isArray(value)) {
|
||||
for (const key of schema.required) {
|
||||
if (value[key] === undefined) errors.push(`${pointer}.${key}:required`);
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.properties && value && typeof value === "object" && !Array.isArray(value)) {
|
||||
for (const [key, childSchema] of Object.entries(schema.properties)) {
|
||||
if (value[key] !== undefined) {
|
||||
errors.push(...validateSchemaSubset(childSchema, value[key], `${pointer}.${key}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.items && Array.isArray(value)) {
|
||||
value.forEach((item, index) => {
|
||||
errors.push(...validateSchemaSubset(schema.items, item, `${pointer}[${index}]`));
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function matchesSchemaType(value, type) {
|
||||
switch (type) {
|
||||
case "array":
|
||||
return Array.isArray(value);
|
||||
case "object":
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
case "string":
|
||||
return typeof value === "string";
|
||||
case "number":
|
||||
return typeof value === "number";
|
||||
case "boolean":
|
||||
return typeof value === "boolean";
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function collectPluginCapabilityGaps(runtime, fixtureResults) {
|
||||
const supported = new Map((runtime.supportedPlugins || []).map(plugin => [plugin.pluginId, new Set(plugin.capabilities || [])]));
|
||||
const fixtureById = new Map((manifest.fixtures || []).map(fixture => [fixture.id, fixture]));
|
||||
|
|
@ -389,6 +490,8 @@ function renderMarkdown(report) {
|
|||
...plugin.missingPaths,
|
||||
...plugin.missingRequiredFixtures.map(id => `fixture:${id}`),
|
||||
...plugin.missingIncubationFields.map(id => `metadata:${id}`),
|
||||
...plugin.schemaErrors.map(id => `schema:${id}`),
|
||||
...plugin.advertisementErrors.map(id => `advertisement:${id}`),
|
||||
].join(", ");
|
||||
lines.push(`| ${plugin.title || plugin.pluginId} | ${plugin.status} | ${plugin.ownerRepo} | ${plugin.capabilities.join(", ")} | ${missing} |`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue