import type { IOFormatCapabilityMatrixIR, IOFormatMatrixExecution, IOFormatMatrixFormat, IOFormatMatrixOperation, } from "./io-format-capability-matrix"; export const IO_FORMAT_UI_GATE_SCHEMA = 1 as const; export const IO_FORMAT_UI_TASK = "M12-05C" as const; export const IO_FORMAT_PROJECT_ACCEPT = ".blend,application/octet-stream" as const; export interface IOFormatUIRouteIR { format: IOFormatMatrixFormat; operation: IOFormatMatrixOperation; execution: IOFormatMatrixExecution; extensions: string[]; } export interface IOFormatUIRegistryIR { schemaVersion: typeof IO_FORMAT_UI_GATE_SCHEMA; task: typeof IO_FORMAT_UI_TASK; parentMatrixSha256: string; projectFileAccept: typeof IO_FORMAT_PROJECT_ACCEPT; importRoutes: IOFormatUIRouteIR[]; exportRoutes: IOFormatUIRouteIR[]; } export interface IOFormatUICommandRef { format: IOFormatMatrixFormat; operation: IOFormatMatrixOperation; execution: IOFormatMatrixExecution; } export class IOFormatUIGateError extends Error { readonly code = "IO_FORMAT_UNSUPPORTED" as const; constructor(message: string) { super(`IO_FORMAT_UNSUPPORTED: ${message}`); this.name = "IOFormatUIGateError"; } } const SHA256 = /^[a-f0-9]{64}$/; const FORMAT_EXTENSIONS: Record = { GLTF: [".gltf"], GLB: [".glb"], OBJ: [".obj"], STL: [".stl"], PLY: [".ply"], USD: [".usd", ".usda", ".usdc"], ALEMBIC: [".abc"], }; function routeFor( matrix: IOFormatCapabilityMatrixIR, operation: IOFormatMatrixOperation, execution: IOFormatMatrixExecution, ): IOFormatUIRouteIR[] { return matrix.formats .filter((entry) => { const route = entry.operations[operation][execution.toLowerCase() as "local" | "server"]; const runtimeStatus = operation === "IMPORT" ? entry.runtimeImportStatus : entry.runtimeExportStatus; return runtimeStatus === "AVAILABLE" && route.status === "READY" && route.execution === execution; }) .map((entry) => ({ format: entry.format, operation, execution, extensions: [...FORMAT_EXTENSIONS[entry.format]], })); } export function buildIOFormatUIRegistry(matrix: IOFormatCapabilityMatrixIR, parentMatrixSha256: string): IOFormatUIRegistryIR { if (!SHA256.test(parentMatrixSha256)) throw new IOFormatUIGateError("parent matrix SHA-256 is invalid"); return { schemaVersion: IO_FORMAT_UI_GATE_SCHEMA, task: IO_FORMAT_UI_TASK, parentMatrixSha256, projectFileAccept: IO_FORMAT_PROJECT_ACCEPT, importRoutes: routeFor(matrix, "IMPORT", "LOCAL"), exportRoutes: routeFor(matrix, "EXPORT", "LOCAL"), }; } function exactKeys(value: Record, expected: readonly string[], path: string): void { const actual = Object.keys(value).sort(); const allowed = [...expected].sort(); if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) throw new IOFormatUIGateError(`${path} contains undeclared fields`); } function record(value: unknown, path: string): Record { if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IOFormatUIGateError(`${path} must be an object`); return value as Record; } function parseRoute(value: unknown, path: string, operation: IOFormatMatrixOperation): IOFormatUIRouteIR { const input = record(value, path); exactKeys(input, ["format", "operation", "execution", "extensions"], path); if (typeof input.format !== "string" || !(input.format in FORMAT_EXTENSIONS) || input.operation !== operation || input.execution !== "LOCAL") throw new IOFormatUIGateError(`${path} route identity is invalid`); if (!Array.isArray(input.extensions) || input.extensions.length !== FORMAT_EXTENSIONS[input.format as IOFormatMatrixFormat].length || input.extensions.some((extension, index) => extension !== FORMAT_EXTENSIONS[input.format as IOFormatMatrixFormat][index])) throw new IOFormatUIGateError(`${path}.extensions are invalid`); return { format: input.format as IOFormatMatrixFormat, operation, execution: "LOCAL", extensions: [...input.extensions as string[]] }; } export function parseIOFormatUIRegistry(value: unknown): IOFormatUIRegistryIR { const input = record(value, "input"); exactKeys(input, ["schemaVersion", "task", "parentMatrixSha256", "projectFileAccept", "importRoutes", "exportRoutes"], "input"); if (input.schemaVersion !== IO_FORMAT_UI_GATE_SCHEMA || input.task !== IO_FORMAT_UI_TASK || typeof input.parentMatrixSha256 !== "string" || !SHA256.test(input.parentMatrixSha256) || input.projectFileAccept !== IO_FORMAT_PROJECT_ACCEPT) throw new IOFormatUIGateError("registry header is invalid"); if (!Array.isArray(input.importRoutes) || !Array.isArray(input.exportRoutes)) throw new IOFormatUIGateError("registry routes are invalid"); const importRoutes = input.importRoutes.map((route, index) => parseRoute(route, `importRoutes[${index}]`, "IMPORT")); const exportRoutes = input.exportRoutes.map((route, index) => parseRoute(route, `exportRoutes[${index}]`, "EXPORT")); const identities = [...importRoutes, ...exportRoutes].map((route) => `${route.operation}:${route.execution}:${route.format}`); if (new Set(identities).size !== identities.length) throw new IOFormatUIGateError("registry contains duplicate route identities"); return { schemaVersion: IO_FORMAT_UI_GATE_SCHEMA, task: IO_FORMAT_UI_TASK, parentMatrixSha256: input.parentMatrixSha256, projectFileAccept: IO_FORMAT_PROJECT_ACCEPT, importRoutes, exportRoutes }; } function routeIdentity(route: IOFormatUIRouteIR): string { return `${route.operation}:${route.execution}:${route.format}:${route.extensions.join("|")}`; } export function validateIOFormatUIRegistry(value: unknown, matrix: IOFormatCapabilityMatrixIR, parentMatrixSha256: string): IOFormatUIRegistryIR { const parsed = parseIOFormatUIRegistry(value); const expected = buildIOFormatUIRegistry(matrix, parentMatrixSha256); const actualRoutes = [...parsed.importRoutes, ...parsed.exportRoutes].map(routeIdentity); const expectedRoutes = [...expected.importRoutes, ...expected.exportRoutes].map(routeIdentity); if (parsed.parentMatrixSha256 !== parentMatrixSha256 || actualRoutes.length !== expectedRoutes.length || actualRoutes.some((route, index) => route !== expectedRoutes[index])) throw new IOFormatUIGateError("registry route is not declared by the capability matrix"); return parsed; } function routeMatches(registry: IOFormatUIRegistryIR, command: IOFormatUICommandRef): boolean { const routes = command.operation === "IMPORT" ? registry.importRoutes : registry.exportRoutes; return routes.some((route) => route.format === command.format && route.execution === command.execution); } export function filterIOFormatOperatorCommands(commands: readonly T[], registry: IOFormatUIRegistryIR): T[] { return commands.filter((command) => !command.ioFormat || routeMatches(registry, command.ioFormat)); } export function gateIOFormatFileSelection(fileName: string, registry: IOFormatUIRegistryIR): { status: "READY"; kind: "BLEND" } | { status: "BLOCKED"; code: "IO_FORMAT_UNSUPPORTED"; extension: string } { const extension = fileName.trim().toLowerCase().match(/\.[a-z0-9]+$/)?.[0] ?? ""; if (extension === ".blend") return { status: "READY", kind: "BLEND" }; const route = registry.importRoutes.find((candidate) => candidate.extensions.includes(extension)); if (route) return { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", extension }; return { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", extension }; } export function ioFormatUIAccept(registry: IOFormatUIRegistryIR): string { const importExtensions = registry.importRoutes.flatMap((route) => route.extensions); return [registry.projectFileAccept, ...importExtensions].join(","); }