From 46de0bd309c3445f0674c4c2c8eae5b16a5211a1 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 9 Jul 2026 16:35:28 +0100 Subject: [PATCH] Preserve advertised metadata in web provider fallback --- docs/eve-migration-roadmap.md | 6 ++++ scripts/start-browser-reference.ps1 | 2 +- tools/parity/parity-manifest.json | 12 ++++++- tools/web-reference/run-layout-probe.mjs | 1 + web/eve-runtime-capability.json | 2 ++ web/layout-probe.js | 11 ++---- web/provider-advertisements.mjs | 34 +++++++++++++++++++ web/provider-advertisements.test.mjs | 43 ++++++++++++++++++++++++ web/surface.js | 8 ++--- 9 files changed, 103 insertions(+), 16 deletions(-) create mode 100644 web/provider-advertisements.mjs create mode 100644 web/provider-advertisements.test.mjs diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index 3b98c8a..fafc404 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -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 diff --git a/scripts/start-browser-reference.ps1 b/scripts/start-browser-reference.ps1 index 3514e54..248dbc0 100644 --- a/scripts/start-browser-reference.ps1 +++ b/scripts/start-browser-reference.ps1 @@ -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; diff --git a/tools/parity/parity-manifest.json b/tools/parity/parity-manifest.json index a5d93d8..79b0523 100644 --- a/tools/parity/parity-manifest.json +++ b/tools/parity/parity-manifest.json @@ -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" diff --git a/tools/web-reference/run-layout-probe.mjs b/tools/web-reference/run-layout-probe.mjs index b7a56ef..a4371ae 100644 --- a/tools/web-reference/run-layout-probe.mjs +++ b/tools/web-reference/run-layout-probe.mjs @@ -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", diff --git a/web/eve-runtime-capability.json b/web/eve-runtime-capability.json index 7e2260b..e9153d2 100644 --- a/web/eve-runtime-capability.json +++ b/web/eve-runtime-capability.json @@ -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" diff --git a/web/layout-probe.js b/web/layout-probe.js index 4ed49c9..dd3f7bf 100644 --- a/web/layout-probe.js +++ b/web/layout-probe.js @@ -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) { diff --git a/web/provider-advertisements.mjs b/web/provider-advertisements.mjs new file mode 100644 index 0000000..a9ea701 --- /dev/null +++ b/web/provider-advertisements.mjs @@ -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)); +} diff --git a/web/provider-advertisements.test.mjs b/web/provider-advertisements.test.mjs new file mode 100644 index 0000000..fb1f6f2 --- /dev/null +++ b/web/provider-advertisements.test.mjs @@ -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"]); +}); diff --git a/web/surface.js b/web/surface.js index 62d13ba..5e1f548 100644 --- a/web/surface.js +++ b/web/surface.js @@ -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) {