/* RFC-0564: CRDT genome log implementation. The genome log is a G-Set (Grow-Only Set) of signed GenomeLogEntry records stored as append-only NDJSON in werkstatt.genome.log. Provides append, read (with signature verification), G-Set union merge, membership view derivation, Ed25519 signing, and file size reporting for the 10MB threshold warning. Do not implement SWIM protocol gossip — that lives in handlers.ts. Do not implement config loading — that lives in config.ts. Do not implement compaction — that is a future RFC. The genome log is a G-Set CRDT — entries are grow-only, never removed. RFC-0564: initial implementation — G-Set genome log with Ed25519 signing, signature verification on read, set-union merge, and membership view derivation. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. */ import { join } from "node:path"; import { signBytes, verifyBytes } from "@warpgogol/werkstatt-shared/passport"; import type { GenomeLogEntry, SwimMembershipView, SwimMember, SwimMemberStatus } from "./types.ts"; import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; const GENOME_LOG_FILENAME = "werkstatt.genome.log"; const GENOME_LOG_SIZE_THRESHOLD = 10 * 1024 * 1024; function canonicalEntryBytes(entry: Omit): Uint8Array { const canonical = JSON.stringify({ workshopId: entry.workshopId, event: entry.event, timestamp: entry.timestamp, source: entry.source, }); return new Uint8Array(Buffer.from(canonical, "utf8")); } export async function signGenomeEntry( entry: Omit, privateKeyHex: string, ): Promise { const signature = await signBytes(privateKeyHex, canonicalEntryBytes(entry)); return { ...entry, signature }; } export async function verifyGenomeEntry( entry: GenomeLogEntry, publicKeyMultibase: string, ): Promise { const { signature, ...rest } = entry; return verifyBytes(publicKeyMultibase, canonicalEntryBytes(rest), signature); } export async function appendGenomeEntry( workspaceRoot: string, entry: GenomeLogEntry, io: WorkspaceIO = createDefaultIO().io, ): Promise { const logPath = join(workspaceRoot, GENOME_LOG_FILENAME); const line = JSON.stringify(entry) + "\n"; await io.appendFile(logPath, line); } export async function readGenomeLog( workspaceRoot: string, publicKeyMultibase?: string, io: WorkspaceIO = createDefaultIO().io, ): Promise<{ entries: GenomeLogEntry[]; skipped: number }> { const logPath = join(workspaceRoot, GENOME_LOG_FILENAME); let raw: string; try { raw = await io.readFile(logPath); } catch { return { entries: [], skipped: 0 }; } const lines = raw.split("\n").filter((l) => l.trim().length > 0); const entries: GenomeLogEntry[] = []; let skipped = 0; for (const line of lines) { try { const parsed = JSON.parse(line) as GenomeLogEntry; if (publicKeyMultibase) { const valid = await verifyGenomeEntry(parsed, publicKeyMultibase); if (!valid) { skipped++; continue; } } entries.push(parsed); } catch { skipped++; } } return { entries, skipped }; } function entryIdentity(entry: GenomeLogEntry): string { return `${entry.workshopId}|${entry.event}|${entry.timestamp}|${entry.source}`; } export function mergeGenomeLogs(local: GenomeLogEntry[], peer: GenomeLogEntry[]): GenomeLogEntry[] { const seen = new Set(); const result: GenomeLogEntry[] = []; for (const entry of [...local, ...peer]) { const id = entryIdentity(entry); if (!seen.has(id)) { seen.add(id); result.push(entry); } } return result; } export function deriveMembershipView(entries: GenomeLogEntry[]): SwimMembershipView { const latestByWorkshop = new Map(); for (const entry of entries) { const existing = latestByWorkshop.get(entry.workshopId); if (!existing || entry.timestamp > existing.timestamp) { latestByWorkshop.set(entry.workshopId, entry); } } const members: SwimMember[] = []; let alive = 0; let suspect = 0; let dead = 0; for (const [workshopId, entry] of latestByWorkshop) { const status = entry.event as SwimMemberStatus; if (status === "alive") alive++; else if (status === "suspect") suspect++; else if (status === "dead") dead++; members.push({ workshopId, endpoint: "", operatorVC: "", status, joinedAt: entry.timestamp, lastSeen: entry.timestamp, }); } const total = members.length; return { members, total, alive, suspect, dead }; } export async function getGenomeLogSize( workspaceRoot: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const logPath = join(workspaceRoot, GENOME_LOG_FILENAME); try { const stats = await io.stat(logPath); return stats.size; } catch { return 0; } } export function isGenomeLogSizeWarning(size: number): boolean { return size > GENOME_LOG_SIZE_THRESHOLD; } export { GENOME_LOG_FILENAME, GENOME_LOG_SIZE_THRESHOLD };