import { fnv1a64 } from "./replay.mjs"; export const CONTRACT_SCHEMA = "sim-contracts.v1"; export const CONTRACT_VERSION = 1; export const SCHEMAS = Object.freeze({ task: "sim_task.v1", command: "command-envelope.v1", snapshot: "snapshot.v1", event: "event-log.v1", workspace: "workspace-package.v1", }); const COMMAND_FIELDS = new Set([ "schema_version", "kind", "context_id", "session_id", "seq", "operation_id", "simulation_tick", "sent_at_monotonic_ms", "payload", ]); const TASK_FIELDS = new Set([ "schema", "context_id", "session_id", "state", "mode", "program_status", "seq", "simulation_tick", "current_line", "state_hash", ]); const SNAPSHOT_FIELDS = new Set([ "schema", "snapshot_id", "base_snapshot_id", "seq", "tick", "state", "state_hash", ]); const EVENT_FIELDS = new Set(["seq", "tick", "kind", "payload", "payload_hash"]); const PACKAGE_V1_FIELDS = new Set([ "schema", "format_version", "context_id", "committed_seq", "checkpoint", "timeline", "journal", "content_hash", "package_hash", ]); const PACKAGE_V2_FIELDS = new Set([...PACKAGE_V1_FIELDS, "migration", "integrity"]); const SIDE_EFFECT_KINDS = new Set([ "SESSION_CREATE", "LOAD_PROJECT", "START", "STOP", "RESET", "LOAD_WORKSPACE", "SIM_STEP", "COMMAND", "PROGRAM_LOAD", "PROGRAM_RUN", "PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "PROGRAM_STEP", "PROGRAM_SEEK", "JOG_START", "JOG_STOP", "HOME", "SET_MODE", "SET_OVERRIDE", "SPINDLE_COMMAND", "COOLANT_COMMAND", "TOOL_PREPARE", "TOOL_CHANGE", "PROBE_CONFIG", "HAL_COMMAND", "HAL_SET", "PLC_START", "PLC_STOP", "ENCODER_SET", "COOLANT_COMMAND", "COOLANT_FAULT", "DOOR_SET", "INTERLOCK_SET", "INTERLOCK_RESET", "DEVICE_ESTOP", "DEVICE_ACK_ISSUE", "DEVICE_ACK", "DEVICE_ACK_ABORT", "CONFIGURATION_BATCH", "PROJECT_SAVE", "IMPORT", "EXPORT", "CHECKPOINT", "RECOVER", "SUBSCRIBE", "UNSUBSCRIBE", "CANCEL", ]); function fail(code) { throw new Error(code); } function object(value, code) { if (!value || typeof value !== "object" || Array.isArray(value)) fail(code); return value; } function string(value, code, max = 256) { if (typeof value !== "string" || value.length === 0 || value.length > max) fail(code); return value; } function integer(value, code, positive = false) { if (!Number.isSafeInteger(value) || value < (positive ? 1 : 0)) fail(code); return value; } function finite(value, code) { if (typeof value !== "number" || !Number.isFinite(value)) fail(code); return value; } function exactFields(value, allowed, code) { for (const key of Object.keys(value)) if (!allowed.has(key)) fail(code); } function noNonFinite(value, code = "contract_nonfinite") { if (typeof value === "number") { if (!Number.isFinite(value)) fail(code); return; } if (Array.isArray(value)) { for (const item of value) noNonFinite(item, code); return; } if (value && typeof value === "object") { for (const item of Object.values(value)) noNonFinite(item, code); } } function hashUnsigned(value, field) { const unsigned = { ...value }; delete unsigned[field]; return fnv1a64(unsigned); } export function validateCommandEnvelope(value, { contextId, sessionId } = {}) { const envelope = object(value, "contract_command_object"); exactFields(envelope, COMMAND_FIELDS, "contract_command_unknown_field"); if (envelope.schema_version !== CONTRACT_VERSION) fail("contract_command_schema"); string(envelope.kind, "contract_command_kind", 128); string(envelope.context_id, "contract_command_context"); string(envelope.session_id, "contract_command_session"); integer(envelope.seq, "contract_command_seq", true); if (envelope.operation_id !== undefined) string(envelope.operation_id, "contract_command_operation", 128); if (SIDE_EFFECT_KINDS.has(envelope.kind) && envelope.operation_id === undefined) fail("contract_command_operation_required"); if (envelope.simulation_tick !== undefined) integer(envelope.simulation_tick, "contract_command_tick"); if (envelope.sent_at_monotonic_ms !== undefined) finite(envelope.sent_at_monotonic_ms, "contract_command_sent_at"); object(envelope.payload, "contract_command_payload"); noNonFinite(envelope.payload); if (contextId !== undefined && envelope.context_id !== contextId) fail("contract_command_context_mismatch"); if (sessionId !== undefined && envelope.session_id !== sessionId) fail("contract_command_session_mismatch"); return structuredClone(envelope); } export function validateSimTask(value, { contextId, sessionId } = {}) { const task = object(value, "contract_task_object"); exactFields(task, TASK_FIELDS, "contract_task_unknown_field"); if (task.schema !== SCHEMAS.task) fail("contract_task_schema"); string(task.context_id, "contract_task_context"); string(task.session_id, "contract_task_session"); if (!new Set(["NEW", "READY", "RUNNING", "PAUSED", "ESTOP", "FAULT", "STOPPED", "DESTROYED"]).has(task.state)) fail("contract_task_state"); if (!new Set(["MANUAL", "MDI", "AUTO"]).has(task.mode)) fail("contract_task_mode"); if (!new Set(["idle", "loaded", "running", "paused", "completed", "aborted", "fault"]).has(task.program_status)) fail("contract_task_program_status"); integer(task.seq, "contract_task_seq", true); integer(task.simulation_tick, "contract_task_tick"); if (task.current_line !== null) integer(task.current_line, "contract_task_line"); noNonFinite(task); if (typeof task.state_hash !== "string" || task.state_hash !== hashUnsigned(task, "state_hash")) fail("contract_task_hash"); if (contextId !== undefined && task.context_id !== contextId) fail("contract_task_context_mismatch"); if (sessionId !== undefined && task.session_id !== sessionId) fail("contract_task_session_mismatch"); return structuredClone(task); } export function validateSnapshot(value) { const snapshot = object(value, "contract_snapshot_object"); exactFields(snapshot, SNAPSHOT_FIELDS, "contract_snapshot_unknown_field"); if (snapshot.schema !== SCHEMAS.snapshot) fail("contract_snapshot_schema"); string(snapshot.snapshot_id, "contract_snapshot_id"); if (snapshot.base_snapshot_id !== null) string(snapshot.base_snapshot_id, "contract_snapshot_base"); integer(snapshot.seq, "contract_snapshot_seq", true); integer(snapshot.tick, "contract_snapshot_tick"); if (snapshot.state === undefined) fail("contract_snapshot_state"); noNonFinite(snapshot.state); if (typeof snapshot.state_hash !== "string" || snapshot.state_hash !== fnv1a64(snapshot.state)) fail("contract_snapshot_hash"); if (snapshot.snapshot_id !== `snap:${snapshot.seq}:${snapshot.state_hash}`) fail("contract_snapshot_id_mismatch"); return structuredClone(snapshot); } export function validateEventLog(value) { const log = object(value, "contract_event_log_object"); if (log.schema !== SCHEMAS.event || !Array.isArray(log.events)) fail("contract_event_log_schema"); integer(log.cursor, "contract_event_cursor"); integer(log.tick, "contract_event_tick"); let previousTick = 0; for (let index = 0; index < log.events.length; index += 1) { const event = object(log.events[index], "contract_event_object"); exactFields(event, EVENT_FIELDS, "contract_event_unknown_field"); if (event.seq !== index + 1) fail("contract_event_sequence"); integer(event.seq, "contract_event_seq", true); integer(event.tick, "contract_event_tick"); if (event.tick < previousTick) fail("contract_event_tick_regression"); previousTick = event.tick; string(event.kind, "contract_event_kind", 64); object(event.payload, "contract_event_payload"); noNonFinite(event.payload); if (event.payload_hash !== fnv1a64(event.payload)) fail("contract_event_payload_hash"); } if (log.cursor !== (log.events.length === 0 ? 0 : log.events.at(-1).seq)) fail("contract_event_cursor_mismatch"); if (log.tick !== (log.events.length === 0 ? 0 : log.events.at(-1).tick)) fail("contract_event_tick_mismatch"); return structuredClone(log); } function validateTimeline(value) { const timeline = object(value, "contract_workspace_timeline"); if (timeline.schema !== "replay-timeline.v1" || !timeline.events || !timeline.snapshots) fail("contract_workspace_timeline_schema"); validateEventLog(timeline.events); const snapshots = object(timeline.snapshots, "contract_workspace_snapshots"); if (snapshots.schema !== SCHEMAS.snapshot || !Array.isArray(snapshots.snapshots)) fail("contract_workspace_snapshots_schema"); const seen = new Set(); for (const snapshot of snapshots.snapshots) { validateSnapshot(snapshot); if (snapshot.base_snapshot_id !== null && !seen.has(snapshot.base_snapshot_id)) fail("contract_workspace_snapshot_base_missing"); if (seen.has(snapshot.snapshot_id)) fail("contract_workspace_snapshot_duplicate"); seen.add(snapshot.snapshot_id); } return timeline; } export function validateWorkspacePackage(value, { contextId } = {}) { const pkg = object(value, "contract_workspace_object"); if (pkg.schema !== SCHEMAS.workspace) fail("contract_workspace_schema"); integer(pkg.format_version, "contract_workspace_format"); if (pkg.format_version !== 1 && pkg.format_version !== 2) fail("contract_workspace_format_unsupported"); exactFields(pkg, pkg.format_version === 1 ? PACKAGE_V1_FIELDS : PACKAGE_V2_FIELDS, "contract_workspace_unknown_field"); string(pkg.context_id, "contract_workspace_context"); integer(pkg.committed_seq, "contract_workspace_committed_seq"); validateTimeline(pkg.timeline); if (pkg.committed_seq > pkg.timeline.events.cursor) fail("contract_workspace_committed_seq_ahead"); if (!Array.isArray(pkg.journal)) fail("contract_workspace_journal"); if (pkg.checkpoint !== null && pkg.checkpoint !== undefined) object(pkg.checkpoint, "contract_workspace_checkpoint"); noNonFinite(pkg); if (pkg.content_hash !== hashUnsigned({ context_id: pkg.context_id, committed_seq: pkg.committed_seq, checkpoint: pkg.checkpoint ?? null, journal: pkg.journal ?? [], timeline: pkg.timeline, }, "ignored")) fail("contract_workspace_content_hash"); if (pkg.package_hash !== hashUnsigned(pkg, "package_hash")) fail("contract_workspace_package_hash"); if (pkg.format_version === 2) { if (!pkg.migration || pkg.migration.from_version !== 1 || pkg.migration.to_version !== 2 || pkg.migration.lossless !== true || typeof pkg.migration.source_package_hash !== "string") fail("contract_workspace_migration"); if (!pkg.integrity || pkg.integrity.timeline_hash !== fnv1a64(pkg.timeline) || pkg.integrity.journal_hash !== fnv1a64(pkg.journal) || pkg.integrity.checkpoint_hash !== (pkg.checkpoint ? fnv1a64(pkg.checkpoint) : null)) fail("contract_workspace_integrity"); } if (contextId !== undefined && pkg.context_id !== contextId) fail("contract_workspace_context_mismatch"); return structuredClone(pkg); } export { COMMAND_FIELDS, EVENT_FIELDS, SNAPSHOT_FIELDS, TASK_FIELDS };