LAYER / src /lib /integrity.ts
Claude
Add artifact-immutability lock at the grid boundary
e2dea69 unverified
Raw
History Blame Contribute Delete
2.27 kB
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<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,
};
}