/* jsonl — crash-safe JSONL reader and writer for the operation journal (RFC-0958). Do not implement step execution logic — that lives in runner.ts. Do not import mission-specific types — the journal is pure infrastructure. RFC-0958: initial JSONL reader/writer — appendRecord, readJournal, findIncompleteOperation. RFC-1219: findIncompleteOperation scans all op-started newest→oldest and returns status/failedStep — a completed later op no longer masks an earlier incomplete one; op-interrupted is non-terminal. 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 path from "node:path"; import type { JournalRecord } from "./types.ts"; import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; export async function appendRecord( journalPath: string, record: JournalRecord, io: WorkspaceIO = createDefaultIO().io, ): Promise { const dir = path.dirname(journalPath); if (!(await io.exists(dir))) { await io.mkdir(dir); } const line = JSON.stringify(record) + "\n"; await io.appendFile(journalPath, line); } export class TornLineError extends Error { constructor( message: string, readonly lineNumber: number, readonly rawLine: string, ) { super(message); this.name = "TornLineError"; } } export async function readJournal( journalPath: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { if (!(await io.exists(journalPath))) { return []; } const content = await io.readFile(journalPath); if (content.length === 0) { return []; } const lines = content.split("\n"); const records: JournalRecord[] = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.length === 0) { if (i === lines.length - 1) { continue; } continue; } try { const parsed = JSON.parse(line) as JournalRecord; records.push(parsed); } catch { const isLastNonEmpty = i === lines.length - 1 || (i === lines.length - 2 && lines[lines.length - 1].length === 0); if (isLastNonEmpty) { throw new TornLineError( `Torn final line at journal line ${i + 1} — discard or inspect manually: ${line.slice(0, 80)}`, i + 1, line, ); } throw new TornLineError( `Corrupt journal line ${i + 1} (not at EOF) — hard error, inspect manually: ${line.slice(0, 80)}`, i + 1, line, ); } } return records; } export interface IncompleteOperation { opId: string; op: string; lastSeq: number; /** * RFC-1219: "interrupted" when the op recorded a step-failed or * op-interrupted marker (resumable failure) — "running" when the op is * open with no failure signal (in-flight or crashed mid-step). */ status: "interrupted" | "running"; /** Latest failed step for the op, when one was recorded. */ failedStep?: string; } /** * RFC-1219: scans every op-started newest→oldest and returns the first op * without a terminal record (op-done / op-abandoned). op-interrupted is a * non-terminal marker and does not close an op. Previously only the last * op-started was inspected — a completed later op masked an earlier * incomplete one (warpgogol-m000192 class of failure). */ export function findIncompleteOperation(records: JournalRecord[]): IncompleteOperation | null { const terminalOpIds = new Set( records.filter((r) => r.kind === "op-done" || r.kind === "op-abandoned").map((r) => r.opId), ); const opStarted = records.filter((r) => r.kind === "op-started"); for (let i = opStarted.length - 1; i >= 0; i--) { const started = opStarted[i]; if (terminalOpIds.has(started.opId)) { continue; } let lastSeq = -1; let failedStep: string | undefined; for (const r of records) { if (r.opId !== started.opId) continue; if ( (r.kind === "step-started" || r.kind === "step-done" || r.kind === "step-failed" || r.kind === "step-skipped") && r.seq > lastSeq ) { lastSeq = r.seq; } if (r.kind === "step-failed" || r.kind === "op-interrupted") { failedStep = r.kind === "step-failed" ? r.step : r.failedStep; } } return { opId: started.opId, op: started.op, lastSeq, status: failedStep !== undefined ? "interrupted" : "running", ...(failedStep !== undefined ? { failedStep } : {}), }; } return null; }