Discover browser world surfaces through Odin

This commit is contained in:
Meta Vens 2026-07-10 14:42:20 +01:00
commit 6270999e11
9 changed files with 137 additions and 95 deletions

View file

@ -182,8 +182,10 @@ Start the browser reference surface runner:
powershell -ExecutionPolicy Bypass -File .\scripts\start-browser-reference.ps1
```
Open `http://127.0.0.1:8891/`. The page can render local fixture and
advertisement surfaces:
Open `http://127.0.0.1:8798/`. Odin-owned Hermodr serves the browser reference
and lowers Odin-visible provider advertisements and surfaces. Local fixtures
remain an offline oracle when the live catalog is unavailable. The page can
render:
- `Fensalir Direct2D`, a fixture surface for the planned
CultMesh-to-`AquariumUiDocument` lowering.

View file

@ -115,6 +115,10 @@ advertisements and typed command/receipt documents are the boundary.
attachment, export consumer, and report artifacts.
- Eve retains the browser reference as the behavior oracle and a minimal generic
interactive-world fixture.
- The browser reference starts through Odin-owned Hermodr, preserves advertised
provider and surface identity, selects `interactive-world` by capability, and
resolves assets through provider-advertised CDN routes. Its live startup path
contains no Aetheria identifiers; local catalogs remain offline oracle data.
## Remaining Work
@ -127,9 +131,9 @@ advertisements and typed command/receipt documents are the boundary.
### Generic Game Lowering
- Route browser and remaining runtimes through Odin's aggregated provider
discovery rather than local catalogs or sibling paths; Unity's direct
CultMesh Verse discovery is the working reference.
- Route remaining runtimes through Odin's aggregated provider discovery rather
than local catalogs or sibling paths; browser/Hermodr and Unity direct Verse
discovery are the working references.
- Expand world lowering only through generic surface contracts: entities,
transforms, animation state, collision/navigation intent, cameras, effects,
interaction prompts, and command boundaries.

View file

@ -836,7 +836,9 @@ async function resolveGravitySurfaceDocuments(node, props, options, state) {
async function fetchGravitySurfaceDocument(request, options) {
if (typeof fetch !== "function" || typeof window === "undefined")
return undefined;
const providerId = currentSurfaceDocument?.providerId || options.provider?.providerId || "aetheria";
const providerId = currentSurfaceDocument?.providerId || options.provider?.providerId || "";
if (!providerId)
return undefined;
const params = new URLSearchParams();
params.set("documentId", request.documentId);
if (request.schemaId)
@ -1525,7 +1527,7 @@ function gravityBodiesFromDocument(document) {
waveSpeed: numberProp(body.waveSpeed, 0),
icon: assetUriFromRef(body.iconAsset) || assetUriFromRef(displayBody?.iconAsset),
iconSize: numberProp(displayBody?.iconSize, numberProp(body.iconSize, 0)),
tint: "cultmesh://aetheria/assets/textures/tint_splat",
tint: "",
};
});
}
@ -2224,10 +2226,6 @@ function textClassName(kind, props, node) {
classes.push("detail", "mono");
if (classes.length === 0)
classes.push("detail", "eve-text");
if (node.id === "aetheria.main_menu.root.title")
classes.push("eve-root-title");
if (node.id === "aetheria.main_menu.root.subtitle")
classes.push("eve-root-subtitle");
return classes.join(" ");
}
function applyControlBoxProps(element, props) {

View file

@ -1017,7 +1017,8 @@ async function fetchGravitySurfaceDocument(
options: EveBrowserLoweringOptions,
): Promise<unknown> {
if (typeof fetch !== "function" || typeof window === "undefined") return undefined;
const providerId = currentSurfaceDocument?.providerId || options.provider?.providerId || "aetheria";
const providerId = currentSurfaceDocument?.providerId || options.provider?.providerId || "";
if (!providerId) return undefined;
const params = new URLSearchParams();
params.set("documentId", request.documentId);
if (request.schemaId) params.set("schemaId", request.schemaId);
@ -1864,7 +1865,7 @@ function gravityBodiesFromDocument(document: Record<string, unknown> | undefined
waveSpeed: numberProp(body.waveSpeed, 0),
icon: assetUriFromRef((body as Record<string, unknown>).iconAsset) || assetUriFromRef(displayBody?.iconAsset),
iconSize: numberProp(displayBody?.iconSize, numberProp((body as Record<string, unknown>).iconSize, 0)),
tint: "cultmesh://aetheria/assets/textures/tint_splat",
tint: "",
};
});
}
@ -2642,8 +2643,6 @@ function textClassName(kind: string, props: Record<string, unknown>, node: EveSu
if (kind === "label") classes.push("cultui-label");
if (props.role === "mono") classes.push("detail", "mono");
if (classes.length === 0) classes.push("detail", "eve-text");
if (node.id === "aetheria.main_menu.root.title") classes.push("eve-root-title");
if (node.id === "aetheria.main_menu.root.subtitle") classes.push("eve-root-subtitle");
return classes.join(" ");
}

View file

@ -1,56 +1,28 @@
param(
[int] $Port = 8891
[int] $Port = 8798,
[string] $OdinRoot = "E:\Projects\Odin",
[string] $OdinCultMeshUri = $(if ($env:ODIN_CULTMESH_URI) { $env:ODIN_CULTMESH_URI } else { "cultmesh://odin/rendezvous/provider-catalog" })
)
$ErrorActionPreference = "Stop"
$projectRoot = Split-Path -Parent $PSScriptRoot
$webRoot = Join-Path $projectRoot "web"
$logRoot = Join-Path $projectRoot "artifacts\web"
New-Item -ItemType Directory -Force $logRoot | Out-Null
$pidPath = Join-Path $logRoot "eve-browser-reference.pid"
$outLog = Join-Path $logRoot "eve-browser-reference.out.log"
$errLog = Join-Path $logRoot "eve-browser-reference.err.log"
if (Test-Path $pidPath) {
$oldPid = Get-Content $pidPath -ErrorAction SilentlyContinue
if ($oldPid) {
Stop-Process -Id ([int]$oldPid) -ErrorAction SilentlyContinue
}
$launcher = Join-Path $OdinRoot "scripts\start-hermodr.ps1"
if (-not (Test-Path -LiteralPath $launcher)) {
throw "Odin-owned Hermodr launcher is missing: $launcher"
}
$nodeScript = @'
const http = require('http');
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','.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;
const file = path.normalize(path.join(root, clean));
if (!file.startsWith(root)) {
res.writeHead(403);
res.end('forbidden');
return;
$healthUrl = "http://127.0.0.1:$Port/health"
try {
$health = Invoke-RestMethod -Uri $healthUrl -TimeoutSec 2
if ($health.ok -and $health.service -eq "hermodr-browser-lowering") {
Write-Host "Eve browser reference already available at http://127.0.0.1:$Port/"
exit 0
}
fs.readFile(file, (err, data) => {
if (err) {
res.writeHead(404);
res.end('not found');
return;
}
res.writeHead(200, {'content-type': types[path.extname(file)] || 'application/octet-stream'});
res.end(data);
});
}).listen(port, '127.0.0.1', () => console.log(`Eve browser reference listening on http://127.0.0.1:${port}`));
'@
} catch {
# Hermodr is not running on the requested local port.
}
$scriptPath = Join-Path $logRoot "serve-eve-reference.cjs"
Set-Content -LiteralPath $scriptPath -Value $nodeScript -Encoding UTF8
$proc = Start-Process -FilePath "node" -ArgumentList @($scriptPath, $webRoot, $Port) -WorkingDirectory $projectRoot -RedirectStandardOutput $outLog -RedirectStandardError $errLog -PassThru -WindowStyle Hidden
$proc.Id | Set-Content -LiteralPath $pidPath
Start-Sleep -Milliseconds 500
Get-Content $outLog -Tail 5
Write-Host "PID $($proc.Id)"
& $launcher -HostAddress "127.0.0.1" -Port $Port -OdinCultMeshUri $OdinCultMeshUri -NoWindow
if (-not $?) {
throw "Hermodr failed to start."
}
Write-Host "Eve browser reference available at http://127.0.0.1:$Port/"

View file

@ -12,6 +12,9 @@
"supportedFeatures": [
"embeddedDocuments",
"providerAdvertisements",
"odinProviderDiscovery",
"genericWorldDiscovery",
"providerAssets",
"stateBindings",
"styleTokens",
"authorityWitnesses"
@ -57,6 +60,8 @@
"ownership": "runtime-lowers-provider-world-surface-without-owning-world-state",
"evidencePaths": [
"web/surface.js",
"web/hermodr-provider-catalog.mjs",
"scripts/start-browser-reference.ps1",
"packages/eve-browser-lowering/src/index.ts",
"web/fixtures/eve-world-smoke.provider-advertisement.json"
]
@ -79,10 +84,11 @@
"test": {
"ownerRepo": "Eve",
"status": "node-tests-and-parity-harness",
"command": "node --test web/eve-dsl.test.mjs && npm --prefix packages/eve-browser-lowering test && powershell -ExecutionPolicy Bypass -File .\\scripts\\run-parity-harness.ps1",
"command": "node --test web/*.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",
"web/hermodr-provider-catalog.test.mjs",
"packages/eve-browser-lowering/test/browser-lowering.test.mjs",
"../EveConformance/tools/parity/parity-manifest.json",
"scripts/run-parity-harness.ps1"

View file

@ -0,0 +1,41 @@
export function createHermodrProviderTargets(catalog) {
const providers = new Map((catalog?.providers || [])
.map(provider => [provider.id || provider.providerId, provider])
.filter(([providerId]) => providerId));
return (catalog?.surfaces || [])
.filter(surface => surface?.providerId && surface?.surfaceId)
.map(surface => {
const provider = providers.get(surface.providerId) || {};
const advertisedSurface = (provider.surfaces || [])
.find(candidate => candidate.surfaceId === surface.surfaceId) || {};
const surfaceId = surface.surfaceId;
const surfaceKind = surface.surfaceKind || advertisedSurface.surfaceKind || "";
return {
...provider,
providerId: surface.providerId,
targetId: `${surface.providerId}::${surfaceId}`,
title: surface.title || advertisedSurface.title || provider.title || surfaceId,
kind: provider.kind || "eve.provider",
freshness: { state: "odin-visible-cultmesh" },
surfaces: [{
...advertisedSurface,
transport: "hermodr-surface",
surfaceId,
surfaceKind,
url: `/hermodr/surface/${encodeURIComponent(surface.providerId)}?surfaceId=${encodeURIComponent(surfaceId)}`,
}],
};
});
}
export function selectInitialProviderTarget(targets, requestedId = "", preferredSurfaceKind = "interactive-world") {
const available = Array.isArray(targets) ? targets : [];
return available.find(target =>
target.targetId === requestedId ||
target.providerId === requestedId ||
target.surfaces?.some(surface => surface.surfaceId === requestedId) ||
target.aliases?.includes(requestedId))
|| available.find(target => target.surfaces?.some(surface => surface.surfaceKind === preferredSurfaceKind))
|| available[0];
}

View file

@ -0,0 +1,38 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
createHermodrProviderTargets,
selectInitialProviderTarget,
} from "./hermodr-provider-catalog.mjs";
const catalog = {
providers: [{
id: "fixture.game",
title: "Fixture game",
surfaces: [
{ surfaceId: "fixture.menu", surfaceKind: "menu" },
{ surfaceId: "fixture.world", surfaceKind: "interactive-world" },
],
}],
surfaces: [
{ providerId: "fixture.game", surfaceId: "fixture.menu", title: "Menu" },
{ providerId: "fixture.game", surfaceId: "fixture.world", surfaceKind: "interactive-world", title: "World" },
],
};
test("preserves provider and advertised surface identity from Hermodr", () => {
const targets = createHermodrProviderTargets(catalog);
assert.equal(targets.length, 2);
assert.equal(targets[1].providerId, "fixture.game");
assert.equal(targets[1].targetId, "fixture.game::fixture.world");
assert.equal(targets[1].surfaces[0].surfaceId, "fixture.world");
assert.equal(targets[1].surfaces[0].surfaceKind, "interactive-world");
assert.match(targets[1].surfaces[0].url, /surfaceId=fixture\.world/);
});
test("selects an interactive world without product identity", () => {
const targets = createHermodrProviderTargets(catalog);
assert.equal(selectInitialProviderTarget(targets).targetId, "fixture.game::fixture.world");
assert.equal(selectInitialProviderTarget(targets, "fixture.menu").targetId, "fixture.game::fixture.menu");
});

View file

@ -5,6 +5,10 @@ import {
} from "../packages/eve-browser-lowering/dist/index.js";
import { compileEveDsl } from "./eve-dsl.js";
import { mergeProviderAdvertisement } from "./provider-advertisements.mjs";
import {
createHermodrProviderTargets,
selectInitialProviderTarget,
} from "./hermodr-provider-catalog.mjs";
const statusEl = document.querySelector("#status");
const app = document.querySelector("#app");
@ -19,7 +23,7 @@ let liveHermodr = false;
let openProviderGeneration = 0;
providerSelect.addEventListener("change", () => {
const provider = providers.find(candidate => candidate.providerId === providerSelect.value);
const provider = providers.find(candidate => (candidate.targetId || candidate.providerId) === providerSelect.value);
if (provider) void openProvider(provider);
});
@ -34,18 +38,16 @@ async function bootProviders() {
}
providerSelect.replaceChildren(...providers.map(provider => {
const option = document.createElement("option");
option.value = provider.providerId;
option.value = provider.targetId || provider.providerId;
option.textContent = provider.title || provider.providerId;
return option;
}));
const requestedProviderId = new URLSearchParams(location.search).get("provider");
const firstProduct = providers.find(provider => provider.providerId === requestedProviderId || provider.aliases?.includes(requestedProviderId))
|| providers.find(provider => provider.providerId === "aetheria")
|| providers.find(provider => provider.providerId === "aetheria.main_menu.root")
|| providers.find(provider => provider.providerId === "aetheria.inventory.panel")
|| providers.find(provider => provider.providerId === "repixelizer")
|| providers[0];
providerSelect.value = firstProduct.providerId;
const params = new URLSearchParams(location.search);
const requestedProviderId = params.get("surface") || params.get("provider") || "";
const firstProduct = liveHermodr
? selectInitialProviderTarget(providers, requestedProviderId)
: providers.find(provider => provider.providerId === requestedProviderId || provider.aliases?.includes(requestedProviderId)) || providers[0];
providerSelect.value = firstProduct.targetId || firstProduct.providerId;
await openProvider(firstProduct);
}
@ -60,27 +62,7 @@ async function loadHermodrProviders() {
const response = await fetch("/hermodr/verse/catalog", { cache: "no-store" });
if (!response.ok) return [];
const catalog = await response.json();
const advertised = new Map();
for (const provider of catalog.providers || []) {
advertised.set(provider.id || provider.providerId, provider);
}
return (catalog.surfaces || [])
.filter(surface => surface?.providerId && surface?.surface)
.map(surface => {
const provider = advertised.get(surface.providerId) || {};
return {
...provider,
providerId: surface.providerId,
title: surface.title || provider.title || surface.providerId,
kind: provider.kind || "eve.surface",
freshness: { state: "odin-visible-cultmesh" },
surfaces: [{
transport: "hermodr-surface",
surfaceId: surface.providerId,
url: `/hermodr/surface/${encodeURIComponent(surface.providerId)}`,
}],
};
});
return createHermodrProviderTargets(catalog);
} catch {
return [];
}