Initialize first-party Eve plugin owners

This commit is contained in:
Meta Vens 2026-07-10 09:43:24 +01:00
commit 6f23900898
14 changed files with 441 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules/
artifacts/

6
LICENSE Normal file
View file

@ -0,0 +1,6 @@
Mozilla Public License Version 2.0
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.

15
README.md Normal file
View file

@ -0,0 +1,15 @@
# EvePlugins
First-party reusable semantic plugins for Eve. Each plugin owns its domain
semantics and runs through the runtime-independent Eve plugin ABI. Eve owns the
contract; this repository owns plugin behavior.
The first owner plugin is `tex.math`, implemented under
`plugins/eve-plugin-tex`. It uses KaTeX for parsing and typesetting, emits
render-result projections, and never mutates provider state.
```powershell
npm install
npm test
```

56
package-lock.json generated Normal file
View file

@ -0,0 +1,56 @@
{
"name": "@gamecult/eve-plugins",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@gamecult/eve-plugins",
"version": "0.1.0",
"license": "MPL-2.0",
"workspaces": [
"plugins/*"
]
},
"node_modules/@gamecult/eve-plugin-tex": {
"resolved": "plugins/eve-plugin-tex",
"link": true
},
"node_modules/commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/katex": {
"version": "0.16.47",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz",
"integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==",
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"
],
"license": "MIT",
"dependencies": {
"commander": "^8.3.0"
},
"bin": {
"katex": "cli.js"
}
},
"plugins/eve-plugin-tex": {
"name": "@gamecult/eve-plugin-tex",
"version": "0.1.0",
"license": "MPL-2.0",
"dependencies": {
"katex": "^0.16.22"
},
"bin": {
"eve-plugin-tex": "src/sidecar.mjs"
}
}
}
}

11
package.json Normal file
View file

@ -0,0 +1,11 @@
{
"name": "@gamecult/eve-plugins",
"version": "0.1.0",
"private": true,
"license": "MPL-2.0",
"workspaces": ["plugins/*"],
"scripts": {
"test": "C:\\Progra~1\\nodejs\\node.exe --test plugins/*/test/*.test.mjs",
"witness:tex": "C:\\Progra~1\\nodejs\\node.exe scripts/run-tex-witness.mjs"
}
}

View file

@ -0,0 +1,28 @@
{
"schema": "gamecult.eve.plugin_advertisement.v1",
"pluginId": "tex.math",
"ownerService": "eveplugins",
"version": "0.1.0",
"manifestAddress": "file://plugins/eve-plugin-tex/plugin.json",
"runtime": {
"invocationModel": "executable-sidecar",
"contract": "gamecult.eve.plugin_abi.v1",
"transports": ["stdio"],
"authority": ["renderer-independent", "no-provider-state-mutation", "provider-accepts-or-denies-commands"],
"sidecar": {
"processKind": "long-running-daemon",
"protocol": "stdio-ndjson",
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
"operations": ["describe", "validate", "project", "lower", "measure", "apply"],
"commandEnvelope": "gamecult.eve.command_invocation.v1",
"receiptSchema": "gamecult.eve.plugin_receipt.v1",
"stateAuthority": "emits-render-results-only-provider-owns-source-state"
}
},
"schemas": ["tex.math.source.v1", "tex.math.render_request.v1", "tex.math.render_result.v1"],
"componentKinds": ["embed.tex"],
"commands": [],
"fixtures": ["tex-math-basic"]
}

View file

@ -0,0 +1,8 @@
{
"schema": "tex.math.source.v1",
"documentId": "tex.basic",
"source": "E = mc^2",
"displayMode": true,
"macros": {}
}

View file

@ -0,0 +1,14 @@
{
"name": "@gamecult/eve-plugin-tex",
"version": "0.1.0",
"description": "Runtime-independent TeX semantic sidecar for Eve surfaces.",
"license": "MPL-2.0",
"type": "module",
"bin": {
"eve-plugin-tex": "./src/sidecar.mjs"
},
"dependencies": {
"katex": "^0.16.22"
}
}

View file

@ -0,0 +1,18 @@
{
"schema": "gamecult.eve.plugin_abi_fixture.v1",
"fixtureId": "tex-math-live-sidecar",
"pluginId": "tex.math",
"ownerRepo": "EvePlugins",
"contract": "gamecult.eve.plugin_abi.v1",
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
"authority": "EvePlugins owns TeX semantics; providers own source state and runtimes own projection.",
"operations": [
{ "operation": "describe", "input": {}, "expect": { "pluginId": "tex.math", "componentKinds": ["embed.tex"], "commands": [], "capabilities": ["embed.tex", "tex.inline", "tex.block"] } },
{ "operation": "validate", "input": { "componentKinds": ["embed.tex", "embed.norn"], "source": "E = mc^2" }, "expect": { "acceptedComponentKinds": ["embed.tex"], "rejectedComponentKinds": ["embed.norn"], "validSource": true, "requiredCapabilities": ["embed.tex"] } },
{ "operation": "project", "input": { "documentId": "tex.basic", "source": "E = mc^2", "displayMode": true }, "expect": { "projectionKind": "render-request", "ownedComponentKinds": ["embed.tex"], "outputSchemas": ["tex.math.render_request.v1", "tex.math.render_result.v1"] } },
{ "operation": "lower", "input": { "documentId": "tex.basic", "source": "E = mc^2", "displayMode": true }, "expect": { "loweringKind": "typeset-fragment", "preservedComponentKinds": ["embed.tex"], "requiredSchemas": ["tex.math.render_request.v1", "tex.math.render_result.v1"], "fallbackKind": "source-text" } },
{ "operation": "measure", "input": { "documentId": "tex.basic", "source": "E = mc^2" }, "expect": { "measurementKind": "typeset-baseline-metrics", "measurementOutputs": ["baseline", "advance", "inkBounds", "fallbackBounds"], "preservesProviderAuthority": true } },
{ "operation": "apply", "input": { "documentId": "tex.basic", "source": "E = mc^2", "displayMode": true }, "expect": { "stateEffects": ["cache-render-result", "emit-fallback-on-render-failure"], "receiptSchema": "gamecult.eve.plugin_receipt.v1" } }
]
}

View file

@ -0,0 +1,40 @@
{
"schema": "gamecult.eve.plugin.v1",
"pluginId": "tex.math",
"title": "TeX Math",
"owner": "EvePlugins",
"incubation": {
"ownerRepo": "EvePlugins",
"currentHostRepo": "EvePlugins",
"splitTarget": "EvePlugins/eve-plugin-tex",
"graduationTrigger": "Graduated: EvePlugins owns the executable TeX sidecar, fixtures, tests, and conformance witness."
},
"componentKinds": ["embed.tex"],
"commands": [],
"runtime": {
"invocationModel": "executable-sidecar",
"contract": "gamecult.eve.plugin_abi.v1",
"transports": ["stdio"],
"authority": ["renderer-independent", "no-provider-state-mutation", "provider-accepts-or-denies-commands"],
"sidecar": {
"processKind": "long-running-daemon",
"protocol": "stdio-ndjson",
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
"operations": ["describe", "validate", "project", "lower", "measure", "apply"],
"commandEnvelope": "gamecult.eve.command_invocation.v1",
"receiptSchema": "gamecult.eve.plugin_receipt.v1",
"stateAuthority": "emits-render-results-only-provider-owns-source-state"
}
},
"stateSchemas": ["tex.math.source.v1", "tex.math.render_request.v1", "tex.math.render_result.v1"],
"fixtures": [
{ "fixtureId": "tex-math-basic", "schema": "tex.math.source.v1", "path": "plugins/eve-plugin-tex/fixtures/basic-source.json" }
],
"abiFixtures": ["plugins/eve-plugin-tex/plugin-abi-fixture.json"],
"capabilities": {
"requires": ["embed.tex"],
"optionalCapabilities": ["tex.inline", "tex.block", "tex.scene-placement"]
}
}

View file

@ -0,0 +1,130 @@
import { createHash } from "node:crypto";
import katex from "katex";
export const pluginId = "tex.math";
export const operations = ["describe", "validate", "project", "lower", "measure", "apply"];
const renderCache = new Map();
export function dispatch(request) {
if (request?.schema !== "gamecult.eve.plugin_abi.request.v1") throw new Error("Unsupported request schema.");
if (request.pluginId !== pluginId) throw new Error(`Expected plugin ${pluginId}.`);
if (!operations.includes(request.operation)) throw new Error(`Unsupported operation ${request.operation}.`);
switch (request.operation) {
case "describe": return describe();
case "validate": return validate(request.input || {});
case "project": return project(request.input || {});
case "lower": return lower(request.input || {});
case "measure": return measure(request.input || {});
case "apply": return apply(request.input || {});
}
}
export function describe() {
return {
pluginId,
componentKinds: ["embed.tex"],
commands: [],
capabilities: ["embed.tex", "tex.inline", "tex.block"],
operations,
semanticEngine: "katex",
stateAuthority: "provider-owns-source-state",
};
}
export function validate(input) {
const requested = Array.isArray(input.componentKinds) ? input.componentKinds : [];
let validSource = true;
let diagnostic = null;
if (typeof input.source === "string") {
try {
render(input);
} catch (error) {
validSource = false;
diagnostic = error.message;
}
}
return {
acceptedComponentKinds: requested.filter((kind) => kind === "embed.tex"),
rejectedComponentKinds: requested.filter((kind) => kind !== "embed.tex"),
validSource,
...(diagnostic ? { diagnostic } : {}),
requiredCapabilities: ["embed.tex"],
};
}
export function project(input) {
return {
schema: "tex.math.render_request.v1",
projectionKind: "render-request",
ownedComponentKinds: ["embed.tex"],
outputSchemas: ["tex.math.render_request.v1", "tex.math.render_result.v1"],
documentId: requiredString(input.documentId, "documentId"),
source: requiredString(input.source, "source"),
displayMode: Boolean(input.displayMode),
macros: normalizeMacros(input.macros),
};
}
export function lower(input) {
const rendered = render(input);
return {
schema: "tex.math.render_result.v1",
loweringKind: "typeset-fragment",
preservedComponentKinds: ["embed.tex"],
requiredSchemas: ["tex.math.render_request.v1", "tex.math.render_result.v1"],
fallbackKind: "source-text",
documentId: requiredString(input.documentId, "documentId"),
source: input.source,
html: rendered.html,
contentHash: rendered.contentHash,
engine: "katex",
};
}
export function measure(input) {
render(input);
return {
measurementKind: "typeset-baseline-metrics",
measurementOutputs: ["baseline", "advance", "inkBounds", "fallbackBounds"],
measurementAuthority: "runtime-layout-required",
preservesProviderAuthority: true,
};
}
export function apply(input) {
const rendered = render(input);
renderCache.set(rendered.contentHash, rendered);
return {
stateEffects: ["cache-render-result", "emit-fallback-on-render-failure"],
receiptSchema: "gamecult.eve.plugin_receipt.v1",
contentHash: rendered.contentHash,
cacheStatus: "stored",
providerStateMutated: false,
};
}
function render(input) {
const source = requiredString(input.source, "source");
const macros = normalizeMacros(input.macros);
const contentHash = createHash("sha256").update(JSON.stringify({ source, macros, displayMode: Boolean(input.displayMode) })).digest("hex");
const cached = renderCache.get(contentHash);
if (cached) return cached;
return {
contentHash,
html: katex.renderToString(source, { displayMode: Boolean(input.displayMode), macros, throwOnError: true, output: "htmlAndMathml" }),
};
}
function normalizeMacros(value) {
if (value == null) return {};
if (typeof value !== "object" || Array.isArray(value)) throw new Error("macros must be an object.");
return Object.fromEntries(Object.entries(value).map(([key, expansion]) => [key, requiredString(expansion, `macro ${key}`)]));
}
function requiredString(value, field) {
if (typeof value !== "string" || !value.trim()) throw new Error(`${field} is required.`);
return value;
}

View file

@ -0,0 +1,28 @@
#!/usr/bin/env node
import readline from "node:readline";
import { dispatch, pluginId } from "./plugin.mjs";
readline.createInterface({ input: process.stdin }).on("line", (line) => {
if (!line.trim()) return;
let request;
try {
request = JSON.parse(line.replace(/^\uFEFF/, ""));
respond(request, dispatch(request));
} catch (error) {
respond(request, {}, "rejected", error.message);
}
});
function respond(request, output, status = "accepted", message) {
process.stdout.write(`${JSON.stringify({
schema: "gamecult.eve.plugin_abi.response.v1",
pluginId,
operation: request?.operation || "unknown",
requestId: request?.requestId || "unknown",
status,
output,
...(message ? { diagnostics: [{ message }] } : {}),
})}\n`);
}

View file

@ -0,0 +1,28 @@
import assert from "node:assert/strict";
import test from "node:test";
import { dispatch } from "../src/plugin.mjs";
function request(operation, input = {}) {
return dispatch({ schema: "gamecult.eve.plugin_abi.request.v1", pluginId: "tex.math", operation, requestId: operation, input });
}
test("typesets TeX through KaTeX without taking provider authority", () => {
const result = request("lower", { documentId: "basic", source: "E = mc^2", displayMode: true });
assert.equal(result.schema, "tex.math.render_result.v1");
assert.equal(result.engine, "katex");
assert.match(result.html, /katex/);
});
test("validates owned components and rejects invalid TeX", () => {
assert.deepEqual(request("validate", { componentKinds: ["embed.tex", "embed.norn"], source: "x^2" }), {
acceptedComponentKinds: ["embed.tex"], rejectedComponentKinds: ["embed.norn"], validSource: true, requiredCapabilities: ["embed.tex"],
});
assert.equal(request("validate", { componentKinds: ["embed.tex"], source: "\\notacommand{" }).validSource, false);
});
test("caches render results without mutating provider state", () => {
const result = request("apply", { documentId: "basic", source: "E = mc^2" });
assert.equal(result.cacheStatus, "stored");
assert.equal(result.providerStateMutated, false);
});

View file

@ -0,0 +1,56 @@
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import readline from "node:readline";
const root = path.resolve(import.meta.dirname, "..");
const fixture = JSON.parse(fs.readFileSync(path.join(root, "plugins/eve-plugin-tex/plugin-abi-fixture.json"), "utf8"));
const child = spawn(process.execPath, [path.join(root, "plugins/eve-plugin-tex/src/sidecar.mjs")], { cwd: root, stdio: ["pipe", "pipe", "inherit"] });
const responses = readline.createInterface({ input: child.stdout })[Symbol.asyncIterator]();
try {
for (let index = 0; index < fixture.operations.length; index += 1) {
const operation = fixture.operations[index];
const requestId = `tex-witness-${index}`;
child.stdin.write(`${JSON.stringify({ schema: fixture.requestSchema, pluginId: fixture.pluginId, operation: operation.operation, requestId, input: operation.input })}\n`);
const next = await responses.next();
assert.equal(next.done, false);
const response = JSON.parse(next.value);
assert.equal(response.schema, fixture.responseSchema);
assert.equal(response.pluginId, fixture.pluginId);
assert.equal(response.operation, operation.operation);
assert.equal(response.requestId, requestId);
assert.equal(response.status, "accepted");
assertSubset(response.output, operation.expect, operation.operation);
}
} finally {
child.stdin.end();
child.kill();
}
const outputDir = path.join(root, "artifacts", "tex-math");
fs.mkdirSync(outputDir, { recursive: true });
const witness = {
schema: "gamecult.eve.plugin_witness.v1",
witnessId: "tex.math.owner-sidecar",
pluginId: "tex.math",
ownerRepo: "EvePlugins",
transport: "stdio-ndjson",
operations: fixture.operations.map(({ operation }) => operation),
status: "passed",
};
fs.writeFileSync(path.join(outputDir, "runtime-witness.json"), `${JSON.stringify(witness, null, 2)}\n`);
console.log(path.join(outputDir, "runtime-witness.json"));
function assertSubset(actual, expected, label) {
if (Array.isArray(expected)) {
assert.deepEqual(actual, expected, label);
return;
}
if (expected && typeof expected === "object") {
for (const [key, value] of Object.entries(expected)) assertSubset(actual?.[key], value, `${label}.${key}`);
return;
}
assert.equal(actual, expected, label);
}