Add independent semantic fields plugin

This commit is contained in:
Meta Vens 2026-07-10 14:50:47 +01:00
commit e562055bfe
12 changed files with 378 additions and 11 deletions

12
package-lock.json generated
View file

@ -31,6 +31,10 @@
"zod": "^3.25.76"
}
},
"node_modules/@gamecult/eve-plugin-fields": {
"resolved": "plugins/eve-plugin-fields",
"link": true
},
"node_modules/@gamecult/eve-plugin-tex": {
"resolved": "plugins/eve-plugin-tex",
"link": true
@ -73,6 +77,14 @@
"katex": "cli.js"
}
},
"plugins/eve-plugin-fields": {
"name": "@gamecult/eve-plugin-fields",
"version": "0.1.0",
"bin": {
"eve-plugin-fields": "src/sidecar.mjs",
"eve-plugin-fields-cultnet": "src/cultnet-sidecar.mjs"
}
},
"plugins/eve-plugin-tex": {
"name": "@gamecult/eve-plugin-tex",
"version": "0.1.0",

View file

@ -6,7 +6,8 @@
"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"
"witness:tex": "C:\\Progra~1\\nodejs\\node.exe scripts/run-tex-witness.mjs tex",
"witness:fields": "C:\\Progra~1\\nodejs\\node.exe scripts/run-tex-witness.mjs fields"
},
"dependencies": {
"@msgpack/msgpack": "^3.1.3",

View file

@ -0,0 +1,28 @@
{
"schema": "gamecult.eve.plugin_advertisement.v1",
"pluginId": "fields.surface",
"ownerService": "eveplugins",
"version": "0.1.0",
"manifestAddress": "file://plugins/eve-plugin-fields/plugin.json",
"runtime": {
"invocationModel": "executable-sidecar",
"contract": "gamecult.eve.plugin_abi.v1",
"transports": ["cultnet-rudp", "stdio"],
"authority": ["renderer-independent", "no-provider-state-mutation", "provider-accepts-or-denies-commands"],
"sidecar": {
"processKind": "long-running-daemon",
"protocol": "cultnet-operation-v0",
"debugProtocol": "stdio-ndjson",
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
"operations": ["describe", "validate", "project", "measure"],
"commandEnvelope": "gamecult.eve.command_invocation.v1",
"receiptSchema": "gamecult.eve.plugin_receipt.v1",
"stateAuthority": "projects-provider-owned-field-documents-without-mutating-them"
}
},
"schemas": ["gamecult.fields.surface2d.v1", "gamecult.fields.scalar.v1", "gamecult.fields.vector.v1", "gamecult.fields.objects.v1"],
"componentKinds": ["field.surface2d", "gravity.surface"],
"commands": [],
"fixtures": ["fields-surface-basic"]
}

View file

@ -0,0 +1,10 @@
{
"schema": "gamecult.fields.surface2d.v1",
"fieldId": "fixture.field",
"bounds": { "minX": -10, "minY": -10, "maxX": 10, "maxY": 10 },
"channels": [
{ "channelId": "height", "kind": "scalar", "documentId": "cultmesh://fixture/fields/height", "schemaId": "gamecult.fields.scalar.v1" },
{ "channelId": "flow", "kind": "vector", "documentId": "cultmesh://fixture/fields/flow", "schemaId": "gamecult.fields.vector.v1" }
],
"objects": { "documentId": "cultmesh://fixture/fields/objects", "schemaId": "gamecult.fields.objects.v1" }
}

View file

@ -0,0 +1,10 @@
{
"name": "@gamecult/eve-plugin-fields",
"version": "0.1.0",
"type": "module",
"private": true,
"bin": {
"eve-plugin-fields": "src/sidecar.mjs",
"eve-plugin-fields-cultnet": "src/cultnet-sidecar.mjs"
}
}

View file

@ -0,0 +1,16 @@
{
"schema": "gamecult.eve.plugin_abi_fixture.v1",
"fixtureId": "fields-surface-live-sidecar",
"pluginId": "fields.surface",
"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 field semantics; providers own sampled field state and runtimes own native projection.",
"operations": [
{ "operation": "describe", "input": {}, "expect": { "pluginId": "fields.surface", "componentKinds": ["field.surface2d", "gravity.surface"], "commands": [] } },
{ "operation": "validate", "input": { "componentKinds": ["field.surface2d", "world.scene3d"], "document": { "schema": "gamecult.fields.surface2d.v1", "bounds": { "minX": -1, "minY": -1, "maxX": 1, "maxY": 1 } } }, "expect": { "acceptedComponentKinds": ["field.surface2d"], "rejectedComponentKinds": ["world.scene3d"], "validDocument": true } },
{ "operation": "project", "input": { "document": { "schema": "gamecult.fields.surface2d.v1", "fieldId": "fixture.field", "bounds": { "minX": -1, "minY": -1, "maxX": 1, "maxY": 1 }, "channels": [{ "channelId": "height", "kind": "scalar", "documentId": "cultmesh://fixture/height", "schemaId": "gamecult.fields.scalar.v1" }] } }, "expect": { "projectionKind": "semantic-field-surface", "ownedComponentKinds": ["field.surface2d", "gravity.surface"], "providerStateMutated": false } },
{ "operation": "measure", "input": { "document": { "schema": "gamecult.fields.surface2d.v1", "fieldId": "fixture.field", "bounds": { "minX": -1, "minY": -2, "maxX": 3, "maxY": 2 }, "channels": [] } }, "expect": { "measurementKind": "field-domain-metrics", "width": 4, "height": 4, "preservesProviderAuthority": true } }
]
}

View file

@ -0,0 +1,40 @@
{
"schema": "gamecult.eve.plugin.v1",
"pluginId": "fields.surface",
"title": "Semantic Fields",
"owner": "EvePlugins",
"incubation": {
"ownerRepo": "EvePlugins",
"currentHostRepo": "EvePlugins",
"splitTarget": "EvePlugins/eve-plugin-fields",
"graduationTrigger": "Split when field schemas, sidecar witness, and two runtime adapters are stable."
},
"componentKinds": ["field.surface2d", "gravity.surface"],
"commands": [],
"runtime": {
"invocationModel": "executable-sidecar",
"contract": "gamecult.eve.plugin_abi.v1",
"transports": ["cultnet-rudp", "stdio"],
"authority": ["renderer-independent", "no-provider-state-mutation", "provider-accepts-or-denies-commands"],
"sidecar": {
"processKind": "long-running-daemon",
"protocol": "cultnet-operation-v0",
"debugProtocol": "stdio-ndjson",
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
"operations": ["describe", "validate", "project", "measure"],
"commandEnvelope": "gamecult.eve.command_invocation.v1",
"receiptSchema": "gamecult.eve.plugin_receipt.v1",
"stateAuthority": "projects-provider-owned-field-documents-without-mutating-them"
}
},
"stateSchemas": ["gamecult.fields.surface2d.v1", "gamecult.fields.scalar.v1", "gamecult.fields.vector.v1", "gamecult.fields.objects.v1"],
"fixtures": [
{ "fixtureId": "fields-surface-basic", "schema": "gamecult.fields.surface2d.v1", "path": "plugins/eve-plugin-fields/fixtures/basic-field.json" }
],
"abiFixtures": ["plugins/eve-plugin-fields/plugin-abi-fixture.json"],
"capabilities": {
"requires": ["field.surface2d"],
"optionalCapabilities": ["gravity.surface", "field.scalar", "field.vector", "field.objects"]
}
}

View file

@ -0,0 +1,62 @@
#!/usr/bin/env node
import { Buffer } from "node:buffer";
import { decode, encode } from "@msgpack/msgpack";
import { startCultNetOperationServer } from "cultnet-ts";
import { dispatch, pluginId } from "./plugin.mjs";
const host = process.env.EVE_PLUGIN_HOST || "127.0.0.1";
const port = Number(process.env.EVE_PLUGIN_PORT || 0);
const runtimeId = "eve-plugin-fields";
const server = await startCultNetOperationServer({
runtimeId,
host,
port,
handler: request => {
if (request.serviceId !== pluginId) throw new Error(`Expected service ${pluginId}.`);
if (request.payloadSchema !== "gamecult.eve.plugin_abi.request.v1") throw new Error("Unsupported plugin payload schema.");
const abiRequest = decode(Buffer.from(request.payload, "base64"));
let abiResponse;
try {
abiResponse = {
schema: "gamecult.eve.plugin_abi.response.v1",
pluginId,
operation: abiRequest.operation,
requestId: abiRequest.requestId,
status: "accepted",
output: dispatch(abiRequest),
};
} catch (error) {
abiResponse = {
schema: "gamecult.eve.plugin_abi.response.v1",
pluginId,
operation: abiRequest?.operation || request.operation,
requestId: abiRequest?.requestId || request.messageId,
status: "rejected",
output: {},
diagnostics: [{ message: error instanceof Error ? error.message : String(error) }],
};
}
return {
schemaVersion: "cultnet.operation_response.v0",
messageId: request.messageId,
serviceId: pluginId,
operation: request.operation,
status: abiResponse.status,
payloadSchema: abiResponse.schema,
payloadEncoding: "messagepack-base64",
payload: Buffer.from(encode(abiResponse)).toString("base64"),
diagnostics: (abiResponse.diagnostics || []).map(item => item.message),
sourceRuntimeId: runtimeId,
};
},
});
process.stdout.write(`${JSON.stringify({ schema: "gamecult.eve.plugin_endpoint.v1", pluginId, endpoint: server.endpoint })}\n`);
const stop = async () => {
await server.close();
process.exit(0);
};
process.once("SIGINT", stop);
process.once("SIGTERM", stop);
await new Promise(() => {});

View file

@ -0,0 +1,102 @@
export const pluginId = "fields.surface";
export const operations = ["describe", "validate", "project", "measure"];
export const componentKinds = ["field.surface2d", "gravity.surface"];
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 "measure": return measure(request.input || {});
}
}
export function describe() {
return {
pluginId,
componentKinds,
commands: [],
capabilities: ["field.surface2d", "gravity.surface", "field.scalar", "field.vector", "field.objects"],
operations,
stateAuthority: "provider-owns-field-documents",
};
}
export function validate(input) {
const requested = Array.isArray(input.componentKinds) ? input.componentKinds : [];
const document = input.document || {};
const diagnostics = [];
if (document.schema && document.schema !== "gamecult.fields.surface2d.v1") diagnostics.push(`Unsupported field schema ${document.schema}.`);
if (document.bounds && !validBounds(document.bounds)) diagnostics.push("Field bounds must have increasing finite axes.");
return {
acceptedComponentKinds: requested.filter(kind => componentKinds.includes(kind)),
rejectedComponentKinds: requested.filter(kind => !componentKinds.includes(kind)),
validDocument: diagnostics.length === 0,
diagnostics,
requiredCapabilities: ["field.surface2d"],
};
}
export function project(input) {
const document = requiredObject(input.document, "document");
if (document.schema !== "gamecult.fields.surface2d.v1") throw new Error("document must use gamecult.fields.surface2d.v1.");
if (!validBounds(document.bounds)) throw new Error("document bounds are invalid.");
const channels = Array.isArray(document.channels) ? document.channels.map(normalizeChannel) : [];
return {
schema: "gamecult.fields.projection.v1",
projectionKind: "semantic-field-surface",
ownedComponentKinds: componentKinds,
fieldId: requiredString(document.fieldId, "fieldId"),
bounds: { ...document.bounds },
channels,
objects: document.objects ? normalizeDocumentRef(document.objects, "objects") : null,
nativeProjectionAuthority: "runtime-adapter",
providerStateMutated: false,
};
}
export function measure(input) {
const projection = project(input);
return {
measurementKind: "field-domain-metrics",
width: projection.bounds.maxX - projection.bounds.minX,
height: projection.bounds.maxY - projection.bounds.minY,
channelCount: projection.channels.length,
preservesProviderAuthority: true,
};
}
function normalizeChannel(channel, index) {
const value = requiredObject(channel, `channels[${index}]`);
const kind = requiredString(value.kind, `channels[${index}].kind`);
if (!["scalar", "vector", "color", "mask"].includes(kind)) throw new Error(`Unsupported field channel kind ${kind}.`);
return {
channelId: requiredString(value.channelId, `channels[${index}].channelId`),
kind,
...normalizeDocumentRef(value, `channels[${index}]`),
};
}
function normalizeDocumentRef(value, field) {
return {
documentId: requiredString(value.documentId, `${field}.documentId`),
schemaId: requiredString(value.schemaId, `${field}.schemaId`),
};
}
function validBounds(value) {
return value && [value.minX, value.minY, value.maxX, value.maxY].every(Number.isFinite) && value.minX < value.maxX && value.minY < value.maxY;
}
function requiredObject(value, field) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${field} is required.`);
return value;
}
function requiredString(value, field) {
if (typeof value !== "string" || !value.trim()) throw new Error(`${field} is required.`);
return value;
}

View file

@ -0,0 +1,27 @@
#!/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 instanceof Error ? error.message : String(error));
}
});
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,41 @@
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: "fields.surface", operation, requestId: operation, input });
}
const document = {
schema: "gamecult.fields.surface2d.v1",
fieldId: "fixture.field",
bounds: { minX: -10, minY: -5, maxX: 10, maxY: 5 },
channels: [
{ channelId: "height", kind: "scalar", documentId: "cultmesh://fixture/height", schemaId: "gamecult.fields.scalar.v1" },
],
};
test("projects provider-owned field documents without taking native projection authority", () => {
const result = request("project", { document });
assert.equal(result.schema, "gamecult.fields.projection.v1");
assert.equal(result.projectionKind, "semantic-field-surface");
assert.equal(result.channels[0].documentId, "cultmesh://fixture/height");
assert.equal(result.nativeProjectionAuthority, "runtime-adapter");
assert.equal(result.providerStateMutated, false);
});
test("rejects components and schemas outside field ownership", () => {
const result = request("validate", { componentKinds: ["field.surface2d", "world.scene3d"], document });
assert.deepEqual(result.acceptedComponentKinds, ["field.surface2d"]);
assert.deepEqual(result.rejectedComponentKinds, ["world.scene3d"]);
assert.equal(result.validDocument, true);
assert.equal(request("validate", { componentKinds: ["field.surface2d"], document: { ...document, schema: "provider.private.v1" } }).validDocument, false);
});
test("measures field domains without mutating provider state", () => {
const result = request("measure", { document });
assert.equal(result.width, 20);
assert.equal(result.height, 10);
assert.equal(result.channelCount, 1);
assert.equal(result.preservesProviderAuthority, true);
});

View file

@ -8,8 +8,26 @@ import { decode, encode } from "@msgpack/msgpack";
import { invokeCultNetOperation } from "cultnet-ts";
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/cultnet-sidecar.mjs")], { cwd: root, stdio: ["ignore", "pipe", "inherit"] });
const target = process.argv[2] || "tex";
const targets = {
tex: {
directory: "eve-plugin-tex",
artifactDirectory: "tex-math",
witnessId: "tex.math.owner-sidecar",
authority: "plugin-sidecar-owns-tex-semantics-provider-retains-source-and-command-acceptance",
},
fields: {
directory: "eve-plugin-fields",
artifactDirectory: "fields-surface",
witnessId: "fields.surface.owner-sidecar",
authority: "plugin-sidecar-owns-field-semantics-provider-retains-field-state-and-runtime-retains-native-projection",
},
};
const config = targets[target];
if (!config) throw new Error(`Unknown plugin witness target '${target}'.`);
const pluginRoot = path.join(root, "plugins", config.directory);
const fixture = JSON.parse(fs.readFileSync(path.join(pluginRoot, "plugin-abi-fixture.json"), "utf8"));
const child = spawn(process.execPath, [path.join(pluginRoot, "src/cultnet-sidecar.mjs")], { cwd: root, stdio: ["ignore", "pipe", "inherit"] });
const responses = readline.createInterface({ input: child.stdout })[Symbol.asyncIterator]();
const endpointLine = await responses.next();
assert.equal(endpointLine.done, false);
@ -22,7 +40,7 @@ const operationWitnesses = [];
try {
for (let index = 0; index < fixture.operations.length; index += 1) {
const operation = fixture.operations[index];
const requestId = `tex-witness-${index}`;
const requestId = `${target}-witness-${index}`;
const operationStartedAt = performance.now();
const abiRequest = { schema: fixture.requestSchema, pluginId: fixture.pluginId, operation: operation.operation, requestId, input: operation.input };
const envelope = await invokeCultNetOperation(endpoint, {
@ -54,23 +72,23 @@ try {
child.kill();
}
const outputDir = path.join(root, "artifacts", "tex-math");
const outputDir = path.join(root, "artifacts", config.artifactDirectory);
fs.mkdirSync(outputDir, { recursive: true });
const witness = {
schema: "gamecult.eve.plugin_witness.v1",
witnessId: "tex.math.owner-sidecar",
pluginId: "tex.math",
witnessId: config.witnessId,
pluginId: fixture.pluginId,
ownerRepo: "EvePlugins",
status: "pass",
transport: "cultnet-operation-v0+rudp",
generatedAtUtc: new Date().toISOString(),
durationMs: Number((performance.now() - startedAt).toFixed(3)),
operations: operationWitnesses,
advertisementPath: "plugins/eve-plugin-tex/advertisement.json",
fixturePath: "plugins/eve-plugin-tex/plugin-abi-fixture.json",
executable: { artifact: "plugins/eve-plugin-tex/src/cultnet-sidecar.mjs", command: "node" },
advertisementPath: `plugins/${config.directory}/advertisement.json`,
fixturePath: `plugins/${config.directory}/plugin-abi-fixture.json`,
executable: { artifact: `plugins/${config.directory}/src/cultnet-sidecar.mjs`, command: "node" },
diagnostics: [],
authority: "plugin-sidecar-owns-tex-semantics-provider-retains-source-and-command-acceptance",
authority: config.authority,
};
fs.writeFileSync(path.join(outputDir, "runtime-witness.json"), `${JSON.stringify(witness, null, 2)}\n`);
console.log(path.join(outputDir, "runtime-witness.json"));