import { RUN_STATES, type RunState } from "./workflow-types.js"; export { RUN_STATES, type RunState }; export interface RunGateFacts { readonly objective: { readonly evidenceComplete: boolean; readonly verifierPassed: boolean; readonly prerequisitesComplete: boolean; }; readonly workers: { readonly active: boolean; readonly uncertain: boolean; readonly detached: boolean; }; readonly cancellation: { readonly requested: boolean; readonly settled: boolean; }; readonly input: { readonly delivered: boolean }; readonly reconciliation: { readonly recorded: boolean; readonly uncertaintyCleared: boolean }; readonly attention: { readonly cleared: boolean }; } export type RunGateGuard = | "objective.evidence.complete" | "objective.verifier.passed" | "objective.prerequisites.complete" | "workers.none_active" | "workers.none_uncertain" | "workers.none_detached" | "cancellation.requested" | "cancellation.settled" | "input.delivered" | "reconciliation.recorded" | "reconciliation.uncertainty_cleared" | "attention.cleared"; export interface RunTransitionRule { readonly from: RunState; readonly to: RunState; readonly guards: readonly RunGateGuard[]; } const DONE_GUARDS: readonly RunGateGuard[] = [ "objective.evidence.complete", "objective.verifier.passed", "objective.prerequisites.complete", "workers.none_active", "workers.none_uncertain", "workers.none_detached", ]; const RECONCILED: readonly RunGateGuard[] = [ "reconciliation.recorded", "reconciliation.uncertainty_cleared", ]; export const RUN_LEGALITY_TABLE: readonly RunTransitionRule[] = [ { from: "open", to: "verifying", guards: [] }, { from: "open", to: "blocked", guards: [] }, { from: "open", to: "cancel-requested", guards: ["cancellation.requested"] }, { from: "open", to: "ambiguous", guards: [] }, { from: "open", to: "needs-attention", guards: [] }, { from: "verifying", to: "open", guards: [] }, { from: "verifying", to: "blocked", guards: [] }, { from: "verifying", to: "done", guards: DONE_GUARDS }, { from: "verifying", to: "cancel-requested", guards: ["cancellation.requested"] }, { from: "verifying", to: "ambiguous", guards: [] }, { from: "verifying", to: "needs-attention", guards: [] }, { from: "blocked", to: "open", guards: ["input.delivered"] }, { from: "blocked", to: "cancel-requested", guards: ["cancellation.requested"] }, { from: "blocked", to: "ambiguous", guards: [] }, { from: "blocked", to: "needs-attention", guards: [] }, { from: "cancel-requested", to: "cancelled", guards: ["cancellation.settled", "workers.none_uncertain"] }, { from: "cancel-requested", to: "ambiguous", guards: [] }, { from: "cancel-requested", to: "needs-attention", guards: [] }, { from: "ambiguous", to: "open", guards: RECONCILED }, { from: "ambiguous", to: "verifying", guards: RECONCILED }, { from: "ambiguous", to: "blocked", guards: RECONCILED }, { from: "ambiguous", to: "cancel-requested", guards: [...RECONCILED, "cancellation.requested"] }, { from: "ambiguous", to: "cancelled", guards: [...RECONCILED, "cancellation.settled"] }, { from: "ambiguous", to: "done", guards: [...RECONCILED, ...DONE_GUARDS] }, { from: "ambiguous", to: "needs-attention", guards: RECONCILED }, { from: "needs-attention", to: "open", guards: ["attention.cleared", "workers.none_detached"] }, { from: "needs-attention", to: "verifying", guards: ["attention.cleared", "workers.none_detached"] }, { from: "needs-attention", to: "blocked", guards: ["attention.cleared"] }, { from: "needs-attention", to: "cancel-requested", guards: ["cancellation.requested"] }, { from: "needs-attention", to: "ambiguous", guards: [] }, ] as const; export function evaluateRunTransition( from: RunState, to: RunState, facts: RunGateFacts, ): { readonly allowed: true } | { readonly allowed: false; readonly reason: "undeclared-transition" | "guard-failed"; readonly blockers: readonly RunGateGuard[]; } { const rule = RUN_LEGALITY_TABLE.find((candidate) => candidate.from === from && candidate.to === to); if (!rule) return { allowed: false, reason: "undeclared-transition", blockers: [] }; const blockers = rule.guards.filter((guard) => !guardSatisfied(guard, facts)); return blockers.length === 0 ? { allowed: true } : { allowed: false, reason: "guard-failed", blockers }; } function guardSatisfied(guard: RunGateGuard, facts: RunGateFacts): boolean { switch (guard) { case "objective.evidence.complete": return facts.objective.evidenceComplete; case "objective.verifier.passed": return facts.objective.verifierPassed; case "objective.prerequisites.complete": return facts.objective.prerequisitesComplete; case "workers.none_active": return !facts.workers.active; case "workers.none_uncertain": return !facts.workers.uncertain; case "workers.none_detached": return !facts.workers.detached; case "cancellation.requested": return facts.cancellation.requested; case "cancellation.settled": return facts.cancellation.settled; case "input.delivered": return facts.input.delivered; case "reconciliation.recorded": return facts.reconciliation.recorded; case "reconciliation.uncertainty_cleared": return facts.reconciliation.uncertaintyCleared; case "attention.cleared": return facts.attention.cleared; } }