/*
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`;
}