/* effects — component runtime effect outcome type definitions for effect handlers. Do not run effects — this module defines the outcome contract. 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 type { EffectClass, EffectDeclarationV1, CompensationAction, } from "@warpgogol/werkstatt-shared/component"; import type { ComponentId } from "@warpgogol/werkstatt-shared/component"; export type EffectOutcome = "committed" | "aborted" | "compensated" | "withheld" | "failed-rollback" | "quarantined"; export interface EffectUnwindEntryV1 { readonly componentId: ComponentId; readonly effectClass: EffectClass; readonly outcome: EffectOutcome; readonly error: string | null; } export interface EffectUnwindReportV1 { readonly entries: readonly EffectUnwindEntryV1[]; readonly allSucceeded: boolean; readonly quarantined: boolean; } export interface EffectHandlerResult { readonly ok: boolean; readonly outcome: EffectOutcome; readonly error: string | null; } export interface EffectHandler { readonly effectClass: EffectClass; prepare(): Promise; commit(): Promise; abort(): Promise; compensate(): Promise; dispose(): Promise; } export class RevertibleEffectHandler implements EffectHandler { readonly effectClass: EffectClass = "revertible"; private disposed = false; private readonly disposer: () => Promise; private readonly onFail: (error: string) => void; constructor(disposer: () => Promise, onFail?: (error: string) => void) { this.disposer = disposer; this.onFail = onFail ?? (() => {}); } async prepare(): Promise { return { ok: true, outcome: "committed", error: null }; } async commit(): Promise { return { ok: true, outcome: "committed", error: null }; } async abort(): Promise { if (this.disposed) { return { ok: true, outcome: "aborted", error: null }; } return this.dispose(); } async compensate(): Promise { return { ok: false, outcome: "failed-rollback", error: "revertible effects do not support compensation; use dispose", }; } async dispose(): Promise { if (this.disposed) { return { ok: true, outcome: "aborted", error: null }; } try { await this.disposer(); this.disposed = true; return { ok: true, outcome: "aborted", error: null }; } catch (e) { const msg = e instanceof Error ? e.message : String(e); this.onFail(msg); return { ok: false, outcome: "failed-rollback", error: msg }; } } } export class TransactionalEffectHandler implements EffectHandler { readonly effectClass: EffectClass = "transactional"; private prepared = false; private committed = false; private aborted = false; private readonly prepareFn: () => Promise; private readonly commitFn: () => Promise; private readonly abortFn: () => Promise; private readonly idempotencyKey: string; constructor( idempotencyKey: string, prepareFn: () => Promise, commitFn: () => Promise, abortFn: () => Promise, ) { this.idempotencyKey = idempotencyKey; this.prepareFn = prepareFn; this.commitFn = commitFn; this.abortFn = abortFn; } async prepare(): Promise { if (this.prepared || this.committed || this.aborted) { return { ok: true, outcome: "committed", error: null }; } try { await this.prepareFn(); this.prepared = true; return { ok: true, outcome: "committed", error: null }; } catch (e) { return { ok: false, outcome: "failed-rollback", error: e instanceof Error ? e.message : String(e), }; } } async commit(): Promise { if (this.committed) { return { ok: true, outcome: "committed", error: null }; } if (!this.prepared) { return { ok: false, outcome: "failed-rollback", error: "commit called before prepare" }; } try { await this.commitFn(); this.committed = true; return { ok: true, outcome: "committed", error: null }; } catch (e) { return { ok: false, outcome: "failed-rollback", error: e instanceof Error ? e.message : String(e), }; } } async abort(): Promise { if (this.aborted || this.committed) { return { ok: true, outcome: "aborted", error: null }; } try { await this.abortFn(); this.aborted = true; return { ok: true, outcome: "aborted", error: null }; } catch (e) { return { ok: false, outcome: "failed-rollback", error: e instanceof Error ? e.message : String(e), }; } } async compensate(): Promise { return { ok: false, outcome: "failed-rollback", error: "transactional effects use abort, not compensation", }; } async dispose(): Promise { if (this.committed) { return { ok: true, outcome: "committed", error: null }; } if (!this.aborted) { return this.abort(); } return { ok: true, outcome: "aborted", error: null }; } } export class CompensatableEffectHandler implements EffectHandler { readonly effectClass: EffectClass = "compensatable"; private committed = false; private compensated = false; private readonly commitFn: () => Promise; private readonly compensateFn: () => Promise; private readonly compensationAction: CompensationAction; constructor( commitFn: () => Promise, compensateFn: () => Promise, compensationAction: CompensationAction, ) { if (compensationAction.verificationProbes.length === 0) { throw new Error("compensatable effects require at least one verification probe"); } this.commitFn = commitFn; this.compensateFn = compensateFn; this.compensationAction = compensationAction; } get compensation(): CompensationAction { return this.compensationAction; } async prepare(): Promise { return { ok: true, outcome: "committed", error: null }; } async commit(): Promise { if (this.committed) { return { ok: true, outcome: "committed", error: null }; } try { await this.commitFn(); this.committed = true; return { ok: true, outcome: "committed", error: null }; } catch (e) { return { ok: false, outcome: "failed-rollback", error: e instanceof Error ? e.message : String(e), }; } } async abort(): Promise { if (!this.committed) { return { ok: true, outcome: "aborted", error: null }; } return this.compensate(); } async compensate(): Promise { if (this.compensated) { return { ok: true, outcome: "compensated", error: null }; } if (!this.committed) { return { ok: true, outcome: "aborted", error: null }; } try { await this.compensateFn(); this.compensated = true; return { ok: true, outcome: "compensated", error: null }; } catch (e) { return { ok: false, outcome: "quarantined", error: e instanceof Error ? e.message : String(e), }; } } async dispose(): Promise { if (this.committed && !this.compensated) { return this.compensate(); } return { ok: true, outcome: this.compensated ? "compensated" : "aborted", error: null }; } } export class IrreversibleEmissionEffectHandler implements EffectHandler { readonly effectClass: EffectClass = "irreversible-emission"; private withheld = true; private emitted = false; private readonly emitFn: () => Promise; constructor(emitFn: () => Promise) { this.emitFn = emitFn; } async prepare(): Promise { return { ok: true, outcome: "withheld", error: null }; } async commit(): Promise { if (this.emitted) { return { ok: true, outcome: "committed", error: null }; } try { await this.emitFn(); this.emitted = true; this.withheld = false; return { ok: true, outcome: "committed", error: null }; } catch (e) { return { ok: false, outcome: "failed-rollback", error: e instanceof Error ? e.message : String(e), }; } } async abort(): Promise { if (this.emitted) { return { ok: false, outcome: "quarantined", error: "irreversible emission already emitted; cannot abort", }; } this.withheld = false; return { ok: true, outcome: "aborted", error: null }; } async compensate(): Promise { if (this.emitted) { return { ok: false, outcome: "quarantined", error: "irreversible emissions cannot be compensated", }; } return { ok: true, outcome: "withheld", error: null }; } async dispose(): Promise { if (this.withheld && !this.emitted) { return { ok: true, outcome: "withheld", error: null }; } if (this.emitted) { return { ok: true, outcome: "committed", error: null }; } return { ok: true, outcome: "aborted", error: null }; } get isWithheld(): boolean { return this.withheld; } } export function createEffectHandler( declaration: EffectDeclarationV1, handlers: { disposer?: () => Promise; prepare?: () => Promise; commit?: () => Promise; abort?: () => Promise; compensate?: () => Promise; emit?: () => Promise; compensationAction?: CompensationAction; }, ): EffectHandler { switch (declaration.effectClass) { case "revertible": return new RevertibleEffectHandler(handlers.disposer ?? (async () => {})); case "transactional": return new TransactionalEffectHandler( declaration.commitMetadata ?? "tx-key", handlers.prepare ?? (async () => {}), handlers.commit ?? (async () => {}), handlers.abort ?? (async () => {}), ); case "compensatable": { if (!handlers.compensationAction) { throw new Error("EFFECT-02: compensatable effects require a compensationAction"); } return new CompensatableEffectHandler( handlers.commit ?? (async () => {}), handlers.compensate ?? (async () => {}), handlers.compensationAction, ); } case "irreversible-emission": return new IrreversibleEmissionEffectHandler(handlers.emit ?? (async () => {})); default: { const _exhaustive: never = declaration.effectClass; throw new Error(`unknown effect class: ${_exhaustive}`); } } } export function buildUnwindReport(entries: readonly EffectUnwindEntryV1[]): EffectUnwindReportV1 { const allSucceeded = entries.every( (e) => e.outcome === "aborted" || e.outcome === "compensated" || e.outcome === "withheld" || e.outcome === "committed", ); const quarantined = entries.some( (e) => e.outcome === "quarantined" || e.outcome === "failed-rollback", ); return { entries, allSucceeded: allSucceeded && !quarantined, quarantined }; }