// lib/journal.ts — per-incarnation append-only JSONL journal (PRD R6, NG-3). // // One file per incarnation identity (the NG-1 process-launch identity — // NEVER PI_SESSION_ID) under the XDG state home: // ${XDG_STATE_HOME:-~/.local/state}/mosaic-core/.jsonl // // Append-only is structural: the only write primitive is appendFileSync of a // newline-terminated JSON line. No read-modify-write, no truncation, no // rewrite — a journal that can be edited is a journal that cannot prove // anything. Event payloads are pre-sanitized by their producers (the denial // event carries tool+reason only; proposals pass the proposal sanitizer). import { appendFileSync, mkdirSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; export type JournalKind = "reconciliation" | "denial" | "improvement-proposal"; export interface JournalEvent { ts: string; incarnationId: string; kind: JournalKind; [field: string]: unknown; } export interface JournalIO { mkdirSync(path: string, opts: { recursive: boolean }): void; appendFileSync(path: string, data: string): void; } function defaultIO(): JournalIO { return { mkdirSync, appendFileSync }; } export function journalDir(stateHome: string | undefined, home: string = homedir()): string { return join(stateHome ?? join(home, ".local", "state"), "mosaic-core"); } export interface Journal { path: string; append(event: Omit): void; } export function createJournal(opts: { incarnationId: string; stateHome?: string; io?: JournalIO; now?: () => string; }): Journal { const io = opts.io ?? defaultIO(); const now = opts.now ?? (() => new Date().toISOString()); const dir = journalDir(opts.stateHome); const path = join(dir, `${opts.incarnationId}.jsonl`); io.mkdirSync(dir, { recursive: true }); return { path, append(event) { const line = `${JSON.stringify({ ts: now(), incarnationId: opts.incarnationId, ...event })}\n`; io.appendFileSync(path, line); }, }; }