import type { PipelineNode } from "./types"; // Artifact-immutability gate for the boundary between the React Flow grid // (nodes "at the front") and the Test Bench execution console. A snapshot is // locked the moment nodes render into the grid; before any run is allowed to // use that data, a fresh snapshot is recomputed and compared. A mismatch // blocks the run rather than silently proceeding on stale/altered data. export type ArtifactSnapshot = { checksum: string; byteSize: number; loc: number; nodeCount: number; }; export type LockStatus = "locked" | "checked-out" | "returned" | "expired" | "violated"; // Placeholder until the real duration is specified — how long a checked-out // artifact may stay away from the front before its lock is treated as expired. export const DEFAULT_LOCK_TIMEOUT_MS = 180_000; function canonicalize(nodes: PipelineNode[]): string { const stable = nodes .map((n) => ({ id: n.id, label: n.label, category: n.category, summary: n.summary, handoffOut: n.handoffOut ?? null, expertNote: n.expertNote, confidence: n.confidence ?? null, implementations: n.implementations, })) .sort((a, b) => a.id.localeCompare(b.id)); return JSON.stringify(stable); } // No literal source files exist per node, so LOC is a defensible proxy: the // count of non-empty lines across each node's authored text fields. function countLoc(nodes: PipelineNode[]): number { return nodes.reduce((sum, n) => { const text = [n.summary, n.expertNote, n.handoffOut ?? ""].join("\n"); return sum + text.split("\n").filter((line) => line.trim().length > 0).length; }, 0); } async function sha256Hex(text: string): Promise { const bytes = new TextEncoder().encode(text); const digest = await crypto.subtle.digest("SHA-256", bytes); return Array.from(new Uint8Array(digest)) .map((b) => b.toString(16).padStart(2, "0")) .join(""); } export async function snapshotArtifact(nodes: PipelineNode[]): Promise { const canon = canonicalize(nodes); const checksum = await sha256Hex(canon); return { checksum, byteSize: new TextEncoder().encode(canon).length, loc: countLoc(nodes), nodeCount: nodes.length, }; }