1/** 2 * apps/worker — automation run state machine (TASK-0113 §2). 3 * 4 * The ONLY place run-status transitions are validated. `canTransition` is the 5 * pure predicate; `assertTransition` throws on an invalid move (a corrupt 6 * update from a bug or a concurrent writer must be loud, not silent). 7 * 8 * Valid transitions: 9 * PENDING → RUNNING (processor start, startedAt = now) 10 * RUNNING → SUCCEEDED (terminal: every action done / skipped) 11 * RUNNING → FAILED (fatal error, or a retryable error recorded 12 * before the retry decision) 13 * FAILED → PENDING (retry: same runId, retryCount + 1, < cap) 14 * FAILED → DEAD_LETTER (terminal: retryCount reached the cap) 15 * PENDING → FAILED (watchdog: stale at the retry cap) 16 * DEAD_LETTER → PENDING (manual retry via the API, retryCount = 0) 17 * 18 * SUCCEEDED / DEAD_LETTER are terminal (no outgoing edges). 19 */ 20import type { AutomationRunStatus } from "@prisma/client"; 21 22export const TERMINAL: readonly AutomationRunStatus[] = ["SUCCEEDED", "DEAD_LETTER"] as const; 23 24const VALID: Record<AutomationRunStatus, readonly AutomationRunStatus[]> = { 25 PENDING: ["RUNNING", "FAILED"], 26 RUNNING: ["SUCCEEDED", "FAILED", "DEAD_LETTER"], 27 FAILED: ["PENDING", "DEAD_LETTER"], 28 SUCCEEDED: [], 29 DEAD_LETTER: ["PENDING"] 30}; 31 32/** Pure transition predicate. */ 33export function canTransition(from: AutomationRunStatus, to: AutomationRunStatus): boolean { 34 return (VALID[from] ?? []).includes(to); 35} 36 37/** Throw on an invalid transition (the single enforcement point). */ 38export function assertTransition(from: AutomationRunStatus, to: AutomationRunStatus): void { 39 if (!canTransition(from, to)) { 40 throw new Error(`invalid automation run transition: ${from} → ${to}`); 41 } 42} 43 44export function isTerminal(status: AutomationRunStatus): boolean { 45 return (TERMINAL as readonly string[]).includes(status); 46} 47