Preserve advertised metadata in web provider fallback

This commit is contained in:
Meta Vens 2026-07-09 16:35:28 +01:00
commit 46de0bd309
9 changed files with 103 additions and 16 deletions

View file

@ -980,6 +980,12 @@ Recently cut:
capability claims; the local schema subset validators now resolve `$ref` and
enforce enum values so missing availability cannot hide in recursive runtime
capture trees.
- Browser reference fallback discovery now merges local catalog transport/URL
stubs with provider advertisement surface metadata instead of letting the
local stub erase semantics. `web/provider-advertisements.mjs` preserves
advertised `worldInteraction` and `requiresPlugins` for Aetheria and Sai
while keeping fixture-local loading paths, and both `web/surface.js` and the
layout probe consume that shared merge path.
- 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

@ -25,7 +25,7 @@ const fs = require('fs');
const path = require('path');
const root = process.argv[2];
const port = Number(process.argv[3]);
const types = {'.html':'text/html; charset=utf-8','.js':'text/javascript; charset=utf-8','.css':'text/css; charset=utf-8','.json':'application/json; charset=utf-8','.eve':'text/plain; charset=utf-8'};
const types = {'.html':'text/html; charset=utf-8','.js':'text/javascript; charset=utf-8','.mjs':'text/javascript; charset=utf-8','.css':'text/css; charset=utf-8','.json':'application/json; charset=utf-8','.eve':'text/plain; charset=utf-8'};
http.createServer((req, res) => {
const urlPath = decodeURIComponent(new URL(req.url, 'http://localhost').pathname);
const clean = urlPath === '/' ? '/index.html' : urlPath;

View file

@ -1145,7 +1145,15 @@
"expectedSourceSymbols": [
{
"path": "web/surface.js",
"contains": ["loadLocalProviderCatalog", "loadProviderAdvertisement", "publishCommandIntent", "browser.reference"]
"contains": ["loadLocalProviderCatalog", "loadProviderAdvertisement", "mergeProviderAdvertisement", "publishCommandIntent", "browser.reference"]
},
{
"path": "web/provider-advertisements.mjs",
"contains": ["mergeProviderAdvertisement", "mergeProviderSurface", "findProviderCatalogEntry"]
},
{
"path": "web/provider-advertisements.test.mjs",
"contains": ["preserves Sai required and optional nested plugin requirements from advertisement", "merges local fixture transport with advertised Aetheria world interaction", "requiresPlugins", "worldInteraction", "optional-nested", "aetheria.daemon.commands"]
},
{
"path": "web/local-provider-catalog.json",
@ -1181,6 +1189,7 @@
"evidencePaths": [
"web/index.html",
"web/surface.js",
"web/provider-advertisements.mjs",
"packages/eve-browser-lowering/src/index.ts"
],
"pendingProofs": [
@ -1193,6 +1202,7 @@
"command": "node --test web/eve-dsl.test.mjs && npm --prefix packages/eve-browser-lowering test && powershell -ExecutionPolicy Bypass -File .\\scripts\\run-parity-harness.ps1",
"evidencePaths": [
"web/eve-dsl.test.mjs",
"web/provider-advertisements.test.mjs",
"packages/eve-browser-lowering/test/browser-lowering.test.mjs",
"tools/parity/parity-manifest.json",
"scripts/run-parity-harness.ps1"

View file

@ -41,6 +41,7 @@ function createStaticServer(root) {
const types = {
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".mjs": "text/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".json": "application/json; charset=utf-8",
".eve": "text/plain; charset=utf-8",

View file

@ -69,6 +69,7 @@
"evidencePaths": [
"web/index.html",
"web/surface.js",
"web/provider-advertisements.mjs",
"packages/eve-browser-lowering/src/index.ts"
],
"pendingProofs": [
@ -81,6 +82,7 @@
"command": "node --test web/eve-dsl.test.mjs && npm --prefix packages/eve-browser-lowering test && powershell -ExecutionPolicy Bypass -File .\\scripts\\run-parity-harness.ps1",
"evidencePaths": [
"web/eve-dsl.test.mjs",
"web/provider-advertisements.test.mjs",
"packages/eve-browser-lowering/test/browser-lowering.test.mjs",
"tools/parity/parity-manifest.json",
"scripts/run-parity-harness.ps1"

View file

@ -1,5 +1,6 @@
import { renderEveSurface } from "../packages/eve-browser-lowering/dist/index.js";
import { compileEveDsl } from "./eve-dsl.js";
import { findProviderCatalogEntry, mergeProviderAdvertisement } from "./provider-advertisements.mjs";
const app = document.querySelector("#app");
const output = document.querySelector("#eve-layout-probe-output");
@ -40,17 +41,11 @@ try {
async function loadProvider(requestedProviderId) {
const response = await fetch("./local-provider-catalog.json", { cache: "no-store" });
const catalog = await response.json();
const provider = (catalog.providers || []).find(candidate =>
candidate.providerId === requestedProviderId || candidate.aliases?.includes(requestedProviderId));
const provider = findProviderCatalogEntry(catalog, requestedProviderId);
if (!provider) throw new Error(`Unknown local provider: ${requestedProviderId}`);
if (!provider.advertisement) return provider;
const advertisement = await (await fetch(provider.advertisement, { cache: "no-store" })).json();
return {
...advertisement,
...provider,
surfaces: provider.surfaces || advertisement.surfaces || [],
localAdvertisement: advertisement,
};
return mergeProviderAdvertisement(provider, advertisement);
}
async function loadSurface(surface) {

View file

@ -0,0 +1,34 @@
export function mergeProviderAdvertisement(catalogProvider, advertisement = undefined) {
if (!advertisement || typeof advertisement !== "object") {
return catalogProvider;
}
const catalogSurfaces = Array.isArray(catalogProvider?.surfaces) ? catalogProvider.surfaces : [];
const advertisedSurfaces = Array.isArray(advertisement.surfaces) ? advertisement.surfaces : [];
const surfaces = catalogSurfaces.length
? catalogSurfaces.map(surface => mergeProviderSurface(surface, advertisedSurfaces.find(candidate => candidate.surfaceId === surface.surfaceId)))
: advertisedSurfaces;
return {
...advertisement,
...catalogProvider,
surfaces,
localAdvertisement: advertisement,
};
}
export function mergeProviderSurface(catalogSurface, advertisedSurface = undefined) {
if (!advertisedSurface || typeof advertisedSurface !== "object") {
return catalogSurface;
}
return {
...advertisedSurface,
...catalogSurface,
};
}
export function findProviderCatalogEntry(catalog, requestedProviderId) {
const providers = Array.isArray(catalog?.providers) ? catalog.providers : [];
return providers.find(candidate =>
candidate.providerId === requestedProviderId || candidate.aliases?.includes(requestedProviderId));
}

View file

@ -0,0 +1,43 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { findProviderCatalogEntry, mergeProviderAdvertisement } from "./provider-advertisements.mjs";
const catalog = JSON.parse(readFileSync(new URL("./local-provider-catalog.json", import.meta.url), "utf8"));
const aetheriaAdvertisement = JSON.parse(readFileSync(new URL("./fixtures/aetheria.provider-advertisement.json", import.meta.url), "utf8"));
const saiAdvertisement = JSON.parse(readFileSync(new URL("./fixtures/sai-vn.provider-advertisement.json", import.meta.url), "utf8"));
test("merges local fixture transport with advertised Aetheria world interaction", () => {
const catalogProvider = findProviderCatalogEntry(catalog, "aetheria");
const provider = mergeProviderAdvertisement(catalogProvider, aetheriaAdvertisement);
const surface = provider.surfaces.find(candidate => candidate.surfaceId === "aetheria.daemon.game");
assert.equal(surface.transport, "local-json");
assert.equal(surface.url, "./fixtures/aetheria-world-surface.json");
assert.equal(surface.surfaceKind, "interactive-world");
assert.equal(surface.worldInteraction.projectionKind, "provider-authored-world-surface");
assert.equal(surface.worldInteraction.commandBoundary, "aetheria.daemon.commands");
assert.equal(surface.worldInteraction.receiptSchema, "aetheria.eve_command_acceptance_status.v1");
assert.equal(provider.localAdvertisement.providerId, "aetheria");
});
test("preserves Sai required and optional nested plugin requirements from advertisement", () => {
const catalogProvider = findProviderCatalogEntry(catalog, "gamecult.home.vn");
const provider = mergeProviderAdvertisement(catalogProvider, saiAdvertisement);
const surface = provider.surfaces.find(candidate => candidate.surfaceId === "sai.visual_novel.surface");
const byPlugin = new Map(surface.requiresPlugins.map(requirement => [requirement.pluginId, requirement]));
assert.equal(surface.transport, "local-json");
assert.equal(surface.url, "./fixtures/sai-vn-surface.json");
assert.equal(byPlugin.get("sai.vn").availability, "required");
assert.deepEqual(byPlugin.get("sai.vn").requiredCapabilities, [
"vn.stage",
"story.choose",
"story.continue",
"story.jump",
]);
assert.equal(byPlugin.get("norn.graph").availability, "optional-nested");
assert.deepEqual(byPlugin.get("norn.graph").optionalCapabilities, ["embed.norn"]);
assert.equal(byPlugin.get("tex.math").availability, "optional-nested");
assert.deepEqual(byPlugin.get("tex.math").optionalCapabilities, ["embed.tex"]);
});

View file

@ -4,6 +4,7 @@ import {
renderEveSurface,
} from "../packages/eve-browser-lowering/dist/index.js";
import { compileEveDsl } from "./eve-dsl.js";
import { mergeProviderAdvertisement } from "./provider-advertisements.mjs";
const statusEl = document.querySelector("#status");
const app = document.querySelector("#app");
@ -89,12 +90,7 @@ async function loadProviderAdvertisement(provider) {
if (!provider.advertisement) return provider;
const response = await fetch(provider.advertisement);
const advertisement = await response.json();
return {
...advertisement,
...provider,
surfaces: provider.surfaces || advertisement.surfaces || [],
localAdvertisement: advertisement,
};
return mergeProviderAdvertisement(provider, advertisement);
}
async function openProvider(provider) {