diff --git a/tools/eveunity/eveunity-capture-contract.mjs b/tools/eveunity/eveunity-capture-contract.mjs deleted file mode 100644 index 5766ceb..0000000 --- a/tools/eveunity/eveunity-capture-contract.mjs +++ /dev/null @@ -1,160 +0,0 @@ -import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -export function buildUnityCaptureRequest({ - advertisement, - capabilityManifest, - advertisementPath = "", - capabilityManifestPath = "", - captureSurfaceId = "", - stamp = new Date().toISOString().replace(/[:.]/g, "-"), -} = {}) { - if (!advertisement || typeof advertisement !== "object") { - throw new Error("advertisement is required"); - } - if (!capabilityManifest || typeof capabilityManifest !== "object") { - throw new Error("capabilityManifest is required"); - } - - const captureContract = capabilityManifest.lifecycle?.capture?.captureContract; - if (!captureContract) { - throw new Error("capability manifest is missing lifecycle.capture.captureContract"); - } - - const captureSurface = selectCaptureSurface(captureContract, advertisement.providerId, captureSurfaceId); - if (!captureSurface) { - if (captureSurfaceId) { - throw new Error( - `capture surface mismatch: expected ${captureSurfaceList(captureContract)} got ${advertisement.providerId || "missing"}:${captureSurfaceId}`, - ); - } - throw new Error( - `capture provider mismatch: expected ${captureProviderList(captureContract)} got ${advertisement.providerId || "missing"}`, - ); - } - - const targetId = captureContract.targetId || captureContract.runtimeId; - const surface = (advertisement.surfaces || []).find(candidate => candidate.surfaceId === captureSurface.surfaceId); - if (!surface) { - throw new Error(`capture surface not advertised: ${captureSurface.surfaceId}`); - } - - const loweringTargets = surface.worldInteraction?.loweringTargets || []; - if (!loweringTargets.includes(targetId)) { - throw new Error(`surface ${surface.surfaceId} does not advertise lowering target ${targetId}`); - } - - const artifactPath = replaceStamp(captureContract.artifactPattern, stamp); - - return { - schema: captureContract.requestSchema || "gamecult.eve.runtime_capture_request.v1", - runtimeId: capabilityManifest.runtimeId, - ownerRepo: captureContract.ownerRepo, - providerId: advertisement.providerId, - surfaceId: surface.surfaceId, - surfaceKind: surface.surfaceKind || "", - targetId, - projectionKind: surface.worldInteraction?.projectionKind || "", - commandBoundary: surface.worldInteraction?.commandBoundary || "", - receiptSchema: surface.worldInteraction?.receiptSchema || "", - captureKind: captureContract.captureKind, - artifactKind: captureContract.artifactKind, - artifactPath, - conformanceAttachment: captureContract.conformanceAttachment, - authority: captureContract.authority, - sourceAdvertisementPath: advertisementPath, - sourceCapabilityManifestPath: capabilityManifestPath, - issuedAtUtc: stamp, - }; -} - -export function loadJsonFile(filePath) { - return JSON.parse(readFileSync(filePath, "utf8")); -} - -export function writeCaptureRequest(request, outputPath) { - mkdirSync(path.dirname(outputPath), { recursive: true }); - writeFileSync(outputPath, `${JSON.stringify(request, null, 2)}\n`, "utf8"); -} - -function replaceStamp(pattern, stamp) { - if (!pattern || !pattern.includes("{stamp}")) { - throw new Error(`capture artifact pattern must include {stamp}: ${pattern || "missing"}`); - } - return pattern.replace("{stamp}", stamp); -} - -function selectCaptureSurface(captureContract, providerId, surfaceId = "") { - const claims = captureSurfaceClaims(captureContract).filter(claim => claim.providerId === providerId); - if (surfaceId) return claims.find(claim => claim.surfaceId === surfaceId) || null; - return claims[0] || null; -} - -function captureProviderList(captureContract) { - return captureSurfaceClaims(captureContract).map(claim => claim.providerId).filter(Boolean).join(", ") || "missing"; -} - -function captureSurfaceList(captureContract) { - return captureSurfaceClaims(captureContract).map(claim => `${claim.providerId}:${claim.surfaceId}`).join(", ") || "missing"; -} - -function captureSurfaceClaims(captureContract) { - return [ - { - providerId: captureContract.requiredProvider || "", - surfaceId: captureContract.requiredSurface || "", - }, - ...(Array.isArray(captureContract.additionalProviderSurfaces) ? captureContract.additionalProviderSurfaces : []), - ] - .filter(claim => claim && typeof claim === "object") - .map(claim => ({ - providerId: String(claim.providerId || ""), - surfaceId: String(claim.surfaceId || ""), - })) - .filter(claim => claim.providerId && claim.surfaceId); -} - -function parseArgs(argv) { - const args = {}; - for (let index = 0; index < argv.length; index += 1) { - const value = argv[index]; - if (!value.startsWith("--")) continue; - const key = value.slice(2); - const next = argv[index + 1]; - if (!next || next.startsWith("--")) { - args[key] = true; - } else { - args[key] = next; - index += 1; - } - } - return args; -} - -function runCli() { - const args = parseArgs(process.argv.slice(2)); - for (const required of ["advertisement", "capability", "output"]) { - if (!args[required]) { - throw new Error(`missing required --${required}`); - } - } - - const stamp = args.stamp || new Date().toISOString().replace(/[:.]/g, "-"); - const request = buildUnityCaptureRequest({ - advertisement: loadJsonFile(args.advertisement), - capabilityManifest: loadJsonFile(args.capability), - advertisementPath: args.advertisement, - capabilityManifestPath: args.capability, - captureSurfaceId: args["surface-id"] || "", - stamp, - }); - writeCaptureRequest(request, args.output); - process.stdout.write(`${args.output}\n`); -} - -const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : ""; -const modulePath = fileURLToPath(import.meta.url); -if (invokedPath === modulePath) { - runCli(); -} diff --git a/tools/eveunity/eveunity-capture-contract.test.mjs b/tools/eveunity/eveunity-capture-contract.test.mjs deleted file mode 100644 index 816c0f0..0000000 --- a/tools/eveunity/eveunity-capture-contract.test.mjs +++ /dev/null @@ -1,76 +0,0 @@ -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import test from "node:test"; -import { buildUnityCaptureRequest } from "./eveunity-capture-contract.mjs"; - -const advertisement = JSON.parse(readFileSync(new URL("../../web/fixtures/aetheria.provider-advertisement.json", import.meta.url), "utf8")); -const worldSmokeAdvertisement = JSON.parse(readFileSync(new URL("../../web/fixtures/eve-world-smoke.provider-advertisement.json", import.meta.url), "utf8")); -const capabilityManifest = JSON.parse(readFileSync(new URL("../../packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json", import.meta.url), "utf8")); - -test("builds Unity UI Toolkit capture request from provider advertisement", () => { - const request = buildUnityCaptureRequest({ - advertisement, - capabilityManifest, - advertisementPath: "web/fixtures/aetheria.provider-advertisement.json", - capabilityManifestPath: "packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json", - stamp: "smoke", - }); - - assert.equal(request.schema, "gamecult.eve.runtime_capture_request.v1"); - assert.equal(request.runtimeId, "unity-uitoolkit"); - assert.equal(request.ownerRepo, "EveUnity"); - assert.equal(request.providerId, "aetheria"); - assert.equal(request.surfaceId, "aetheria.daemon.game"); - assert.equal(request.targetId, "unity-uitoolkit"); - assert.equal(request.projectionKind, "provider-authored-world-surface"); - assert.equal(request.commandBoundary, "aetheria.daemon.commands"); - assert.equal(request.receiptSchema, "aetheria.eve_command_acceptance_status.v1"); - assert.equal(request.artifactPath, "artifacts/eveunity-uitoolkit-capture/smoke/unity-uitoolkit.png"); - assert.match(request.authority, /provider state/); -}); - -test("builds Unity UI Toolkit capture request for Aetheria editor surface", () => { - const request = buildUnityCaptureRequest({ - advertisement, - capabilityManifest, - captureSurfaceId: "aetheria.daemon.editor", - advertisementPath: "web/fixtures/aetheria.provider-advertisement.json", - capabilityManifestPath: "packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json", - stamp: "smoke", - }); - - assert.equal(request.runtimeId, "unity-uitoolkit"); - assert.equal(request.providerId, "aetheria"); - assert.equal(request.surfaceId, "aetheria.daemon.editor"); - assert.equal(request.surfaceKind, "interactive-world-editor"); - assert.equal(request.projectionKind, "provider-authored-world-editor-surface"); - assert.equal(request.commandBoundary, "aetheria.daemon.commands"); - assert.equal(request.receiptSchema, "aetheria.eve_command_acceptance_status.v1"); -}); - -test("builds Unity UI Toolkit capture request from a declared generic world provider", () => { - const request = buildUnityCaptureRequest({ - advertisement: worldSmokeAdvertisement, - capabilityManifest, - advertisementPath: "web/fixtures/eve-world-smoke.provider-advertisement.json", - capabilityManifestPath: "packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json", - stamp: "smoke", - }); - - assert.equal(request.runtimeId, "unity-uitoolkit"); - assert.equal(request.providerId, "eve.world-smoke"); - assert.equal(request.surfaceId, "eve.world-smoke.surface"); - assert.equal(request.commandBoundary, "eve.world-smoke.commands"); - assert.equal(request.receiptSchema, "eve.world_smoke.command_receipt.v1"); -}); - -test("rejects a capture request when the surface does not advertise the runtime target", () => { - const withoutTarget = structuredClone(advertisement); - const surface = withoutTarget.surfaces.find(candidate => candidate.surfaceId === "aetheria.daemon.game"); - surface.worldInteraction.loweringTargets = surface.worldInteraction.loweringTargets.filter(target => target !== "unity-uitoolkit"); - - assert.throws( - () => buildUnityCaptureRequest({ advertisement: withoutTarget, capabilityManifest, stamp: "smoke" }), - /does not advertise lowering target unity-uitoolkit/, - ); -}); diff --git a/tools/eveunity/eveunity-release-artifact.mjs b/tools/eveunity/eveunity-release-artifact.mjs deleted file mode 100644 index 0dbecc9..0000000 --- a/tools/eveunity/eveunity-release-artifact.mjs +++ /dev/null @@ -1,164 +0,0 @@ -import { spawnSync } from "node:child_process"; -import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -export function buildUnityReleaseArtifactPlan({ request, packageManifest } = {}) { - if (!request || typeof request !== "object") { - throw new Error("release request is required"); - } - if (!packageManifest || typeof packageManifest !== "object") { - throw new Error("package manifest is required"); - } - if (request.artifactKind !== "upm-package") { - throw new Error(`release artifact kind must be upm-package: ${request.artifactKind || "missing"}`); - } - if (packageManifest.name !== request.packageName) { - throw new Error(`package name mismatch: expected ${request.packageName || "missing"} got ${packageManifest.name || "missing"}`); - } - if (packageManifest.version !== request.version) { - throw new Error(`package version mismatch: expected ${request.version || "missing"} got ${packageManifest.version || "missing"}`); - } - if (!request.packageRoot) { - throw new Error("release request missing packageRoot"); - } - if (!request.artifactPath || !request.artifactPath.endsWith(".tgz")) { - throw new Error(`release artifact path must be a .tgz file: ${request.artifactPath || "missing"}`); - } - - const artifactDirectory = path.dirname(request.artifactPath); - const expectedFileName = path.basename(request.artifactPath); - return { - packageRoot: request.packageRoot, - artifactPath: request.artifactPath, - artifactDirectory, - expectedFileName, - npmPackArguments: ["pack", request.packageRoot, "--pack-destination", artifactDirectory], - }; -} - -export function packUnityReleaseArtifact({ - request, - projectRoot = process.cwd(), - npmCommand = "", -} = {}) { - const packageRoot = resolveProjectPath(projectRoot, request?.packageRoot || ""); - const packageManifestPath = path.join(packageRoot, "package.json"); - if (!existsSync(packageManifestPath)) { - throw new Error(`release package manifest not found: ${packageManifestPath}`); - } - - const packageManifest = loadJsonFile(packageManifestPath); - const plan = buildUnityReleaseArtifactPlan({ request, packageManifest }); - const artifactDirectory = resolveProjectPath(projectRoot, plan.artifactDirectory); - const artifactPath = resolveProjectPath(projectRoot, plan.artifactPath); - mkdirSync(artifactDirectory, { recursive: true }); - - const npmInvocation = resolveNpmInvocation(npmCommand); - const packArguments = ["pack", packageRoot, "--pack-destination", artifactDirectory]; - const result = spawnSync(npmInvocation.command, [...npmInvocation.prefixArguments, ...packArguments], { - cwd: projectRoot, - encoding: "utf8", - shell: npmInvocation.shell, - }); - if (result.status !== 0) { - throw new Error(`npm pack failed with exit code ${result.status ?? "spawn-error"}: ${result.error?.message || result.stderr || result.stdout || "no output"}`); - } - - const packedFileName = result.stdout.trim().split(/\r?\n/).filter(Boolean).pop() || plan.expectedFileName; - const packedPath = path.join(artifactDirectory, packedFileName); - if (!existsSync(packedPath)) { - throw new Error(`npm pack did not produce expected output: ${packedPath}`); - } - if (path.resolve(packedPath) !== path.resolve(artifactPath)) { - throw new Error(`npm pack output mismatch: expected ${artifactPath} got ${packedPath}`); - } - - const stats = statSync(artifactPath); - if (stats.size <= 0) { - throw new Error(`release artifact is empty: ${artifactPath}`); - } - - return { - schema: "gamecult.eve.runtime_release_artifact.v1", - ownerRepo: request.ownerRepo, - repository: request.repository, - packageName: request.packageName, - version: request.version, - artifactKind: request.artifactKind, - artifactPath: request.artifactPath, - artifactSizeBytes: stats.size, - packageRoot: request.packageRoot, - sourceRequestPath: request.sourceRequestPath || "", - }; -} - -export function loadJsonFile(filePath) { - return JSON.parse(readFileSync(filePath, "utf8")); -} - -export function writeJsonFile(document, outputPath) { - mkdirSync(path.dirname(outputPath), { recursive: true }); - writeFileSync(outputPath, `${JSON.stringify(document, null, 2)}\n`, "utf8"); -} - -function resolveProjectPath(projectRoot, filePath) { - if (!filePath) return projectRoot; - return path.isAbsolute(filePath) ? filePath : path.resolve(projectRoot, filePath); -} - -function resolveNpmInvocation(npmCommand) { - if (npmCommand) { - return { command: npmCommand, prefixArguments: [], shell: false }; - } - - const npmCliPath = path.join(path.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"); - if (existsSync(npmCliPath)) { - return { command: process.execPath, prefixArguments: [npmCliPath], shell: false }; - } - - return { command: "npm", prefixArguments: [], shell: true }; -} - -function parseArgs(argv) { - const args = {}; - for (let index = 0; index < argv.length; index += 1) { - const value = argv[index]; - if (!value.startsWith("--")) continue; - const key = value.slice(2); - const next = argv[index + 1]; - if (!next || next.startsWith("--")) { - args[key] = true; - } else { - args[key] = next; - index += 1; - } - } - return args; -} - -function runCli() { - const args = parseArgs(process.argv.slice(2)); - for (const required of ["request", "proof-output"]) { - if (!args[required]) { - throw new Error(`missing required --${required}`); - } - } - - const projectRoot = args["project-root"] ? path.resolve(args["project-root"]) : process.cwd(); - const requestPath = resolveProjectPath(projectRoot, args.request); - const request = { - ...loadJsonFile(requestPath), - sourceRequestPath: args.request, - }; - const proof = packUnityReleaseArtifact({ request, projectRoot }); - const proofOutputPath = resolveProjectPath(projectRoot, args["proof-output"]); - writeJsonFile(proof, proofOutputPath); - process.stdout.write(`${proof.artifactPath}\n`); -} - -const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : ""; -const modulePath = fileURLToPath(import.meta.url); -if (invokedPath === modulePath) { - runCli(); -} diff --git a/tools/eveunity/eveunity-release-artifact.test.mjs b/tools/eveunity/eveunity-release-artifact.test.mjs deleted file mode 100644 index 300dc70..0000000 --- a/tools/eveunity/eveunity-release-artifact.test.mjs +++ /dev/null @@ -1,42 +0,0 @@ -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import test from "node:test"; -import { buildUnityReleaseRequest } from "./eveunity-release-contract.mjs"; -import { buildUnityReleaseArtifactPlan } from "./eveunity-release-artifact.mjs"; - -const capabilityManifest = JSON.parse(readFileSync(new URL("../../packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json", import.meta.url), "utf8")); -const packageManifest = JSON.parse(readFileSync(new URL("../../packages/org.gamecult.eve.unity-uitoolkit/package.json", import.meta.url), "utf8")); - -test("plans npm pack for the EveUnity UI Toolkit UPM artifact", () => { - const request = buildUnityReleaseRequest({ - capabilityManifest, - packageManifest, - capabilityManifestPath: "packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json", - packageManifestPath: "packages/org.gamecult.eve.unity-uitoolkit/package.json", - }); - const plan = buildUnityReleaseArtifactPlan({ request, packageManifest }); - - assert.equal(plan.packageRoot, "packages/org.gamecult.eve.unity-uitoolkit"); - assert.equal(plan.artifactPath, "artifacts/eveunity-uitoolkit-release/0.1.0/org.gamecult.eve.unity-uitoolkit-0.1.0.tgz"); - assert.equal(plan.artifactDirectory, "artifacts/eveunity-uitoolkit-release/0.1.0"); - assert.equal(plan.expectedFileName, "org.gamecult.eve.unity-uitoolkit-0.1.0.tgz"); - assert.deepEqual(plan.npmPackArguments, [ - "pack", - "packages/org.gamecult.eve.unity-uitoolkit", - "--pack-destination", - "artifacts/eveunity-uitoolkit-release/0.1.0", - ]); -}); - - -test("rejects release artifact plans for mismatched package identity", () => { - const request = buildUnityReleaseRequest({ capabilityManifest, packageManifest }); - - assert.throws( - () => buildUnityReleaseArtifactPlan({ - request, - packageManifest: { ...packageManifest, name: "org.gamecult.eve.wrong" }, - }), - /package name mismatch/, - ); -}); diff --git a/tools/eveunity/eveunity-release-contract.mjs b/tools/eveunity/eveunity-release-contract.mjs deleted file mode 100644 index 44a60eb..0000000 --- a/tools/eveunity/eveunity-release-contract.mjs +++ /dev/null @@ -1,130 +0,0 @@ -import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -export function buildUnityReleaseRequest({ - capabilityManifest, - packageManifest, - capabilityManifestPath = "", - packageManifestPath = "", - repository = "GameCult/EveUnity", -} = {}) { - if (!capabilityManifest || typeof capabilityManifest !== "object") { - throw new Error("capabilityManifest is required"); - } - if (!packageManifest || typeof packageManifest !== "object") { - throw new Error("packageManifest is required"); - } - - const releaseContract = capabilityManifest.lifecycle?.release?.releaseContract; - if (!releaseContract) { - throw new Error("capability manifest is missing lifecycle.release.releaseContract"); - } - - if (packageManifest.name !== releaseContract.packageName) { - throw new Error(`package name mismatch: expected ${releaseContract.packageName} got ${packageManifest.name || "missing"}`); - } - if (!packageManifest.version || !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(packageManifest.version)) { - throw new Error(`package version is not a SemVer-compatible release version: ${packageManifest.version || "missing"}`); - } - const requiredPackageDependencies = Array.isArray(releaseContract.requiredPackageDependencies) - ? releaseContract.requiredPackageDependencies - : [{ packageName: "org.gamecult.eve.surface", version: "0.1.0", packageManager: "upm", ownerRepo: "Eve" }]; - const packageDependencies = packageManifest.dependencies || {}; - for (const dependency of requiredPackageDependencies) { - if (!dependency?.packageName) { - throw new Error("release contract requiredPackageDependencies entries must name packageName"); - } - const actualVersion = packageDependencies[dependency.packageName]; - if (!actualVersion) { - throw new Error(`${releaseContract.packageName} must depend on ${dependency.packageName}`); - } - if (dependency.version && actualVersion !== dependency.version) { - throw new Error(`${releaseContract.packageName} dependency ${dependency.packageName} expected ${dependency.version} got ${actualVersion}`); - } - } - - const tagName = replaceVersion(releaseContract.tagPattern, packageManifest.version); - const artifactPath = replaceVersion( - releaseContract.artifactPattern || `artifacts/eveunity-uitoolkit-release/{version}/${releaseContract.packageName}-{version}.tgz`, - packageManifest.version, - ); - - return { - schema: releaseContract.requestSchema || "gamecult.eve.runtime_release_request.v1", - ownerRepo: releaseContract.ownerRepo, - repository, - packageName: releaseContract.packageName, - displayName: packageManifest.displayName || "", - version: packageManifest.version, - unity: packageManifest.unity || "", - packageRoot: releaseContract.packageRoot, - versionSource: releaseContract.versionSource, - tagName, - artifactKind: releaseContract.artifactKind, - artifactPath, - publishProof: releaseContract.publishProof, - sourceCapabilityManifestPath: capabilityManifestPath, - sourcePackageManifestPath: packageManifestPath, - dependencies: packageManifest.dependencies || {}, - requiredPackageDependencies, - }; -} - -export function loadJsonFile(filePath) { - return JSON.parse(readFileSync(filePath, "utf8")); -} - -export function writeReleaseRequest(request, outputPath) { - mkdirSync(path.dirname(outputPath), { recursive: true }); - writeFileSync(outputPath, `${JSON.stringify(request, null, 2)}\n`, "utf8"); -} - -function replaceVersion(pattern, version) { - if (!pattern || !pattern.includes("{version}")) { - throw new Error(`release pattern must include {version}: ${pattern || "missing"}`); - } - return pattern.replaceAll("{version}", version); -} - -function parseArgs(argv) { - const args = {}; - for (let index = 0; index < argv.length; index += 1) { - const value = argv[index]; - if (!value.startsWith("--")) continue; - const key = value.slice(2); - const next = argv[index + 1]; - if (!next || next.startsWith("--")) { - args[key] = true; - } else { - args[key] = next; - index += 1; - } - } - return args; -} - -function runCli() { - const args = parseArgs(process.argv.slice(2)); - for (const required of ["capability", "package", "output"]) { - if (!args[required]) { - throw new Error(`missing required --${required}`); - } - } - - const request = buildUnityReleaseRequest({ - capabilityManifest: loadJsonFile(args.capability), - packageManifest: loadJsonFile(args.package), - capabilityManifestPath: args.capability, - packageManifestPath: args.package, - repository: args.repository || "GameCult/EveUnity", - }); - writeReleaseRequest(request, args.output); - process.stdout.write(`${args.output}\n`); -} - -const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : ""; -const modulePath = fileURLToPath(import.meta.url); -if (invokedPath === modulePath) { - runCli(); -} diff --git a/tools/eveunity/eveunity-release-contract.test.mjs b/tools/eveunity/eveunity-release-contract.test.mjs deleted file mode 100644 index 3ee5570..0000000 --- a/tools/eveunity/eveunity-release-contract.test.mjs +++ /dev/null @@ -1,55 +0,0 @@ -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import test from "node:test"; -import { buildUnityReleaseRequest } from "./eveunity-release-contract.mjs"; - -const capabilityManifest = JSON.parse(readFileSync(new URL("../../packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json", import.meta.url), "utf8")); -const packageManifest = JSON.parse(readFileSync(new URL("../../packages/org.gamecult.eve.unity-uitoolkit/package.json", import.meta.url), "utf8")); - -test("builds EveUnity UPM release request from package manifest", () => { - const request = buildUnityReleaseRequest({ - capabilityManifest, - packageManifest, - capabilityManifestPath: "packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json", - packageManifestPath: "packages/org.gamecult.eve.unity-uitoolkit/package.json", - }); - - assert.equal(request.schema, "gamecult.eve.runtime_release_request.v1"); - assert.equal(request.ownerRepo, "EveUnity"); - assert.equal(request.repository, "GameCult/EveUnity"); - assert.equal(request.packageName, "org.gamecult.eve.unity-uitoolkit"); - assert.equal(request.version, "0.1.0"); - assert.equal(request.packageRoot, "packages/org.gamecult.eve.unity-uitoolkit"); - assert.equal(request.versionSource, "packages/org.gamecult.eve.unity-uitoolkit/package.json"); - assert.equal(request.tagName, "eveunity-uitoolkit-v0.1.0"); - assert.equal(request.artifactKind, "upm-package"); - assert.equal(request.artifactPath, "artifacts/eveunity-uitoolkit-release/0.1.0/org.gamecult.eve.unity-uitoolkit-0.1.0.tgz"); - assert.equal(request.dependencies["org.gamecult.eve.surface"], "0.1.0"); - assert.deepEqual(request.requiredPackageDependencies, [ - { - packageName: "org.gamecult.eve.surface", - version: "0.1.0", - packageManager: "upm", - ownerRepo: "Eve", - purpose: "Shared Unity DTOs for gamecult.eve.surface.v1 documents.", - }, - ]); -}); - -test("rejects package manifests that do not match the release contract package", () => { - const wrongPackage = { ...packageManifest, name: "org.gamecult.eve.wrong" }; - - assert.throws( - () => buildUnityReleaseRequest({ capabilityManifest, packageManifest: wrongPackage }), - /package name mismatch/, - ); -}); - -test("rejects package manifests missing required UPM dependencies", () => { - const missingDependencyPackage = { ...packageManifest, dependencies: {} }; - - assert.throws( - () => buildUnityReleaseRequest({ capabilityManifest, packageManifest: missingDependencyPackage }), - /must depend on org\.gamecult\.eve\.surface/, - ); -}); diff --git a/tools/eveunity/eveunity-uitoolkit-capture-artifact.mjs b/tools/eveunity/eveunity-uitoolkit-capture-artifact.mjs deleted file mode 100644 index b20bf15..0000000 --- a/tools/eveunity/eveunity-uitoolkit-capture-artifact.mjs +++ /dev/null @@ -1,327 +0,0 @@ -import { mkdirSync, writeFileSync } from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { buildUnityCaptureRequest, loadJsonFile, writeCaptureRequest } from "./eveunity-capture-contract.mjs"; - -export function buildUnityUiToolkitCaptureArtifact({ - advertisement, - capabilityManifest, - surfaceDocument, - advertisementPath = "", - capabilityManifestPath = "", - captureSurfaceId = "", - stamp = "latest", -} = {}) { - const request = buildUnityCaptureRequest({ - advertisement, - capabilityManifest, - advertisementPath, - capabilityManifestPath, - captureSurfaceId, - stamp, - }); - const projection = buildUnityUiToolkitProjection(surfaceDocument, advertisement, request.surfaceId); - validateProjectionArtifact(projection, request); - - return { - request, - projection: { - ...projection, - artifactKind: "json-projection", - captureKind: "unity-uitoolkit-projection-json", - sourceAdvertisementPath: request.sourceAdvertisementPath, - sourceCapabilityManifestPath: request.sourceCapabilityManifestPath, - issuedAtUtc: request.issuedAtUtc, - }, - }; -} - -export function buildUnityUiToolkitProjection(surfaceDocument, providerAdvertisement, requestedSurfaceId = "") { - const selected = selectSurface(providerAdvertisement, requestedSurfaceId); - const document = normalizeSurfaceDocument(surfaceDocument); - if (document.surfaceId !== selected.surfaceId) { - throw new Error(`Surface document ${document.surfaceId} does not match advertised Unity UI Toolkit surface ${selected.surfaceId}.`); - } - - return { - type: "unity-uitoolkit-projection", - schema: "gamecult.eve.unity_uitoolkit_projection.v1", - runtimeId: "unity-uitoolkit", - providerId: selected.providerId, - surfaceId: selected.surfaceId, - projectionKind: selected.worldInteraction.projectionKind, - commandBoundary: selected.worldInteraction.commandBoundary, - receiptSchema: selected.worldInteraction.receiptSchema, - ownership: selected.worldInteraction.ownership, - root: buildUiNode(document.root), - }; -} - -export function writeUnityUiToolkitCaptureArtifact({ request, projection }, { outputPath, requestOutputPath = "" } = {}) { - if (!outputPath) throw new Error("outputPath is required"); - mkdirSync(path.dirname(outputPath), { recursive: true }); - writeFileSync(outputPath, `${JSON.stringify(projection, null, 2)}\n`, "utf8"); - if (requestOutputPath) { - writeCaptureRequest(request, requestOutputPath); - } -} - -function selectSurface(providerAdvertisement, requestedSurfaceId = "") { - const provider = normalizeProviderAdvertisement(providerAdvertisement); - const surface = requestedSurfaceId - ? provider.surfaces.find(candidate => candidate.surfaceId === requestedSurfaceId) - : provider.surfaces[0]; - - if (!surface) { - throw new Error(`Provider ${provider.providerId} does not advertise surface ${requestedSurfaceId || "(first)"}`); - } - - return { - providerId: provider.providerId, - surfaceId: surface.surfaceId, - worldInteraction: normalizeWorldInteraction(surface.worldInteraction), - }; -} - -function normalizeProviderAdvertisement(providerAdvertisement) { - if (!providerAdvertisement || typeof providerAdvertisement !== "object") { - throw new Error("Provider advertisement is required."); - } - const providerId = firstString(providerAdvertisement.providerId); - if (!providerId) throw new Error("Provider advertisement missing providerId."); - const surfaces = Array.isArray(providerAdvertisement.surfaces) - ? providerAdvertisement.surfaces - .filter(surface => surface && typeof surface === "object" && surface.surfaceId) - .map(surface => ({ - ...surface, - surfaceId: String(surface.surfaceId), - })) - : []; - if (!surfaces.length) throw new Error(`Provider ${providerId} advertises no surfaces.`); - return { ...providerAdvertisement, providerId, surfaces }; -} - -function normalizeSurfaceDocument(surfaceDocument) { - if (!surfaceDocument || typeof surfaceDocument !== "object") { - throw new Error("Surface document is required."); - } - if (surfaceDocument.schema !== "gamecult.eve.surface.v1") { - throw new Error(`Unexpected surface schema: ${surfaceDocument.schema || ""}`); - } - const surface = objectValue(surfaceDocument.surface); - const surfaceId = firstString(surface.id); - if (!surfaceId) throw new Error("Surface document missing surface.id."); - const root = objectValue(surface.root); - if (!root.id) throw new Error(`Surface document ${surfaceId} missing surface.root.`); - return { ...surfaceDocument, surfaceId, root }; -} - -function buildUiNode(component) { - const source = objectValue(component); - const children = Array.isArray(source.children) ? source.children.map(buildUiNode) : []; - const embeddedDocuments = normalizeEmbeddedDocuments(source.embeddedDocuments); - const componentKind = firstString(source.kind); - const pluginProjection = buildPluginProjection(componentKind, source); - const classNames = [ - "eve-component", - `eve-kind-${safeClass(componentKind)}`, - ...pluginClassNames(pluginProjection), - ]; - return { - id: firstString(source.id), - componentKind, - visualElementKind: visualElementKind(componentKind), - classNames, - props: objectValue(source.props), - layout: objectValue(source.layout), - style: objectValue(source.style), - stateBindingCount: Array.isArray(source.stateBindings) ? source.stateBindings.length : 0, - embeddedDocumentCount: embeddedDocuments.length, - embeddedDocuments, - ...(pluginProjection ? { pluginProjection } : {}), - children, - }; -} - -function normalizeEmbeddedDocuments(value) { - return Array.isArray(value) - ? value - .filter(document => document && typeof document === "object") - .map(document => ({ - slotId: firstString(document.slotId, document.id), - documentId: firstString(document.documentId, document.href, document.url), - schemaId: firstString(document.schemaId, document.schema), - presentationKind: firstString(document.presentationKind, document.kind), - })) - .filter(document => document.slotId || document.documentId) - : []; -} - -function visualElementKind(componentKind) { - if (!componentKind) return "VisualElement"; - if (componentKind === "text" || componentKind === "label" || componentKind.startsWith("text.")) return "Label"; - if (componentKind === "control.button" || componentKind === "control.popup") return "Button"; - if (componentKind === "control.text" || componentKind === "control.input.text") return "TextField"; - if (componentKind === "vn.stage") return "SaiVisualNovelStageElement"; - if (componentKind === "panel.dialogue" || componentKind === "text.dialogue") return "SaiVisualNovelDialogueElement"; - if (componentKind === "rail.actions") return "SaiVisualNovelActionRail"; - if (componentKind === "embed.norn") return "NornGraphElement"; - if (componentKind === "embed.tex") return "TeXMathElement"; - return "VisualElement"; -} - -function pluginClassNames(pluginProjection) { - if (!pluginProjection) return []; - return [ - "eve-plugin-projection", - `eve-plugin-${safeClass(pluginProjection.pluginId)}`, - safeClass(pluginProjection.projectionKind), - ].filter(Boolean); -} - -function buildPluginProjection(componentKind, component) { - const props = objectValue(component.props); - if (componentKind === "vn.stage") { - return { - pluginId: "sai.vn", - projectionKind: "sai-vn-stage", - abiSchema: "gamecult.eve.plugin_abi.v1", - commandBoundary: "sidecar-advertised-plugin-abi", - capabilities: ["vn.stage", "story.choose", "story.continue", "story.jump"], - command: "story.choose", - documentId: firstString(props.storyId, component.id), - semanticOwner: "Sai", - availability: "required", - }; - } - - if (componentKind === "embed.norn") { - return { - pluginId: "norn.graph", - projectionKind: "norn-graph", - abiSchema: "gamecult.eve.plugin_abi.v1", - commandBoundary: "sidecar-advertised-plugin-abi", - capabilities: ["embed.norn"], - command: "graph.focus", - documentId: firstString(props.sourceUri, props.documentId, component.id), - semanticOwner: "Norn", - availability: "optional-nested", - }; - } - - if (componentKind === "embed.tex") { - return { - pluginId: "tex.math", - projectionKind: texProjectionKind(props), - abiSchema: "gamecult.eve.plugin_abi.v1", - commandBoundary: "sidecar-advertised-plugin-abi", - capabilities: ["embed.tex", "tex.inline", "tex.block"], - command: "", - documentId: firstString(props.sourceUri, props.source, component.id), - semanticOwner: "EvePlugins", - availability: "optional-nested", - }; - } - - return null; -} - -function texProjectionKind(props) { - const display = firstString(props.display, "inline"); - return display === "block" ? "tex-math-block" : "tex-math-inline"; -} - -function normalizeWorldInteraction(value) { - const source = objectValue(value); - return { - projectionKind: firstString(source.projectionKind), - commandBoundary: firstString(source.commandBoundary), - receiptSchema: firstString(source.receiptSchema), - ownership: firstString(source.ownership), - }; -} - -function validateProjectionArtifact(projection, request) { - if (projection.schema !== "gamecult.eve.unity_uitoolkit_projection.v1") { - throw new Error(`Unexpected Unity UI Toolkit projection schema: ${projection.schema || ""}`); - } - for (const [field, expected] of [ - ["runtimeId", request.runtimeId], - ["providerId", request.providerId], - ["surfaceId", request.surfaceId], - ["projectionKind", request.projectionKind], - ["commandBoundary", request.commandBoundary], - ["receiptSchema", request.receiptSchema], - ]) { - if ((projection[field] || "") !== (expected || "")) { - throw new Error(`Unity UI Toolkit projection ${field} expected ${expected || ""} got ${projection[field] || ""}`); - } - } - if (!projection.root || !projection.root.id) { - throw new Error("Unity UI Toolkit projection artifact must contain a root UI node"); - } -} - -function safeClass(value) { - const text = firstString(value, "unknown"); - return text.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "unknown"; -} - -function objectValue(value) { - return value && typeof value === "object" && !Array.isArray(value) ? value : {}; -} - -function firstString(...values) { - for (const value of values) { - if (typeof value === "string" && value.trim()) return value; - if (typeof value === "number" && Number.isFinite(value)) return String(value); - } - return ""; -} - -function parseArgs(argv) { - const args = {}; - for (let index = 0; index < argv.length; index += 1) { - const value = argv[index]; - if (!value.startsWith("--")) continue; - const key = value.slice(2); - const next = argv[index + 1]; - if (!next || next.startsWith("--")) { - args[key] = true; - } else { - args[key] = next; - index += 1; - } - } - return args; -} - -function runCli() { - const args = parseArgs(process.argv.slice(2)); - for (const required of ["advertisement", "capability", "surface", "output"]) { - if (!args[required]) { - throw new Error(`missing required --${required}`); - } - } - const stamp = args.stamp || "latest"; - const artifact = buildUnityUiToolkitCaptureArtifact({ - advertisement: loadJsonFile(args.advertisement), - capabilityManifest: loadJsonFile(args.capability), - surfaceDocument: loadJsonFile(args.surface), - advertisementPath: args.advertisement, - capabilityManifestPath: args.capability, - captureSurfaceId: args["surface-id"] || "", - stamp, - }); - writeUnityUiToolkitCaptureArtifact(artifact, { - outputPath: args.output, - requestOutputPath: args["request-output"] || "", - }); - process.stdout.write(`${args.output}\n`); -} - -const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : ""; -const modulePath = fileURLToPath(import.meta.url); -if (invokedPath === modulePath) { - runCli(); -} diff --git a/tools/eveunity/eveunity-uitoolkit-capture-artifact.test.mjs b/tools/eveunity/eveunity-uitoolkit-capture-artifact.test.mjs deleted file mode 100644 index a912f3f..0000000 --- a/tools/eveunity/eveunity-uitoolkit-capture-artifact.test.mjs +++ /dev/null @@ -1,88 +0,0 @@ -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; -import test from "node:test"; -import { validateSchemaSubset } from "../../runtimes/incubating/test-support/schema-subset.mjs"; -import { buildUnityUiToolkitCaptureArtifact, buildUnityUiToolkitProjection } from "./eveunity-uitoolkit-capture-artifact.mjs"; - -const unityUiToolkitProjectionSchema = JSON.parse(readFileSync(new URL("../../schemas/gamecult.eve.unity_uitoolkit_projection.v1.schema.json", import.meta.url), "utf8")); -const aetheriaAdvertisement = JSON.parse(readFileSync(new URL("../../web/fixtures/aetheria.provider-advertisement.json", import.meta.url), "utf8")); -const capabilityManifest = JSON.parse(readFileSync(new URL("../../packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json", import.meta.url), "utf8")); -const aetheriaSurfaceDocument = JSON.parse(readFileSync(new URL("../../web/fixtures/aetheria-world-surface.json", import.meta.url), "utf8")); -const worldSmokeAdvertisement = JSON.parse(readFileSync(new URL("../../web/fixtures/eve-world-smoke.provider-advertisement.json", import.meta.url), "utf8")); -const worldSmokeSurfaceDocument = JSON.parse(readFileSync(new URL("../../web/fixtures/eve-world-smoke-surface.json", import.meta.url), "utf8")); -const saiAdvertisement = JSON.parse(readFileSync(new URL("../../web/fixtures/sai-vn.provider-advertisement.json", import.meta.url), "utf8")); -const saiSurfaceDocument = JSON.parse(readFileSync(new URL("../../web/fixtures/sai-vn-surface.json", import.meta.url), "utf8")); - -test("builds Unity UI Toolkit projection capture artifact from provider advertisement and surface", () => { - const { request, projection } = buildUnityUiToolkitCaptureArtifact({ - advertisement: aetheriaAdvertisement, - capabilityManifest, - surfaceDocument: aetheriaSurfaceDocument, - advertisementPath: "web/fixtures/aetheria.provider-advertisement.json", - capabilityManifestPath: "packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json", - stamp: "smoke", - }); - - assert.equal(request.artifactPath, "artifacts/eveunity-uitoolkit-capture/smoke/unity-uitoolkit.png"); - assert.deepEqual(validateSchemaSubset(unityUiToolkitProjectionSchema, projection), []); - assert.equal(projection.schema, "gamecult.eve.unity_uitoolkit_projection.v1"); - assert.equal(projection.runtimeId, "unity-uitoolkit"); - assert.equal(projection.providerId, "aetheria"); - assert.equal(projection.surfaceId, "aetheria.daemon.game"); - assert.equal(projection.commandBoundary, "aetheria.daemon.commands"); - assert.equal(projection.receiptSchema, "aetheria.eve_command_acceptance_status.v1"); - assert.equal(projection.artifactKind, "json-projection"); - assert.equal(projection.captureKind, "unity-uitoolkit-projection-json"); - assert.equal(projection.root.id, "aetheria.daemon.game.root"); - assert.equal(projection.root.visualElementKind, "VisualElement"); - assert.deepEqual(projection.root.classNames.slice(0, 2), ["eve-component", "eve-kind-surface"]); -}); - -test("builds Unity UI Toolkit capture artifact for a generic world provider", () => { - const { projection } = buildUnityUiToolkitCaptureArtifact({ - advertisement: worldSmokeAdvertisement, - capabilityManifest, - surfaceDocument: worldSmokeSurfaceDocument, - advertisementPath: "web/fixtures/eve-world-smoke.provider-advertisement.json", - capabilityManifestPath: "packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json", - stamp: "smoke", - }); - - assert.deepEqual(validateSchemaSubset(unityUiToolkitProjectionSchema, projection), []); - assert.equal(projection.providerId, "eve.world-smoke"); - assert.equal(projection.surfaceId, "eve.world-smoke.surface"); - assert.equal(projection.commandBoundary, "eve.world-smoke.commands"); - assert.equal(projection.receiptSchema, "eve.world_smoke.command_receipt.v1"); - assert.equal(projection.root.id, "eve.world-smoke.root"); - assert.equal(projection.root.children[1].visualElementKind, "VisualElement"); -}); - -test("marks Sai required and Norn TeX optional sidecar plugin UI projections", () => { - const advertisement = structuredClone(saiAdvertisement); - advertisement.surfaces[0].worldInteraction = { - projectionKind: "provider-authored-visual-novel-surface", - commandBoundary: "sai.vn.plugin.commands", - receiptSchema: "gamecult.eve.command_receipt.v1", - loweringTargets: ["unity-uitoolkit"], - ownership: "provider-owns-story-state-independent-plugin-sidecars-own-semantics", - }; - - const projection = buildUnityUiToolkitProjection(saiSurfaceDocument, advertisement, "sai.visual_novel.surface"); - assert.deepEqual(validateSchemaSubset(unityUiToolkitProjectionSchema, projection), []); - const graphNode = projection.root.children.find(child => child.id === "sai.graph"); - const texLayer = projection.root.children.find(child => child.id === "sai.embeds"); - const texNode = texLayer.children.find(child => child.id === "sai.tex.log-power"); - - assert.equal(projection.root.pluginProjection.pluginId, "sai.vn"); - assert.equal(projection.root.pluginProjection.semanticOwner, "Sai"); - assert.equal(projection.root.pluginProjection.availability, "required"); - assert.equal(projection.root.visualElementKind, "SaiVisualNovelStageElement"); - assert.equal(graphNode.pluginProjection.pluginId, "norn.graph"); - assert.equal(graphNode.pluginProjection.semanticOwner, "Norn"); - assert.equal(graphNode.pluginProjection.availability, "optional-nested"); - assert.equal(graphNode.visualElementKind, "NornGraphElement"); - assert.equal(texNode.pluginProjection.pluginId, "tex.math"); - assert.equal(texNode.pluginProjection.semanticOwner, "EvePlugins"); - assert.equal(texNode.pluginProjection.availability, "optional-nested"); - assert.equal(texNode.visualElementKind, "TeXMathElement"); -});