File size: 2,265 Bytes
e2dea69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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,
  };
}