import { ReplayTimeline, REPLAY_SCHEMA, fnv1a64 } from "./replay.mjs"; import { validateEventLog, validateSnapshot } from "./contracts.mjs"; export const TRACE_SCHEMA = "simulation-trace.v1"; function clone(value) { return structuredClone(value); } export function createTrace({ contextId, timeline, state, task, tick = null } = {}) { if (typeof contextId !== "string" || contextId.length === 0) throw new Error("trace_context_invalid"); if (!(timeline instanceof ReplayTimeline)) throw new Error("trace_timeline_invalid"); if (state === undefined) throw new Error("trace_state_missing"); const exported = timeline.export(); const latest = timeline.snapshots.latest(); if (!latest || latest.seq !== timeline.events.cursor || latest.tick !== timeline.events.tick) throw new Error("trace_snapshot_required"); const value = { schema: TRACE_SCHEMA, context_id: contextId, seq: timeline.events.cursor, tick: tick ?? timeline.events.tick, state: clone(state), state_hash: fnv1a64(state), task: task ? clone(task) : null, timeline: exported, event_hash: fnv1a64(exported.events), snapshot_hash: fnv1a64(latest), }; return { ...value, trace_hash: fnv1a64(value) }; } export function validateTrace(trace, { contextId } = {}) { if (!trace || trace.schema !== TRACE_SCHEMA) throw new Error("trace_schema"); if (contextId !== undefined && trace.context_id !== contextId) throw new Error("trace_context_mismatch"); if (!Number.isSafeInteger(trace.seq) || trace.seq < 0 || !Number.isSafeInteger(trace.tick) || trace.tick < 0) throw new Error("trace_cursor_invalid"); if (fnv1a64(trace.state) !== trace.state_hash) throw new Error("trace_state_hash"); if (!trace.timeline || trace.timeline.schema !== REPLAY_SCHEMA) throw new Error("trace_timeline_schema"); validateEventLog(trace.timeline.events); const snapshots = trace.timeline.snapshots?.snapshots; if (!Array.isArray(snapshots) || snapshots.length === 0) throw new Error("trace_snapshot_missing"); for (const snapshot of snapshots) validateSnapshot(snapshot); const timeline = new ReplayTimeline(trace.timeline); const latest = timeline.snapshots.latest(); if (timeline.events.cursor !== trace.seq || timeline.events.tick !== trace.tick || !latest || latest.seq !== trace.seq || latest.tick !== trace.tick) throw new Error("trace_cursor_mismatch"); if (trace.event_hash !== fnv1a64(trace.timeline.events)) throw new Error("trace_event_hash"); if (trace.snapshot_hash !== fnv1a64(latest)) throw new Error("trace_snapshot_hash"); const unsigned = { ...trace }; delete unsigned.trace_hash; if (trace.trace_hash !== fnv1a64(unsigned)) throw new Error("trace_hash"); return { trace: clone(trace), timeline }; }