import type { ProcessObservation } from "./processObservation.js"; import { matchProcessReactiveTrigger } from "./processReactiveMatching.js"; import { type ProcessReactiveAction, type ProcessReactiveScenario, } from "./processReactiveScenario.js"; export { commitProcessReactiveProposal } from "./processReactiveTransition.js"; /** Terminal and failure outcomes produced by the pure reactive reducer. */ export type ProcessReactiveOutcome = | "passed" | "predicate_timeout" | "scenario_deadline" | "ambiguous_match" | "action_rejected" | "target_lost" | "capture_incomplete" | "cancelled" | "cleanup_failed"; /** Lifecycle state shared by the reducer snapshot and its durable projection. */ export type ProcessReactiveStatus = | { readonly status: "running"; readonly outcome: null } | { readonly status: "finished"; readonly outcome: ProcessReactiveOutcome; }; interface ProcessReactiveTransitionState { readonly sequence: number; readonly transition_id: string; readonly state_before: string; readonly trigger_event_ids: readonly string[]; readonly action_event_ids: readonly string[]; readonly action_types: readonly ProcessReactiveAction["type"][]; } /** One deterministic scenario transition suitable for later Evidence projection. */ export type ProcessReactiveTransitionRecord = ProcessReactiveTransitionState & ( | { readonly state_after: string; readonly outcome: null } | { readonly state_after: null; readonly outcome: "passed" } ); interface Counter { readonly id: string; readonly count: number; } interface ReactiveCheckpoint { readonly name: string; readonly event_id: string; readonly capture_order: number; } interface ProcessReactiveSnapshotState { readonly active_state: string; readonly state_entry_capture_order: number; readonly observations: readonly ProcessObservation[]; readonly consumed_event_ids: readonly string[]; readonly transition_uses: readonly Counter[]; readonly state_visits: readonly Counter[]; readonly checkpoints: readonly ReactiveCheckpoint[]; readonly transitions: readonly ProcessReactiveTransitionRecord[]; } /** Immutable reducer state owned by one validated reactive scenario run. */ export type ProcessReactiveSnapshot = ProcessReactiveSnapshotState & ProcessReactiveStatus; /** External fact offered to the pure reducer. */ export type ProcessReactiveInput = | { readonly kind: "observation"; readonly observation: ProcessObservation } | { readonly kind: "state_deadline"; readonly state_id: string; readonly state_entry_capture_order: number; } | { readonly kind: "scenario_deadline" } | { readonly kind: "target_lost" } | { readonly kind: "cancelled" } | { readonly kind: "cleanup_failed" }; /** Per-transition matching summary returned without caller payloads. */ export interface ProcessReactiveEvaluation { readonly transition_id: string; readonly status: "matched" | "unmatched" | "exhausted"; readonly matched_event_ids: readonly string[]; } /** Host acknowledgement for one proposed declarative effect. */ export type ProcessReactiveEffectResult = | { readonly status: "succeeded"; readonly observation: ProcessObservation; } | { readonly status: "rejected" } | { readonly status: "target_lost" }; /** Immutable transition proposal awaiting host effect results. */ export interface ProcessReactiveTransitionProposal { readonly transition_id: string; readonly transition_identity: string; readonly observation_event_id: string; readonly trigger_event_ids: readonly string[]; readonly consume_event_ids: readonly string[]; } /** Pure scenario decision; effects remain declarative for an authorized host. */ export type ProcessReactiveDecision = | { readonly kind: "waiting" | "already_finished"; readonly snapshot: ProcessReactiveSnapshot; readonly evaluations: readonly ProcessReactiveEvaluation[]; } | { readonly kind: "proposal"; readonly snapshot: ProcessReactiveSnapshot; readonly evaluations: readonly ProcessReactiveEvaluation[]; readonly effects: readonly ProcessReactiveAction[]; readonly proposal: ProcessReactiveTransitionProposal; } | { readonly kind: "transition"; readonly snapshot: ProcessReactiveSnapshot; readonly evaluations: readonly ProcessReactiveEvaluation[]; readonly record: ProcessReactiveTransitionRecord; } | { readonly kind: "finished"; readonly snapshot: ProcessReactiveSnapshot; readonly evaluations: readonly ProcessReactiveEvaluation[]; readonly outcome: ProcessReactiveOutcome; }; const counterValue = (counters: readonly Counter[], id: string): number => counters.find((counter) => counter.id === id)?.count ?? 0; type ObservationAdmission = | { readonly kind: "new"; readonly snapshot: ProcessReactiveSnapshot } | { readonly kind: "invalid" }; const observeInput = ( snapshot: ProcessReactiveSnapshot, observation: ProcessObservation, ): ObservationAdmission => { const duplicate = snapshot.observations.find( ({ event_id }) => event_id === observation.event_id, ); if (duplicate !== undefined) return { kind: "invalid" }; const prior = snapshot.observations.at(-1); if (prior !== undefined && observation.capture_order <= prior.capture_order) return { kind: "invalid" }; return { kind: "new", snapshot: { ...snapshot, observations: [...snapshot.observations, observation], }, }; }; /** Create the initial immutable reducer state for one validated scenario. */ export const createProcessReactiveSnapshot = ( scenario: ProcessReactiveScenario, ): ProcessReactiveSnapshot => ({ status: "running", outcome: null, active_state: scenario.initial_state, state_entry_capture_order: 0, observations: [], consumed_event_ids: [], transition_uses: [], state_visits: [{ id: scenario.initial_state, count: 1 }], checkpoints: [], transitions: [], }); const finishedDecision = ( snapshot: ProcessReactiveSnapshot, outcome: ProcessReactiveOutcome, evaluations: readonly ProcessReactiveEvaluation[] = [], ): ProcessReactiveDecision => ({ kind: "finished", outcome, evaluations, snapshot: { ...snapshot, status: "finished", outcome }, }); const reduceControlInput = ( snapshot: ProcessReactiveSnapshot, input: ProcessReactiveInput, ): ProcessReactiveDecision | null => { if (input.kind === "cleanup_failed") return finishedDecision(snapshot, "cleanup_failed"); if (snapshot.status === "finished") return { kind: "already_finished", snapshot, evaluations: [] }; if (input.kind === "observation") return null; if (input.kind === "state_deadline") return input.state_id === snapshot.active_state && input.state_entry_capture_order === snapshot.state_entry_capture_order ? finishedDecision(snapshot, "predicate_timeout") : { kind: "waiting", snapshot, evaluations: [] }; if (input.kind === "scenario_deadline") return finishedDecision(snapshot, "scenario_deadline"); if (input.kind === "target_lost") return finishedDecision(snapshot, "target_lost"); return finishedDecision(snapshot, "cancelled"); }; /** Advance one validated scenario using only an observation or explicit deadline. */ export const reduceProcessReactiveScenario = ( scenario: ProcessReactiveScenario, snapshot: ProcessReactiveSnapshot, input: ProcessReactiveInput, ): ProcessReactiveDecision => { const controlDecision = reduceControlInput(snapshot, input); if (controlDecision !== null) return controlDecision; if (input.kind !== "observation") return finishedDecision(snapshot, "capture_incomplete"); const admitted = observeInput(snapshot, input.observation); if (admitted.kind === "invalid") return finishedDecision(snapshot, "capture_incomplete"); const observed = admitted.snapshot; const state = scenario.states.find(({ id }) => id === observed.active_state); if (state === undefined) return finishedDecision(observed, "target_lost"); const matches = state.on.map((transition) => ({ transition, match: matchProcessReactiveTrigger(transition.when, observed), uses: counterValue(observed.transition_uses, transition.id), })); const evaluations: ProcessReactiveEvaluation[] = matches.map( ({ transition, match, uses }) => ({ transition_id: transition.id, status: uses >= transition.max_uses ? "exhausted" : match.matched ? "matched" : "unmatched", matched_event_ids: match.eventIds, }), ); const eligible = matches.filter( ({ transition, match, uses }) => match.matched && uses < transition.max_uses, ); if (eligible.length === 0) return { kind: "waiting", snapshot: observed, evaluations }; const minimumPriority = eligible.reduce( (minimum, { transition }) => Math.min(minimum, transition.priority), Number.POSITIVE_INFINITY, ); const winners = eligible.filter( ({ transition }) => transition.priority === minimumPriority, ); if (winners.length !== 1) return finishedDecision(observed, "ambiguous_match", evaluations); const winner = winners[0]; if (winner === undefined) return finishedDecision(observed, "capture_incomplete", evaluations); const targetDeclaration = winner.transition.target; if (targetDeclaration.kind === "goto") { const target = scenario.states.find( ({ id }) => id === targetDeclaration.state, ); if ( target === undefined || counterValue(observed.state_visits, target.id) >= target.max_visits ) return finishedDecision(observed, "capture_incomplete", evaluations); } return { kind: "proposal", snapshot: observed, evaluations, effects: winner.transition.actions, proposal: { transition_id: winner.transition.id, transition_identity: canonicalize(winner.transition) ?? "", observation_event_id: input.observation.event_id, trigger_event_ids: winner.match.eventIds, consume_event_ids: winner.match.consumeIds, }, }; }; import canonicalize from "canonicalize";