Export local provider catalog coverage

This commit is contained in:
Meta Vens 2026-07-09 16:09:00 +01:00
commit e063828c2c
8 changed files with 346 additions and 2 deletions

View file

@ -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

View file

@ -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 <runtimeId:status:path-substring>`,
`--expect-local-provider-catalog-provider <runtimeId:providerId>`, and
`--expect-local-provider-catalog-advertisement <runtimeId:advertisement-path-substring>`.
Plugin handoff paths are exported as `plugins[].handoffPath` for owner repos
such as Sai, Norn, and EvePlugins. These paths are incubation evidence for

View file

@ -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": {

View file

@ -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
}

View file

@ -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 `

View file

@ -22,6 +22,9 @@ if (!exportDirectory) {
" --expect-runtime-plugin-projection <runtimeId:pluginId:status:ownerRepo>",
" --expect-provider-runtime-plugin-projection <providerId:surfaceId:runtimeId:pluginId:status:runtimeOwnerRepo>",
" --expect-runtime-plugin-gap <runtimeId:pluginId:ownerRepo>",
" --expect-local-provider-catalog <runtimeId:status:path-substring>",
" --expect-local-provider-catalog-provider <runtimeId:providerId>",
" --expect-local-provider-catalog-advertisement <runtimeId:advertisement-path-substring>",
" --expect-world-lowering-coverage <providerId:surfaceId:targetId:status:ownerRepo:runtimeId>",
" --expect-command-boundary-coverage <providerId:surfaceId:targetId:status:ownerRepo:runtimeId>",
" --expect-world-lowering-gap <providerId:surfaceId:targetId:ownerRepo:runtimeId>",
@ -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 <runtimeId:status:path-substring> 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 <runtimeId:providerId> 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 <runtimeId:advertisement-path-substring> 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)) {

View file

@ -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",

View file

@ -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) {