| import type { PipelineNode } from "./types"; |
|
|
| |
| |
| |
| |
| |
|
|
| export type ArtifactSnapshot = { |
| checksum: string; |
| byteSize: number; |
| loc: number; |
| nodeCount: number; |
| }; |
|
|
| export type LockStatus = "locked" | "checked-out" | "returned" | "expired" | "violated"; |
|
|
| |
| |
| 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); |
| } |
|
|
| |
| |
| 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<string> { |
| 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<ArtifactSnapshot> { |
| const canon = canonicalize(nodes); |
| const checksum = await sha256Hex(canon); |
| return { |
| checksum, |
| byteSize: new TextEncoder().encode(canon).length, |
| loc: countLoc(nodes), |
| nodeCount: nodes.length, |
| }; |
| } |
|
|