import { fnv1a64 } from "../worker/replay.mjs"; import { WorkspaceRuntime, normalizeWorkspacePath } from "./runtime.mjs"; import { parseIni } from "./ini-parser.mjs"; import { resolveResources } from "./resource-resolver.mjs"; import { preflightHalConfiguration } from "./hal-configuration.mjs"; export const CONFIGURATION_STARTUP_SCHEMA = "configuration-startup-runtime.v1"; export const STARTUP_STATES = Object.freeze([ "NEW", "MOUNTED", "PARSED", "RESOLVED", "CORE_CREATED", "HAL_READY", "TASK_READY", "BLOCKED", ]); export const STARTUP_ERRORS = Object.freeze({ invalid_input: "startup_input_invalid", workspace: "startup_workspace_blocked", ini: "startup_ini_blocked", resources: "startup_resources_blocked", core: "startup_core_blocked", hal: "startup_hal_blocked", task: "startup_task_blocked", transition: "startup_transition_invalid", capability: "startup_capability_unsupported", }); const ALLOWED = Object.freeze({ NEW: ["MOUNTED", "BLOCKED"], MOUNTED: ["PARSED", "BLOCKED"], PARSED: ["RESOLVED", "BLOCKED"], RESOLVED: ["CORE_CREATED", "BLOCKED"], CORE_CREATED: ["HAL_READY", "BLOCKED"], HAL_READY: ["TASK_READY", "BLOCKED"], TASK_READY: [], BLOCKED: [], }); function clone(value) { return structuredClone(value); } function text(value) { return typeof value === "string" && value.length > 0 ? value : null; } function coreCall(core, name, ...args) { if (!core) return 0; if (args.some((value) => typeof value === "string") && typeof core.ccall === "function") { return core.ccall(name, "number", args.map((value) => typeof value === "string" ? "string" : "number"), args); } const direct = core[`_${name}`] ?? core[name]; if (typeof direct === "function") return direct(...args); if (typeof core.ccall === "function") { const types = args.map((value) => typeof value === "string" ? "string" : "number"); return core.ccall(name, "number", types, args); } return 0; } function diagnostic(code, source = null, line = 0, extra = {}) { return { status: "blocked", code, source, line: Number.isSafeInteger(line) ? line : 0, ...extra }; } function asWorkspace(value) { if (value && typeof value.read === "function" && typeof value.list === "function") return value; if (value instanceof WorkspaceRuntime) return value; if (value && typeof value === "object" && value.files && typeof value.files === "object") return new WorkspaceRuntime(value); if (value && typeof value === "object") return new WorkspaceRuntime({ files: value }); throw new Error(STARTUP_ERRORS.workspace); } function halManifest(manifest = []) { return Array.isArray(manifest) ? manifest : []; } /** * Deterministic configuration startup pipeline. A pipeline instance is single-use; * all externally visible data is returned by report() and can be hashed/replayed. */ export class ConfigurationStartupStateMachine { constructor({ workspace = null, files = null, entrypoint = null, variables = {}, maxDepth = 32, core = null, task = null, halManifest: manifest = [], coreCreated = false } = {}) { this.state = "NEW"; this.workspace = workspace; this.files = files; this.entrypoint = entrypoint; this.variables = { ...variables }; this.maxDepth = maxDepth; this.core = core; this.task = task; this.manifest = manifest; this.coreCreated = coreCreated === true; this.ini = null; this.resources = null; this.hal = null; this.diagnostics = []; this.transitions = [{ from: null, to: "NEW" }]; this.coreResult = null; this.stateHash = null; } transition(next) { if (!STARTUP_STATES.includes(next) || !ALLOWED[this.state]?.includes(next)) { throw new Error(`${STARTUP_ERRORS.transition}:${this.state}->${next}`); } const from = this.state; this.state = next; this.transitions.push({ from, to: next }); } block(code, source = null, line = 0, extra = {}) { const row = diagnostic(code, source, line, extra); this.diagnostics.push(row); if (this.state !== "BLOCKED") { const from = this.state; this.state = "BLOCKED"; this.transitions.push({ from, to: "BLOCKED", diagnostic: clone(row) }); } this.stateHash = this.hash(); return this.report(); } mount() { if (this.state !== "NEW") return this.report(); try { this.workspace = asWorkspace(this.workspace ?? this.files); if (!text(this.entrypoint)) throw Object.assign(new Error(STARTUP_ERRORS.invalid_input), { code: STARTUP_ERRORS.invalid_input }); this.entrypoint = normalizeWorkspacePath(this.entrypoint); if (!this.entrypoint) throw Object.assign(new Error(STARTUP_ERRORS.invalid_input), { code: STARTUP_ERRORS.invalid_input }); this.workspace.stat(`/workspace/${this.entrypoint}`); this.transition("MOUNTED"); } catch (cause) { return this.block(cause.code ?? cause.message ?? STARTUP_ERRORS.workspace, this.entrypoint, cause.line ?? 0, { cause: String(cause?.message ?? cause) }); } return this.report(); } parse() { if (this.state !== "MOUNTED") return this.report(); try { this.ini = parseIni({ workspace: this.workspace, entrypoint: this.entrypoint, variables: this.variables, maxDepth: this.maxDepth }); this.transition("PARSED"); } catch (cause) { return this.block(cause.code ?? cause.message ?? STARTUP_ERRORS.ini, cause.source ?? this.entrypoint, cause.line ?? 0, { cause: String(cause?.message ?? cause), ...(cause.variable ? { variable: cause.variable } : {}), }); } return this.report(); } resolve() { if (this.state !== "PARSED") return this.report(); try { this.resources = resolveResources({ workspace: this.workspace, entrypoint: this.entrypoint, ini: this.ini }); } catch (cause) { return this.block(cause.code ?? cause.message ?? STARTUP_ERRORS.resources, this.entrypoint, cause.line ?? 0, { cause: String(cause?.message ?? cause) }); } const blocked = this.resources.resources.filter((row) => row.status === "blocked"); if (blocked.length) { this.diagnostics.push(...blocked.map((row) => ({ ...row, status: "blocked" }))); return this.block(STARTUP_ERRORS.resources, blocked[0].source ?? this.entrypoint, blocked[0].line ?? 0, { resources: clone(blocked) }); } try { this.materializeRuntimeResources(); } catch (cause) { return this.block(STARTUP_ERRORS.resources, this.entrypoint, 0, { cause: String(cause?.message ?? cause), operation: "materialize_runtime_resources", }); } this.transition("RESOLVED"); return this.report(); } materializeRuntimeResources() { const generated = this.resources?.generated_files ?? {}; const paths = Object.keys(generated).sort(); if (!paths.length) return; if (!(typeof this.workspace.begin === "function" && typeof this.workspace.write === "function" && typeof this.workspace.commit === "function")) { const files = Object.fromEntries(this.workspace.list().map((row) => [row.path, this.workspace.read(row.path)])); this.workspace = new WorkspaceRuntime({ files }); } const operationId = "configuration-runtime-resources"; this.workspace.begin(operationId); try { for (const path of paths) this.workspace.write(operationId, `/workspace/${path}`, generated[path]); this.workspace.commit(operationId); } catch (cause) { try { this.workspace.abort(operationId); } catch { /* preserve the original materialization error */ } throw cause; } } createCore() { if (this.state !== "RESOLVED") return this.report(); try { const result = this.coreCreated ? 0 : coreCall(this.core, "sim_create", 0, 0); this.coreResult = Number.isFinite(result) ? result : 0; if (result !== 0) return this.block(STARTUP_ERRORS.core, this.entrypoint, 0, { result }); const loaded = coreCall(this.core, "sim_load_workspace", "/workspace"); if (loaded !== 0) return this.block(STARTUP_ERRORS.core, this.entrypoint, 0, { result: loaded, operation: "sim_load_workspace" }); this.transition("CORE_CREATED"); } catch (cause) { return this.block(STARTUP_ERRORS.core, this.entrypoint, 0, { cause: String(cause?.message ?? cause) }); } return this.report(); } prepareHal() { if (this.state !== "CORE_CREATED") return this.report(); const records = []; for (const resource of this.resources.resources.filter((row) => row.status === "resolved" && row.type === "hal")) { let lines; try { const rawLines = this.workspace.read(`/workspace/${resource.path}`).split(/\r?\n/); lines = []; for (const rawLine of rawLines) { if (lines.length > 0 && /\\\s*$/.test(lines.at(-1))) lines[lines.length - 1] = `${lines.at(-1).replace(/\\\s*$/, "")} ${rawLine.trim()}`; else lines.push(rawLine); } } catch (cause) { return this.block(STARTUP_ERRORS.hal, resource.path, resource.line ?? 0, { cause: String(cause?.message ?? cause) }); } const webEquivalentPrograms = resource.path === "linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.hal" ? ["./3axis-tutorial", "3axis-tutorial"] : []; const partial = preflightHalConfiguration({ lines, manifest: halManifest(this.manifest), variables: this.ini?.sections ?? {}, webEquivalentPrograms }); records.push(...partial.records.map((record) => ({ ...record, source: resource.path, resource_id: resource.resource_id }))); } const blockedRecords = records.filter((record) => record.status === "blocked"); this.hal = { schema: "hal-configuration-preflight.v1", status: blockedRecords.length ? "blocked" : "ready", records, record_count: records.length, blocked_count: blockedRecords.length, }; if (this.hal.status !== "ready") { const first = this.hal.records.find((row) => row.status === "blocked"); return this.block(STARTUP_ERRORS.hal, first?.source ?? this.entrypoint, first?.line ?? 0, { hal: clone(this.hal) }); } this.transition("HAL_READY"); return this.report(); } prepareTask() { if (this.state !== "HAL_READY") return this.report(); try { if (this.task && typeof this.task.transition === "function") { const current = this.task.state?.state ?? this.task.state; if (current === "NEW" && typeof this.task.transition === "function") this.task.transition("READY"); } this.transition("TASK_READY"); } catch (cause) { return this.block(STARTUP_ERRORS.task, this.entrypoint, 0, { cause: String(cause?.message ?? cause) }); } return this.report(); } run() { for (const step of ["mount", "parse", "resolve", "createCore", "prepareHal", "prepareTask"]) { if (this.state === "BLOCKED" || this.state === "TASK_READY") break; this[step](); } this.stateHash = this.hash(); return this.report(); } hash() { const unsigned = this.unsignedReport(); return fnv1a64(unsigned); } unsignedReport() { return { schema: CONFIGURATION_STARTUP_SCHEMA, entrypoint: this.entrypoint, state: this.state, transitions: this.transitions, diagnostics: this.diagnostics, ini: this.ini ? { state_hash: this.ini.state_hash, graph: this.ini.graph, includes: this.ini.includes } : null, resources: this.resources ? { state_hash: this.resources.state_hash, resolved_count: this.resources.resolved_count, optional_count: this.resources.optional_count ?? 0, blocked_count: this.resources.blocked_count, generated_files: this.resources.generated_files ?? {}, resources: this.resources.resources } : null, hal: this.hal ? { schema: this.hal.schema, status: this.hal.status, record_count: this.hal.record_count, blocked_count: this.hal.blocked_count, records: this.hal.records } : null, core: this.coreResult === null ? null : { result: this.coreResult }, }; } report() { const unsigned = this.unsignedReport(); return { ...clone(unsigned), status: this.state === "TASK_READY" ? "ready" : "blocked", state_hash: fnv1a64(unsigned) }; } } export function startConfiguration(options = {}) { return new ConfigurationStartupStateMachine(options).run(); } export function createConfigurationStartup(options = {}) { return new ConfigurationStartupStateMachine(options); } export const ConfigurationStartup = ConfigurationStartupStateMachine; export const STARTUP_SCHEMA = CONFIGURATION_STARTUP_SCHEMA;