import { createHash } from "node:crypto"; import type { GraphPatch, PlanningDependency, WorkflowState } from "./types.ts"; function sortRecord(record: Record): Record { return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b))); } function canonicalize(value: unknown): unknown { if (Array.isArray(value)) return value.map(canonicalize); if (!value || typeof value !== "object") return value; return Object.fromEntries( Object.entries(value as Record) .sort(([a], [b]) => a.localeCompare(b)) .map(([key, nested]) => [key, canonicalize(nested)]), ); } function sortedUnique(values: string[]): string[] { return [...new Set(values)].sort((a, b) => a.localeCompare(b)); } function sortedDependencies(values: PlanningDependency[]): PlanningDependency[] { return [...values].sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b))); } export function canonicalState(state: WorkflowState): WorkflowState { return { ...state, places: Object.fromEntries( Object.entries(sortRecord(state.places)).map(([id, place]) => [ id, { ...place, evidenceIds: sortedUnique(place.evidenceIds) }, ]), ), transitions: Object.fromEntries( Object.entries(sortRecord(state.transitions)).map(([id, transition]) => [ id, { ...transition, evidenceIds: sortedUnique(transition.evidenceIds), planning: { ...transition.planning, dependsOn: sortedDependencies(transition.planning.dependsOn), }, contract: { ...transition.contract, expectedEvidence: sortedUnique(transition.contract.expectedEvidence), evidenceSelectors: sortedUnique(transition.contract.evidenceSelectors), allowedEpisodeKinds: sortedUnique(transition.contract.allowedEpisodeKinds), }, }, ]), ), arcs: sortRecord(state.arcs), refinements: Object.fromEntries( Object.entries(sortRecord(state.refinements)).map(([id, refinement]) => [ id, { ...refinement, nodeIds: sortedUnique(refinement.nodeIds), arcIds: sortedUnique(refinement.arcIds), }, ]), ), evidence: Object.fromEntries( Object.entries(sortRecord(state.evidence)).map(([id, evidence]) => [ id, { ...evidence, selectors: sortedUnique(evidence.selectors) }, ]), ), appliedPatches: Object.fromEntries( Object.entries(sortRecord(state.appliedPatches)).map(([key, value]) => [ key, { ...value, snapshotHash: "" }, ]), ), }; } export function canonicalJson(value: unknown): string { return JSON.stringify(canonicalize(value)); } function hash(value: unknown): string { return `sha256:${createHash("sha256").update(canonicalJson(value)).digest("hex")}`; } export function snapshotHash(state: WorkflowState): string { return hash(canonicalState(state)); } export function patchRequestHash(patch: GraphPatch): string { return hash(patch); }