/* RFC-1037: Persistent compensation evidence store using better-sqlite3. Stores CompensationResult records keyed by operationHash so that compensation evidence survives process restarts and can be re-verified later. Does not implement probe execution — that lives in compensation-verifier.ts. Does not implement Bordbuch recording — callers record Bordbuch entries separately. RFC-1037: initial implementation — CompensationStore with SQLite-backed persistence. RFC-1254: drop unused io-migration leftovers */ import { dirname } from "node:path"; import { createRequire } from "node:module"; import type { CompensationResult, ProbeResult } from "@warpgogol/werkstatt-shared/component"; import type { Sha256Digest } from "@warpgogol/werkstatt-shared/fingerprint"; import { type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; const require_ = createRequire(import.meta.url); const SCHEMA_DDL = ` CREATE TABLE IF NOT EXISTS compensation_results ( operation_hash TEXT PRIMARY KEY, compensation_hash TEXT NOT NULL, verified INTEGER NOT NULL, probe_results TEXT NOT NULL, verified_at TEXT NOT NULL, failure_reason TEXT ); CREATE INDEX IF NOT EXISTS idx_compensation_verified ON compensation_results(verified); `; interface CompensationRow { operation_hash: string; compensation_hash: string; verified: number; probe_results: string; verified_at: string; failure_reason: string | null; } export class CompensationStore { private readonly dbPath: string; private db: import("better-sqlite3").Database; private constructor(dbPath: string, db: import("better-sqlite3").Database) { this.dbPath = dbPath; this.db = db; } static async create(dbPath: string, io: WorkspaceIO): Promise { const open = (): import("better-sqlite3").Database => { const BetterSqlite3 = require_("better-sqlite3") as typeof import("better-sqlite3"); const db = new BetterSqlite3(dbPath); db.pragma("journal_mode = WAL"); db.pragma("busy_timeout = 5000"); db.exec(SCHEMA_DDL); return db; }; try { await io.mkdir(dirname(dbPath)); return new CompensationStore(dbPath, open()); } catch (err) { if (await io.exists(dbPath)) { try { await io.rm(dbPath); } catch { // ignore rm errors } } await io.mkdir(dirname(dbPath)); const db = open(); if (err instanceof Error) { console.warn(`compensation-evidence DB was corrupt and has been recreated: ${err.message}`); } return new CompensationStore(dbPath, db); } } save(result: CompensationResult): void { const stmt = this.db.prepare( `INSERT OR REPLACE INTO compensation_results (operation_hash, compensation_hash, verified, probe_results, verified_at, failure_reason) VALUES (?, ?, ?, ?, ?, ?)`, ); stmt.run( result.operationHash, result.compensationHash, result.verified ? 1 : 0, JSON.stringify(result.probeResults), result.verifiedAt, result.failureReason ?? null, ); } get(operationHash: Sha256Digest): CompensationResult | null { const row = this.db .prepare<[string]>("SELECT * FROM compensation_results WHERE operation_hash = ?") .get(operationHash) as CompensationRow | undefined; if (!row) { return null; } return deserializeRow(row); } close(): void { try { this.db.close(); } catch { // ignore close errors } } } function deserializeRow(row: CompensationRow): CompensationResult { const probeResults = JSON.parse(row.probe_results) as ProbeResult[]; const operationHash = row.operation_hash as Sha256Digest; const compensationHash = row.compensation_hash as Sha256Digest; return { operationHash, compensationHash, verified: row.verified === 1, probeResults, verifiedAt: row.verified_at, failureReason: row.failure_reason ?? undefined, }; } export function resolveCompensationStorePath(missionsDir: string, missionId: string): string { return `${missionsDir}/${missionId}/compensation-evidence.db`; }