From e063828c2cf9433243566be024b9c91a0bde2f24 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 9 Jul 2026 16:09:00 +0100 Subject: [PATCH] Export local provider catalog coverage --- docs/eve-migration-roadmap.md | 11 +- docs/parity-testing-harness.md | 9 ++ ...cult.eve.conformance_export.v1.schema.json | 63 +++++++++++ ....eve.local_provider_catalog.v1.schema.json | 95 ++++++++++++++++ scripts/run-conformance-consumer-smoke.ps1 | 8 ++ tools/conformance/consume-export.mjs | 106 ++++++++++++++++++ tools/parity/parity-manifest.json | 1 + tools/parity/run-parity.mjs | 55 +++++++++ 8 files changed, 346 insertions(+), 2 deletions(-) create mode 100644 schemas/gamecult.eve.local_provider_catalog.v1.schema.json diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index d40ab1e..2f095a0 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -811,7 +811,11 @@ Recently cut: advertisement-backed entries for Aetheria, Repixelizer, and Sai through the same `loadProviderAdvertisement` path used by the provider picker. Fixture-only surfaces remain explicit local catalog entries until they gain provider - advertisements or move into their owner packs. + advertisements or move into their owner packs. The fallback catalog is now + schema-backed as `gamecult.eve.local_provider_catalog.v1`, copied into the + conformance export, and indexed as `localProviderCatalogs[]` so external + consumers can verify which provider advertisements the browser reference uses + without importing Eve's source layout. - Provider advertisements can now name sidecar plugin requirements per surface with `surfaces[].requiresPlugins[]`. The parity harness validates those requirements against known plugin manifests, exports them in the conformance @@ -947,7 +951,10 @@ Recently cut: - The surface contract now states nested Norn and TeX placement as optional composition, not Sai custody. The plugin-owner consumer can assert provider-plugin requirement availability, so `sai.vn` remains required while - `norn.graph` and `tex.math` stay optional nested sidecar plugins. + `norn.graph` and `tex.math` stay optional nested sidecar plugins. The browser + fallback catalog only advertises the Sai provider surface; independent Norn + and TeX availability is discovered through that provider's plugin + requirements and the separate plugin advertisements. - Sai, Norn, and TeX now have plugin ABI fixtures for describe, validate, project, lower, measure, and apply behavior. The parity harness validates those fixtures against plugin manifests, so Eve proves the ABI shape without diff --git a/docs/parity-testing-harness.md b/docs/parity-testing-harness.md index d2fc668..109cf0a 100644 --- a/docs/parity-testing-harness.md +++ b/docs/parity-testing-harness.md @@ -170,6 +170,15 @@ 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 capabilities. +Browser reference fallback discovery is exported as `localProviderCatalogs[]`. +Each record names the runtime, source catalog path, exported catalog copy, +schema, purpose, provider ids, advertisement paths, surface count, status, and +validation errors. This proves which local provider/plugin advertisements the +web picker is consuming without turning the fallback file into live discovery +truth. Consumer smokes can assert entries with +`--expect-local-provider-catalog `, +`--expect-local-provider-catalog-provider `, and +`--expect-local-provider-catalog-advertisement `. Plugin handoff paths are exported as `plugins[].handoffPath` for owner repos such as Sai, Norn, and EvePlugins. These paths are incubation evidence for diff --git a/schemas/gamecult.eve.conformance_export.v1.schema.json b/schemas/gamecult.eve.conformance_export.v1.schema.json index a884f4c..5857394 100644 --- a/schemas/gamecult.eve.conformance_export.v1.schema.json +++ b/schemas/gamecult.eve.conformance_export.v1.schema.json @@ -16,6 +16,7 @@ "providerPluginRequirementCoverage", "runtimePluginProjectionCoverage", "providerRuntimePluginProjectionCoverage", + "localProviderCatalogs", "interactiveWorldSurfaces", "worldSurfaceLoweringCoverage", "commandBoundaryCoverage", @@ -352,6 +353,68 @@ "additionalProperties": true } }, + "localProviderCatalogs": { + "type": "array", + "items": { + "type": "object", + "required": [ + "runtimeId", + "ownerRepo", + "catalogPath", + "catalogExportPath", + "schema", + "purpose", + "status", + "providerIds", + "advertisementPaths", + "surfaceCount", + "errors" + ], + "properties": { + "runtimeId": { + "type": "string", + "minLength": 1 + }, + "ownerRepo": { + "type": "string", + "minLength": 1 + }, + "catalogPath": { + "type": "string", + "minLength": 1 + }, + "catalogExportPath": { + "type": "string", + "minLength": 1 + }, + "schema": { + "type": "string", + "minLength": 1 + }, + "purpose": { + "type": "string", + "minLength": 1 + }, + "status": { + "type": "string", + "minLength": 1 + }, + "providerIds": { + "type": "array" + }, + "advertisementPaths": { + "type": "array" + }, + "surfaceCount": { + "type": "number" + }, + "errors": { + "type": "array" + } + }, + "additionalProperties": true + } + }, "splitTargetBlockers": { "type": "array", "items": { diff --git a/schemas/gamecult.eve.local_provider_catalog.v1.schema.json b/schemas/gamecult.eve.local_provider_catalog.v1.schema.json new file mode 100644 index 0000000..b86f69f --- /dev/null +++ b/schemas/gamecult.eve.local_provider_catalog.v1.schema.json @@ -0,0 +1,95 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "gamecult.eve.local_provider_catalog.v1", + "title": "Eve Local Provider Catalog", + "type": "object", + "required": [ + "schema", + "purpose", + "providers" + ], + "properties": { + "schema": { + "const": "gamecult.eve.local_provider_catalog.v1" + }, + "purpose": { + "type": "string", + "minLength": 1 + }, + "providers": { + "type": "array", + "items": { + "type": "object", + "required": [ + "providerId", + "title", + "kind", + "surfaces" + ], + "properties": { + "providerId": { + "type": "string", + "minLength": 1 + }, + "aliases": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "title": { + "type": "string", + "minLength": 1 + }, + "kind": { + "type": "string", + "minLength": 1 + }, + "advertisement": { + "type": "string" + }, + "freshness": { + "type": "object", + "required": [ + "state" + ], + "properties": { + "state": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": true + }, + "surfaces": { + "type": "array", + "items": { + "type": "object", + "required": [ + "transport", + "surfaceId" + ], + "properties": { + "transport": { + "type": "string", + "minLength": 1 + }, + "surfaceId": { + "type": "string", + "minLength": 1 + }, + "url": { + "type": "string" + } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true + } + } + }, + "additionalProperties": true +} diff --git a/scripts/run-conformance-consumer-smoke.ps1 b/scripts/run-conformance-consumer-smoke.ps1 index 88d267c..7da72ea 100644 --- a/scripts/run-conformance-consumer-smoke.ps1 +++ b/scripts/run-conformance-consumer-smoke.ps1 @@ -45,10 +45,18 @@ try { --expect-schema gamecult.eve.runtime_split_handoff.v1 ` --expect-schema gamecult.eve.plugin_handoff.v1 ` --expect-schema gamecult.eve.provider_handoff.v1 ` + --expect-schema gamecult.eve.local_provider_catalog.v1 ` --expect-schema gamecult.eve.electron_shell_projection.v1 ` --expect-schema gamecult.eve.tui_grid.v1 ` --expect-schema gamecult.eve.web_layout_probe.v1 ` --expect-capability-gap runtime:Fensalir:direct2d:capture:missing ` + --expect-local-provider-catalog web:valid:local-provider-catalog.json ` + --expect-local-provider-catalog-provider web:aetheria ` + --expect-local-provider-catalog-provider web:repixelizer ` + --expect-local-provider-catalog-provider web:gamecult.home.vn ` + --expect-local-provider-catalog-advertisement web:aetheria.provider-advertisement.json ` + --expect-local-provider-catalog-advertisement web:repixelizer.provider-advertisement.json ` + --expect-local-provider-catalog-advertisement web:sai-vn.provider-advertisement.json ` --expect-world-lowering-coverage aetheria:aetheria.daemon.game:web-reference:claimed:Eve:web ` --expect-world-lowering-coverage aetheria:aetheria.daemon.game:unity-uitoolkit:claimed:EveUnity:unity-uitoolkit ` --expect-world-lowering-coverage aetheria:aetheria.daemon.game:unity-scene:claimed:EveUnity:unity-scene ` diff --git a/tools/conformance/consume-export.mjs b/tools/conformance/consume-export.mjs index 5ed527d..30826fe 100644 --- a/tools/conformance/consume-export.mjs +++ b/tools/conformance/consume-export.mjs @@ -22,6 +22,9 @@ if (!exportDirectory) { " --expect-runtime-plugin-projection ", " --expect-provider-runtime-plugin-projection ", " --expect-runtime-plugin-gap ", + " --expect-local-provider-catalog ", + " --expect-local-provider-catalog-provider ", + " --expect-local-provider-catalog-advertisement ", " --expect-world-lowering-coverage ", " --expect-command-boundary-coverage ", " --expect-world-lowering-gap ", @@ -143,6 +146,7 @@ function validateIndex(index, directory, expectations, errors) { const runtimePluginProjectionCoverage = Array.isArray(index.runtimePluginProjectionCoverage) ? index.runtimePluginProjectionCoverage : []; const providerRuntimePluginProjectionCoverage = Array.isArray(index.providerRuntimePluginProjectionCoverage) ? index.providerRuntimePluginProjectionCoverage : []; const runtimePluginProjectionGaps = Array.isArray(index.runtimePluginProjectionGaps) ? index.runtimePluginProjectionGaps : []; + const localProviderCatalogs = Array.isArray(index.localProviderCatalogs) ? index.localProviderCatalogs : []; const interactiveWorldSurfaces = Array.isArray(index.interactiveWorldSurfaces) ? index.interactiveWorldSurfaces : []; const worldSurfaceLoweringCoverage = Array.isArray(index.worldSurfaceLoweringCoverage) ? index.worldSurfaceLoweringCoverage : []; const commandBoundaryCoverage = Array.isArray(index.commandBoundaryCoverage) ? index.commandBoundaryCoverage : []; @@ -174,6 +178,7 @@ function validateIndex(index, directory, expectations, errors) { validateRuntimePluginProjectionCoverage(index.runtimePluginProjectionCoverage, errors); validateProviderRuntimePluginProjectionCoverage(index.providerRuntimePluginProjectionCoverage, errors); validateRuntimePluginProjectionGaps(index.runtimePluginProjectionGaps, errors); + validateLocalProviderCatalogs(index.localProviderCatalogs, directory, errors); validateSplitTargetBlockers(index.splitTargetBlockers, errors); validateSplitHandoffMoveCoverage(index.splitHandoffMoveCoverage, errors); validatePluginHandoffMoveCoverage(index.pluginHandoffMoveCoverage, errors); @@ -234,6 +239,39 @@ function validateIndex(index, directory, expectations, errors) { errors.push(`runtimePluginProjectionGaps:${expectation.runtimeId}:${expectation.pluginId}:reason:missing`); } } + for (const expectation of expectations.localProviderCatalogs) { + const catalog = localProviderCatalogs.find(candidate => candidate.runtimeId === expectation.runtimeId); + if (!catalog) { + errors.push(`localProviderCatalogs:${expectation.runtimeId}:missing`); + continue; + } + if (catalog.status !== expectation.status) { + errors.push(`localProviderCatalogs:${expectation.runtimeId}:status:expected ${expectation.status} got ${catalog.status || ""}`); + } + if (!catalog.catalogPath?.includes(expectation.pathSubstring) && !catalog.catalogExportPath?.includes(expectation.pathSubstring)) { + errors.push(`localProviderCatalogs:${expectation.runtimeId}:path:expected substring ${expectation.pathSubstring}`); + } + } + for (const expectation of expectations.localProviderCatalogProviders) { + const catalog = localProviderCatalogs.find(candidate => candidate.runtimeId === expectation.runtimeId); + if (!catalog) { + errors.push(`localProviderCatalogs:${expectation.runtimeId}:missing`); + continue; + } + if (!Array.isArray(catalog.providerIds) || !catalog.providerIds.includes(expectation.providerId)) { + errors.push(`localProviderCatalogs:${expectation.runtimeId}:provider:${expectation.providerId}:missing`); + } + } + for (const expectation of expectations.localProviderCatalogAdvertisements) { + const catalog = localProviderCatalogs.find(candidate => candidate.runtimeId === expectation.runtimeId); + if (!catalog) { + errors.push(`localProviderCatalogs:${expectation.runtimeId}:missing`); + continue; + } + if (!Array.isArray(catalog.advertisementPaths) || !catalog.advertisementPaths.some(candidate => candidate.includes(expectation.pathSubstring))) { + errors.push(`localProviderCatalogs:${expectation.runtimeId}:advertisement:${expectation.pathSubstring}:missing`); + } + } for (const expectation of expectations.worldLoweringCoverage) { const coverage = worldSurfaceLoweringCoverage.find(candidate => candidate.providerId === expectation.providerId && @@ -1070,6 +1108,25 @@ function validateProviderPluginRequirementCoverage(records, errors) { } } +function validateLocalProviderCatalogs(catalogs, directory, errors) { + if (!Array.isArray(catalogs)) { + errors.push("localProviderCatalogs:missing"); + return; + } + for (const [index, catalog] of catalogs.entries()) { + for (const field of ["runtimeId", "ownerRepo", "catalogPath", "catalogExportPath", "schema", "purpose", "status"]) { + if (!catalog?.[field]) errors.push(`localProviderCatalogs:${index}:${field}:missing`); + } + for (const field of ["providerIds", "advertisementPaths", "errors"]) { + if (!Array.isArray(catalog?.[field])) errors.push(`localProviderCatalogs:${index}:${field}:expected array`); + } + if (typeof catalog?.surfaceCount !== "number") errors.push(`localProviderCatalogs:${index}:surfaceCount:expected number`); + if (catalog?.catalogExportPath && !existsSync(path.join(directory, catalog.catalogExportPath))) { + errors.push(`localProviderCatalogs:${index}:catalogExportPath:${catalog.catalogExportPath}:missing`); + } + } +} + function validateInteractiveWorldSurfaces(surfaces, errors) { if (!Array.isArray(surfaces)) { errors.push("interactiveWorldSurfaces:missing"); @@ -1197,6 +1254,9 @@ function parseArguments(args) { runtimePluginProjectionCoverage: [], providerRuntimePluginProjectionCoverage: [], runtimePluginGaps: [], + localProviderCatalogs: [], + localProviderCatalogProviders: [], + localProviderCatalogAdvertisements: [], worldLoweringCoverage: [], commandBoundaryCoverage: [], worldLoweringGaps: [], @@ -1251,6 +1311,9 @@ function parseArguments(args) { ["--expect-runtime-plugin-projection", expectations.runtimePluginProjectionCoverage], ["--expect-provider-runtime-plugin-projection", expectations.providerRuntimePluginProjectionCoverage], ["--expect-runtime-plugin-gap", expectations.runtimePluginGaps], + ["--expect-local-provider-catalog", expectations.localProviderCatalogs], + ["--expect-local-provider-catalog-provider", expectations.localProviderCatalogProviders], + ["--expect-local-provider-catalog-advertisement", expectations.localProviderCatalogAdvertisements], ["--expect-world-lowering-coverage", expectations.worldLoweringCoverage], ["--expect-command-boundary-coverage", expectations.commandBoundaryCoverage], ["--expect-world-lowering-gap", expectations.worldLoweringGaps], @@ -1361,6 +1424,12 @@ function parseArguments(args) { target.push(parseRuntimePluginProjectionCoverageExpectation(value)); } else if (option === "--expect-provider-runtime-plugin-projection") { target.push(parseProviderRuntimePluginProjectionCoverageExpectation(value)); + } else if (option === "--expect-local-provider-catalog") { + target.push(parseLocalProviderCatalogExpectation(value)); + } else if (option === "--expect-local-provider-catalog-provider") { + target.push(parseLocalProviderCatalogProviderExpectation(value)); + } else if (option === "--expect-local-provider-catalog-advertisement") { + target.push(parseLocalProviderCatalogAdvertisementExpectation(value)); } else { target.push(value); } @@ -1384,6 +1453,43 @@ function parseScreenshotMetricExpectation(value) { }; } +function parseLocalProviderCatalogExpectation(value) { + const parts = value.split(":"); + if (parts.length !== 3 || parts.some(part => !part)) { + console.error(`Expected local provider catalog in form, got: ${value}`); + process.exit(2); + } + return { + runtimeId: parts[0], + status: parts[1], + pathSubstring: parts[2], + }; +} + +function parseLocalProviderCatalogProviderExpectation(value) { + const parts = value.split(":"); + if (parts.length !== 2 || parts.some(part => !part)) { + console.error(`Expected local provider catalog provider in form, got: ${value}`); + process.exit(2); + } + return { + runtimeId: parts[0], + providerId: parts[1], + }; +} + +function parseLocalProviderCatalogAdvertisementExpectation(value) { + const parts = value.split(":"); + if (parts.length !== 2 || parts.some(part => !part)) { + console.error(`Expected local provider catalog advertisement in form, got: ${value}`); + process.exit(2); + } + return { + runtimeId: parts[0], + pathSubstring: parts[1], + }; +} + function parseRuntimeCaptureArtifactExpectation(value) { const parts = value.split(":"); if (parts.length !== 5 || parts.some(part => !part)) { diff --git a/tools/parity/parity-manifest.json b/tools/parity/parity-manifest.json index 2eadf6a..e379ea5 100644 --- a/tools/parity/parity-manifest.json +++ b/tools/parity/parity-manifest.json @@ -27,6 +27,7 @@ "gamecult.eve.web_layout_probe.v1": "schemas/gamecult.eve.web_layout_probe.v1.schema.json", "gamecult.eve.provider_scenario.v1": "schemas/gamecult.eve.provider_scenario.v1.schema.json", "gamecult.eve.provider_advertisement.v1": "schemas/gamecult.eve.provider_advertisement.v1.schema.json", + "gamecult.eve.local_provider_catalog.v1": "schemas/gamecult.eve.local_provider_catalog.v1.schema.json", "gamecult.eve.provider_handoff.v1": "schemas/gamecult.eve.provider_handoff.v1.schema.json", "gamecult.eve.conformance_handoff.v1": "schemas/gamecult.eve.conformance_handoff.v1.schema.json", "gamecult.eve.electron_shell_projection.v1": "schemas/gamecult.eve.electron_shell_projection.v1.schema.json", diff --git a/tools/parity/run-parity.mjs b/tools/parity/run-parity.mjs index bf99eeb..72e5eba 100644 --- a/tools/parity/run-parity.mjs +++ b/tools/parity/run-parity.mjs @@ -502,6 +502,14 @@ async function validateRuntimeLocalProviderCatalog(runtime) { return [`${catalogPath}:invalid-json:${error instanceof Error ? error.message : String(error)}`]; } + errors.push(...await validateJsonDocument( + manifest.schemas?.["gamecult.eve.local_provider_catalog.v1"], + catalogPath, + { + schema: "gamecult.eve.local_provider_catalog.v1", + }, + )); + const providers = Array.isArray(catalog.providers) ? catalog.providers : []; const providerIds = new Set(providers.map(provider => provider.providerId).filter(Boolean)); for (const providerId of catalogConfig.requiredProviders || []) { @@ -1990,6 +1998,7 @@ function buildConformanceExport(report) { runtimePluginProjectionCoverage: collectRuntimePluginProjectionCoverage(report), runtimePluginProjectionGaps: collectRuntimePluginProjectionGaps(report), providerRuntimePluginProjectionCoverage: collectProviderRuntimePluginProjectionCoverage(report), + localProviderCatalogs: collectLocalProviderCatalogs(report), interactiveWorldSurfaces: collectInteractiveWorldSurfaces(report), worldSurfaceLoweringCoverage: collectWorldSurfaceLoweringCoverage(report), commandBoundaryCoverage: collectCommandBoundaryCoverage(report), @@ -2073,6 +2082,44 @@ function buildSchemaCatalog() { .sort((left, right) => left.schemaId.localeCompare(right.schemaId)); } +function collectLocalProviderCatalogs(report) { + return (report.runtimes || []) + .filter(runtime => runtime.localProviderCatalog?.path) + .map(runtime => { + const catalogPath = runtime.localProviderCatalog.path; + let catalog = {}; + try { + catalog = JSON.parse(readFileSync(path.join(repoRoot, catalogPath), "utf8")); + } catch { + catalog = {}; + } + const providers = Array.isArray(catalog.providers) ? catalog.providers : []; + const catalogDirectory = path.posix.dirname(normalizePath(catalogPath)); + return { + runtimeId: runtime.id, + ownerRepo: runtime.ownerRepo || "", + catalogPath, + catalogExportPath: makeLocalProviderCatalogExportPath(runtime.id, catalogPath), + schema: catalog.schema || "", + purpose: catalog.purpose || "", + status: (runtime.localProviderCatalogErrors || []).length ? "invalid" : "valid", + providerIds: providers.map(provider => provider.providerId).filter(Boolean).sort(), + advertisementPaths: providers + .map(provider => resolveCatalogPath(catalogDirectory, provider.advertisement || "")) + .filter(Boolean) + .sort(), + surfaceCount: providers.reduce((count, provider) => count + (Array.isArray(provider.surfaces) ? provider.surfaces.length : 0), 0), + errors: runtime.localProviderCatalogErrors || [], + }; + }) + .sort((left, right) => left.runtimeId.localeCompare(right.runtimeId)); +} + +function makeLocalProviderCatalogExportPath(runtimeId, catalogPath) { + if (!runtimeId || !catalogPath) return ""; + return `catalogs/${runtimeId}/${path.basename(catalogPath)}`; +} + function buildCapabilityMatrix(report) { const packs = (report.conformancePacks || []).map(pack => { const fixtures = (report.fixtures || []).filter(fixture => fixture.pack === pack.id); @@ -3121,6 +3168,14 @@ async function writeConformanceExport(conformanceExport, directory) { await mkdir(path.dirname(destinationPath), { recursive: true }); await writeFile(destinationPath, await readFile(sourcePath, "utf8")); } + for (const catalog of conformanceExport.localProviderCatalogs || []) { + if (!catalog.catalogPath || !catalog.catalogExportPath) continue; + const sourcePath = path.join(repoRoot, catalog.catalogPath); + if (!existsSync(sourcePath)) continue; + const destinationPath = path.join(directory, catalog.catalogExportPath); + await mkdir(path.dirname(destinationPath), { recursive: true }); + await writeFile(destinationPath, await readFile(sourcePath, "utf8")); + } } function collectConformanceHandoffs(conformanceExport) {