import { PatchValidationError } from "./errors.ts"; import type { PlanningDependency, Transition, TransitionPlanning, TransitionPlanningInput, WorkflowState, } from "./types.ts"; function validateDependencyShape(dependency: PlanningDependency): void { switch (dependency.kind) { case "place_state": if (!dependency.placeId || !dependency.state) { throw new PatchValidationError("place_state dependency requires placeId and state"); } if (dependency.state !== "satisfied" && dependency.state !== "invalidated") { throw new PatchValidationError(`Invalid place_state dependency state: ${String(dependency.state)}`); } return; case "gate_state": if (!dependency.placeId || !dependency.state) { throw new PatchValidationError("gate_state dependency requires placeId and state"); } if (dependency.state !== "open" && dependency.state !== "closed" && dependency.state !== "blocked") { throw new PatchValidationError(`Invalid gate_state dependency state: ${String(dependency.state)}`); } return; case "transition_outcome": if (!dependency.transitionId || !dependency.outcome) { throw new PatchValidationError("transition_outcome dependency requires transitionId and outcome"); } return; case "evidence_selector": if (!dependency.selector) throw new PatchValidationError("evidence_selector dependency requires selector"); return; } } export function normalizePlanning( input: TransitionPlanningInput, source: TransitionPlanning["source"], requiresRefinement = input.commitment === "directional", ): TransitionPlanning { if (!input?.commitment) throw new PatchValidationError("create_transition requires planning.commitment"); const planning: TransitionPlanning = { commitment: input.commitment, dependsOn: structuredClone(input.dependsOn ?? []), requiresRefinement, source, ...(input.reconsiderWhen ? { reconsiderWhen: input.reconsiderWhen } : {}), ...(input.refinementTrigger ? { refinementTrigger: input.refinementTrigger } : {}), }; for (const dependency of planning.dependsOn) validateDependencyShape(dependency); if (planning.commitment === "provisional" && planning.dependsOn.length === 0 && !planning.reconsiderWhen) { throw new PatchValidationError("Provisional transition requires dependsOn or reconsiderWhen"); } return planning; } function evidenceMatches(state: WorkflowState, selector: string): boolean { const normalized = selector.trim().toLowerCase(); return Object.values(state.evidence).some((evidence) => evidence.selectors.some((candidate) => candidate.trim().toLowerCase() === normalized), ); } export function dependencySatisfied(state: WorkflowState, dependency: PlanningDependency): boolean { switch (dependency.kind) { case "place_state": return state.places[dependency.placeId]?.state === dependency.state; case "gate_state": return state.places[dependency.placeId]?.gateState === dependency.state; case "transition_outcome": { const status = state.transitions[dependency.transitionId]?.status; return dependency.outcome === "success" ? status === "completed" : status === "failed"; } case "evidence_selector": return evidenceMatches(state, dependency.selector); } } export function validatePlanningDependencies(state: WorkflowState, transition: Transition): void { const unmet = transition.planning.dependsOn.filter((dependency) => !dependencySatisfied(state, dependency)); if (unmet.length > 0) { throw new PatchValidationError( `Transition ${transition.id} has unmet planning dependencies: ${unmet.map((item) => item.kind).join(", ")}`, ); } } export function validateTransitionPlanning(state: WorkflowState, transition: Transition): void { for (const dependency of transition.planning.dependsOn) { validateDependencyShape(dependency); if (dependency.kind === "place_state" || dependency.kind === "gate_state") { if (!state.places[dependency.placeId]) { throw new PatchValidationError(`Planning dependency Place not found: ${dependency.placeId}`); } } else if (dependency.kind === "transition_outcome" && !state.transitions[dependency.transitionId]) { throw new PatchValidationError(`Planning dependency Transition not found: ${dependency.transitionId}`); } } if ( transition.planning.commitment === "provisional" && transition.planning.dependsOn.length === 0 && !transition.planning.reconsiderWhen ) { throw new PatchValidationError(`Provisional transition ${transition.id} requires dependsOn or reconsiderWhen`); } if (transition.status === "active" && transition.planning.commitment !== "committed") { throw new PatchValidationError(`Active transition ${transition.id} must be committed`); } }