UNPKG

@exaflow/core

Version:

Core package for exaflow flow execution framework

1,639 lines (1,624 loc) 86.2 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { CelEvaluator: () => CelEvaluator, FlowAnalyzer: () => FlowAnalyzer, FlowEngine: () => FlowEngine, FlowValidator: () => FlowValidator, PathTester: () => PathTester, StateManager: () => StateManager, inferNodeTypes: () => inferNodeTypes, runPathTests: () => runPathTests, validateFlow: () => validateFlow }); module.exports = __toCommonJS(index_exports); // src/core/flow-engine.ts var import_eventemitter32 = require("eventemitter3"); // src/core/state-manager.ts var import_eventemitter3 = require("eventemitter3"); // src/core/cel-evaluator.ts var import_cel_js = require("cel-js"); var CelEvaluator = class { constructor() { this.cache = /* @__PURE__ */ new Map(); } compile(expression, options) { const parsed = (0, import_cel_js.parse)(expression); if (parsed.isSuccess && options?.cache) { this.cache.set(expression, parsed.cst); } return { success: parsed.isSuccess, error: !parsed.isSuccess ? parsed.errors.join(", ") : void 0 }; } evaluate(expression, context, { onError } = { onError: void 0 }) { if (!expression || typeof expression !== "string") return void 0; try { let cst = this.cache.get(expression); if (!cst) { const parsed = (0, import_cel_js.parse)(expression); if (parsed.isSuccess) { cst = parsed.cst; this.cache.set(expression, cst); } else { onError?.(new Error(parsed.errors.join(", "))); return void 0; } } const result = (0, import_cel_js.evaluate)( cst, context && typeof context === "object" ? { ...context } : {} ); return result; } catch (err) { onError?.(err instanceof Error ? err : new Error(err)); return void 0; } } clearCache() { this.cache.clear(); } }; // src/utils/evaluator-factory.ts var EvaluatorFactory = class { /** * Get the default set of evaluators (singleton pattern for performance) */ static getDefaultEvaluators() { if (!this.defaultEvaluators) { this.defaultEvaluators = { cel: new CelEvaluator() }; } return this.defaultEvaluators; } /** * Create evaluators with optional custom ones */ static createEvaluators(customEvaluators) { if (customEvaluators) { return { ...this.getDefaultEvaluators(), ...customEvaluators }; } return this.getDefaultEvaluators(); } /** * Get a specific evaluator by language */ static getEvaluator(language, evaluators) { const allEvaluators = evaluators ?? this.getDefaultEvaluators(); const evaluator = allEvaluators[language]; if (!evaluator) { throw new Error(`No evaluator found for language: ${language}`); } return evaluator; } }; EvaluatorFactory.defaultEvaluators = null; // src/utils/error-handler.ts var XSYSErrorHandler = class { constructor(enableLogging = false) { this.enableLogging = enableLogging; } /** * Create a standardized XSYS error */ createError(code, message, context, cause) { return { code, message, context, cause }; } /** * Handle and log errors consistently */ handleError(error, context) { let xsysError; if (this.isXSYSError(error)) { xsysError = error; } else if (error instanceof Error) { xsysError = this.createError( "UNKNOWN_ERROR", error.message, context, error ); } else { xsysError = this.createError("UNKNOWN_ERROR", String(error), context); } if (this.enableLogging) { console.error("XSYS Error:", xsysError); } return xsysError; } /** * Safe execution wrapper with error handling */ async safeExecute(operation, fallback, context) { try { const result = await operation(); return { success: true, result }; } catch (error) { const xsysError = this.handleError(error, context); return { success: false, result: fallback, error: xsysError }; } } /** * Safe synchronous execution wrapper */ safeExecuteSync(operation, fallback, context) { try { const result = operation(); return { success: true, result }; } catch (error) { const xsysError = this.handleError(error, context); return { success: false, result: fallback, error: xsysError }; } } /** * Validate if an object is an XSYS error */ isXSYSError(obj) { return typeof obj === "object" && obj !== null && "code" in obj && "message" in obj; } }; var ErrorCodes = { // Flow execution errors NODE_NOT_FOUND: "NODE_NOT_FOUND", PATH_NOT_FOUND: "PATH_NOT_FOUND", INVALID_TRANSITION: "INVALID_TRANSITION", CONDITION_EVALUATION_FAILED: "CONDITION_EVALUATION_FAILED", // State management errors STATE_ACTION_FAILED: "STATE_ACTION_FAILED", INVALID_STATE_VALUE: "INVALID_STATE_VALUE", // Validation errors VALIDATION_FAILED: "VALIDATION_FAILED", INVALID_FLOW: "INVALID_FLOW", // Expression evaluation errors EXPRESSION_COMPILATION_FAILED: "EXPRESSION_COMPILATION_FAILED", EXPRESSION_EVALUATION_FAILED: "EXPRESSION_EVALUATION_FAILED", UNSUPPORTED_EXPRESSION_LANGUAGE: "UNSUPPORTED_EXPRESSION_LANGUAGE", // General errors UNKNOWN_ERROR: "UNKNOWN_ERROR", OPERATION_TIMEOUT: "OPERATION_TIMEOUT" }; var globalErrorHandler = new XSYSErrorHandler(); function createConditionEvaluationError(condition, cause) { return globalErrorHandler.createError( ErrorCodes.CONDITION_EVALUATION_FAILED, `Failed to evaluate condition: ${condition}`, { condition }, cause ); } function createStateActionError(action, cause) { return globalErrorHandler.createError( ErrorCodes.STATE_ACTION_FAILED, `Failed to execute state action: ${action}`, { action }, cause ); } // src/utils/state-executor.ts var StateActionExecutor = class { constructor(options = {}) { this.expressionLanguage = options.expressionLanguage ?? "cel"; this.enableLogging = options.enableLogging ?? false; this.evaluators = options.evaluators ?? EvaluatorFactory.getDefaultEvaluators(); } /** * Execute a single state action on the provided state */ executeAction(action, state) { const newState = { ...state }; const errors = []; try { const { type, target, value, expression } = action; switch (type) { case "set": if (expression) { const evaluatedValue = this.evaluateExpression( expression, newState ); this.setNestedValue(newState, target, evaluatedValue); if (this.enableLogging) { console.log(`Setting ${target} to ${evaluatedValue}.`); } } else { this.setNestedValue(newState, target, value); } break; default: throw new Error(`Unknown action type: ${type}`); } return { success: true, newState, errors }; } catch (error) { const xsysError = createStateActionError( `${action.type}:${action.target}`, error instanceof Error ? error : void 0 ); errors.push(xsysError.message); return { success: false, newState: state, // Return original state on error errors }; } } /** * Execute multiple state actions in sequence */ executeActions(actions, initialState) { let currentState = { ...initialState }; const allErrors = []; for (const action of actions) { const result = this.executeAction(action, currentState); if (result.success) { currentState = result.newState; } else { allErrors.push(...result.errors); } } return { success: allErrors.length === 0, newState: currentState, errors: allErrors }; } /** * Evaluate a condition expression */ evaluateCondition(expression, state) { try { const expr = (expression || "").trim(); if (!expr) return true; const evaluator = this.evaluators[this.expressionLanguage]; if (!evaluator) { throw new Error( `Unsupported expression language: ${this.expressionLanguage}` ); } const result = evaluator.evaluate(expr, state); if (typeof result === "boolean") { return result; } if (this.enableLogging) { console.warn( `Condition ${expr} evaluated to ${result}`, "Expected boolean result" ); } return false; } catch (error) { const xsysError = createConditionEvaluationError( expression, error instanceof Error ? error : void 0 ); if (this.enableLogging) { console.warn("XSYS Condition Evaluation Error:", xsysError); } return false; } } /** * Evaluate an expression and return the result */ evaluateExpression(expression, state) { let evalError; const evaluator = this.evaluators[this.expressionLanguage]; if (!evaluator) { throw new Error( `Unsupported expression language: ${this.expressionLanguage}` ); } const value = evaluator.evaluate(expression, state, { onError: (err) => { evalError = err; } }); if (evalError) { throw evalError; } return value; } /** * Set nested value in state using dot notation */ setNestedValue(state, path, value) { if (!(typeof state === "object" && state !== null)) { throw new Error("State must be an object"); } const keys = path.split("."); const lastKey = keys.pop(); if (!lastKey) { throw new Error(`Invalid path: ${path}`); } const target = keys.reduce( (obj, key) => { if (!(key in obj)) obj[key] = {}; const prop = obj[key]; if (!prop) { throw new Error(`Invalid path: ${path}`); } if (typeof prop !== "object") { throw new Error(`Invalid path: ${path}`); } return prop; }, state ); target[lastKey] = value; } }; // src/utils/schema-validator.ts var import_ajv = __toESM(require("ajv")); var import_ajv_formats = __toESM(require("ajv-formats")); var SchemaValidator = class { constructor() { this.compiledSchemas = /* @__PURE__ */ new Map(); this.ajv = new import_ajv.default({ allErrors: true, // Collect all validation errors removeAdditional: false, // Don't remove additional properties useDefaults: true, // Apply default values from schema coerceTypes: false // Don't coerce types automatically }); (0, import_ajv_formats.default)(this.ajv); } /** * Compile and cache a JSON schema for validation */ compileSchema(schema, schemaId) { const id = schemaId || JSON.stringify(schema); let validator = this.compiledSchemas.get(id); if (!validator) { try { validator = this.ajv.compile(schema); this.compiledSchemas.set(id, validator); } catch (error) { throw new Error( `Failed to compile schema: ${error instanceof Error ? error.message : String(error)}` ); } } return validator; } /** * Validate data against a JSON schema */ validate(data, schema, schemaId) { try { const validator = this.compileSchema(schema, schemaId); const isValid = validator(data); if (isValid) { return { isValid: true, errors: [], data }; } const errors = this.formatValidationErrors(validator.errors || []); return { isValid: false, errors, data }; } catch (error) { return { isValid: false, errors: [ `Schema validation failed: ${error instanceof Error ? error.message : String(error)}` ], data }; } } /** * Validate data and throw an error if validation fails */ validateOrThrow(data, schema, schemaId) { const result = this.validate(data, schema, schemaId); if (!result.isValid) { const error = new Error( `Schema validation failed: ${result.errors.join(", ")}` ); error.validationErrors = result.errors; error.data = data; throw error; } } /** * Format Ajv validation errors into human-readable messages */ formatValidationErrors(errors) { return errors.map((error) => { const path = error.instancePath || "root"; const message = error.message || "validation failed"; switch (error.keyword) { case "required": return `Missing required property: ${error.params?.missingProperty} at ${path}`; case "type": return `Invalid type at ${path}: expected ${error.params?.type}, got ${typeof error.data}`; case "minimum": return `Value at ${path} (${error.data}) is below minimum ${error.params?.limit}`; case "maximum": return `Value at ${path} (${error.data}) is above maximum ${error.params?.limit}`; case "minLength": return `String at ${path} is too short (minimum length: ${error.params?.limit})`; case "maxLength": return `String at ${path} is too long (maximum length: ${error.params?.limit})`; case "pattern": return `String at ${path} does not match required pattern`; case "format": return `Invalid format at ${path}: expected ${error.params?.format}`; case "additionalProperties": return `Unexpected property '${error.params?.additionalProperty}' at ${path}`; default: return `Validation error at ${path}: ${message}`; } }); } /** * Clear compiled schema cache */ clearCache() { this.compiledSchemas.clear(); } /** * Get cache statistics */ getCacheStats() { return { size: this.compiledSchemas.size, schemas: Array.from(this.compiledSchemas.keys()) }; } }; var globalSchemaValidator = new SchemaValidator(); // src/core/state-manager.ts var StateManager = class extends import_eventemitter3.EventEmitter { constructor(initialState = {}, rules = [], options = {}) { super(); this.rules = rules; this.expressionLanguage = options.expressionLanguage ?? "cel"; this.evaluators = EvaluatorFactory.getDefaultEvaluators(); this.stateSchema = options.stateSchema; this.schemaValidator = globalSchemaValidator; this.validateOnChange = options.validateOnChange ?? true; this.validateState(initialState, "Initial state validation failed"); this.state = { ...initialState }; this.actionExecutor = new StateActionExecutor({ expressionLanguage: this.expressionLanguage, evaluators: this.evaluators, enableLogging: true }); } getState() { return JSON.parse(JSON.stringify(this.state)); } setState(newState) { const oldState = JSON.parse(JSON.stringify(this.state)); const updatedState = { ...this.state, ...newState }; this.validateState(updatedState, "State update validation failed"); this.state = updatedState; this.emit("stateChange", { oldState, newState: this.getState() }); this.evaluateRules(); } executeActions(actions) { const oldState = JSON.parse(JSON.stringify(this.state)); const result = this.actionExecutor.executeActions(actions, this.state); const newState = result.newState; this.validateState(newState, "Action execution validation failed"); this.state = newState; this.emit("stateChange", { oldState, newState: this.getState() }); this.evaluateRules(); } evaluateCondition(expression) { return this.actionExecutor.evaluateCondition(expression, this.state); } evaluateRules() { for (const rule of this.rules) { if (this.evaluateCondition(rule.condition)) { this.executeRule(rule); } } } executeRule(rule) { switch (rule.action) { case "forceTransition": this.emit("error", { error: new Error(`Force transition to ${rule.target}`), context: { rule, type: "forceTransition" } }); break; case "setState": if (rule.target && rule.value !== void 0) { const result = this.actionExecutor.executeAction( { type: "set", target: rule.target, value: rule.value }, this.state ); if (result.success) { this.state = result.newState; } } break; case "triggerEvent": break; } } reset(newState = {}) { const oldState = JSON.parse(JSON.stringify(this.state)); const resetState = JSON.parse(JSON.stringify(newState)); this.validateState(resetState, "State reset validation failed"); this.state = resetState; this.emit("stateChange", { oldState, newState: this.getState() }); this.evaluateRules(); } /** * Validate state against the JSON schema if one is provided */ validateState(state, errorPrefix) { if (!this.stateSchema || !this.validateOnChange) { return; } try { const result = this.schemaValidator.validate(state, this.stateSchema); if (!result.isValid) { const error = new Error(`${errorPrefix}: ${result.errors.join(", ")}`); this.emit("error", { error, context: { type: "schemaValidation", errors: result.errors } }); throw error; } } catch (error) { if (error instanceof Error) { throw error; } const validationError = new Error( `${errorPrefix}: Unexpected validation error` ); this.emit("error", { error: validationError, context: { type: "schemaValidation", originalError: error } }); throw validationError; } } }; // src/utils/content-interpolator.ts var ContentInterpolator = class { constructor(options = {}) { this.expressionLanguage = options.expressionLanguage ?? "cel"; this.enableLogging = options.enableLogging ?? false; this.evaluators = EvaluatorFactory.getDefaultEvaluators(); } /** * Interpolate expressions in content string */ interpolate(content, state) { if (!content || typeof content !== "string") { return { content: content || "", hasInterpolations: false, errors: [] }; } const errors = []; let hasInterpolations = false; const escapedPlaceholder = "___XSYS_ESCAPED_INTERPOLATION___"; let escapedInterpolations = []; let processedContent = content.replace( /\\\$\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/g, (_match, expression) => { escapedInterpolations.push(`\${${expression}}`); return `${escapedPlaceholder}${escapedInterpolations.length - 1}`; } ); const interpolationRegex = /\$\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/g; const interpolatedContent = processedContent.replace( interpolationRegex, (_match, expression) => { hasInterpolations = true; try { const result = this.evaluateExpression(expression.trim(), state); if (result === void 0 || result === null) { const errorMsg = `Variable "${expression}" is undefined`; errors.push(errorMsg); if (this.enableLogging) { console.warn("XSYS Content Interpolation Warning:", errorMsg); } return ""; } return this.formatValue(result); } catch (error) { const errorMsg = `Failed to interpolate "${expression}": ${error instanceof Error ? error.message : String(error)}`; errors.push(errorMsg); if (this.enableLogging) { console.warn("XSYS Content Interpolation Error:", errorMsg); } return ""; } } ); const finalContent = interpolatedContent.replace( new RegExp(`${escapedPlaceholder}(\\d+)`, "g"), (match, ...args) => { return escapedInterpolations[parseInt(args[0])] || match; } ); return { content: finalContent, hasInterpolations, errors }; } /** * Check if content contains interpolation expressions */ hasInterpolations(content) { if (!content || typeof content !== "string") return false; return /\$\{[^}]+\}/.test(content); } /** * Extract all interpolation expressions from content */ extractExpressions(content) { if (!content || typeof content !== "string") return []; const expressions = []; const interpolationRegex = /\$\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/g; let match; while ((match = interpolationRegex.exec(content)) !== null) { if (!match || !match[1]) { continue; } expressions.push(match[1].trim()); } return expressions; } /** * Validate all expressions in content */ validateExpressions(content) { const expressions = this.extractExpressions(content); const errors = []; for (const expression of expressions) { try { const evaluator = this.evaluators[this.expressionLanguage]; if (!evaluator) { errors.push( `Unsupported expression language: ${this.expressionLanguage}` ); continue; } if (evaluator.compile) { const result = evaluator.compile(expression, { cache: false }); if (!result.success && result.error) { errors.push(`Invalid expression "${expression}": ${result.error}`); } } } catch (error) { errors.push( `Expression validation failed for "${expression}": ${error instanceof Error ? error.message : String(error)}` ); } } return { valid: errors.length === 0, errors }; } /** * Evaluate a single expression */ evaluateExpression(expression, state) { const evaluator = this.evaluators[this.expressionLanguage]; if (!evaluator) { throw new Error( `Unsupported expression language: ${this.expressionLanguage}` ); } let evaluationError; const result = evaluator.evaluate(expression, state, { onError: (err) => { evaluationError = err; } }); if (evaluationError) { throw evaluationError; } return result; } /** * Format a value for display in content */ formatValue(value) { if (value === null || value === void 0) { return ""; } if (typeof value === "string") { return value; } if (typeof value === "number") { return Number.isInteger(value) ? value.toString() : value.toFixed(2); } if (typeof value === "boolean") { return value ? "true" : "false"; } if (Array.isArray(value)) { return value.map((v) => this.formatValue(v)).join(", "); } if (typeof value === "object") { try { return JSON.stringify(value); } catch { return "[Object]"; } } return String(value); } }; // src/utils/infer-node-types.ts function inferNodeTypes(nodes) { const incoming = /* @__PURE__ */ new Map(); const outgoing = /* @__PURE__ */ new Map(); for (const n of nodes) { outgoing.set(n.id, n.outlets?.length ?? 0); for (const p of n.outlets ?? []) { incoming.set(p.to, (incoming.get(p.to) ?? 0) + 1); } } const typeMap = {}; for (const n of nodes) { const out = outgoing.get(n.id) ?? 0; const inc = incoming.get(n.id) ?? 0; if (out === 0 && inc === 0) { typeMap[n.id] = "isolated"; } else if (!inc && out > 0) { typeMap[n.id] = "start"; } else if (out === 0 && inc > 0) { typeMap[n.id] = "end"; } else if (out > 1) { typeMap[n.id] = "decision"; } else { typeMap[n.id] = "action"; } } return typeMap; } // src/core/flow-engine.ts var FlowEngine = class extends import_eventemitter32.EventEmitter { constructor(flow, options = {}) { super(); this.currentNode = null; this.history = []; this.flow = flow; this.nodeTypes = inferNodeTypes(flow.nodes); this.options = { enableHistory: true, maxHistorySize: 100, autoSave: false, ...options }; const initialState = { ...JSON.parse(JSON.stringify(flow.globalState || {})), ...JSON.parse(JSON.stringify(options.initialState || {})) }; if (options.stateManager) { this.stateManager = options.stateManager; } else { this.stateManager = new StateManager( initialState, flow.stateRules || [], { expressionLanguage: flow.expressionLanguage ?? "cel", stateSchema: flow.stateSchema, validateOnChange: true } ); } this.contentInterpolator = new ContentInterpolator({ expressionLanguage: flow.expressionLanguage ?? "cel", enableLogging: options.enableLogging ?? false }); this.stateManager.on( "stateChange", (data) => this.emit("stateChange", data) ); this.stateManager.on( "error", (data) => { if (data.context?.type === "forceTransition" && data.context.rule?.target) { this.handleForcedTransition(data.context.rule.target); } else { this.emit("error", data); } } ); } async start() { const startNode = this.findNodeById(this.flow.startNodeId); if (!startNode) { throw new Error( `Start node with id "${this.flow.startNodeId}" not found` ); } return this.transitionToNode(startNode); } async next(choiceId) { if (!this.currentNode) { throw new Error("No current node. Call start() first."); } let targetNode = null; const currentNodeType = this.nodeTypes[this.currentNode.id]; if (choiceId) { const outletResult = this.findOutletById(choiceId); if (!outletResult || outletResult.fromNodeId !== this.currentNode.id) { throw new Error(`Invalid choice: ${choiceId}`); } targetNode = this.findNodeById(outletResult.outlet.to); } else { if (currentNodeType === "decision") { targetNode = this.evaluateAutomaticDecision(this.currentNode); } else { if (currentNodeType === "action") { const outlets = this.getOutlets(this.currentNode.id); if (outlets.length === 1 && outlets[0]) { targetNode = this.findNodeById(outlets[0].to); } } } } if (!targetNode && currentNodeType !== "decision") { throw new Error("No valid transition found"); } if (currentNodeType === "decision" && !choiceId) { return this.createExecutionResult(); } if (!targetNode) { throw new Error("No valid transition found"); } return this.transitionToNode(targetNode, choiceId); } getCurrentNode() { if (!this.currentNode) { return null; } const nodeType = this.nodeTypes[this.currentNode.id]; if (!nodeType) { throw new Error(`Node type not found for node: ${this.currentNode.id}`); } return { node: this.getInterpolatedNode(this.currentNode), type: nodeType }; } getHistory() { return [...this.history]; } getAvailableChoices() { if (!this.currentNode) { return []; } const currentNodeType = this.nodeTypes[this.currentNode.id]; if (currentNodeType === "end") { return []; } const allOutlets = this.getOutlets(this.currentNode.id); const showDisabled = this.options.showDisabledChoices || false; const currentState = this.stateManager.getState(); let outletsToShow; let choices; const isSingleChoice = allOutlets.length === 1; const singleChoiceDefaultLabel = "Continue"; if (showDisabled) { outletsToShow = allOutlets; choices = outletsToShow.map((outlet, index) => { const isEnabled = this.evaluateOutletCondition(outlet); const description = outlet.metadata && "description" in outlet.metadata && typeof outlet.metadata.description === "string" ? outlet.metadata.description : void 0; let label = this.contentInterpolator.interpolate( outlet.label ? outlet.label : isSingleChoice ? singleChoiceDefaultLabel : "Choice " + (index + 1), // Ensure outlets have labels currentState ).content; return { id: outlet.id, label, description, outletId: outlet.id, disabled: !isEnabled, disabledReason: !isEnabled && outlet.condition ? "Condition not met" : void 0 }; }); } else { outletsToShow = allOutlets.filter( (outlet) => this.evaluateOutletCondition(outlet) ); choices = outletsToShow.map((outlet, index) => { let label = this.contentInterpolator.interpolate( outlet.label ? outlet.label : isSingleChoice ? singleChoiceDefaultLabel : "Choice " + (index + 1), // Ensure outlets have labels currentState ).content; return { id: outlet.id, label, description: outlet.metadata && "description" in outlet.metadata && typeof outlet.metadata.description === "string" ? outlet.metadata.description : void 0, outletId: outlet.id }; }); } const enabledChoices = showDisabled ? choices.filter((choice) => !choice.disabled) : choices; if (enabledChoices.length === 1 && enabledChoices[0] && !enabledChoices[0].label?.trim()) { const continueChoice = enabledChoices[0]; if (!continueChoice.id || !continueChoice.outletId) { throw new Error("Invalid choice structure: missing required fields"); } const targetOutlet = outletsToShow.find( (p) => p.id === continueChoice.id ); const targetNode = targetOutlet ? this.findNodeById(targetOutlet.to) : null; return [ { ...continueChoice, id: continueChoice.id, outletId: continueChoice.outletId, label: singleChoiceDefaultLabel, description: `Continue to ${targetNode?.title || "next step"}` }, ...choices.filter((choice) => choice.disabled) // Add any disabled choices if showing them ]; } return choices; } isComplete() { if (!this.currentNode) { return false; } const currentNodeType = this.nodeTypes[this.currentNode.id]; return currentNodeType === "end"; } canGoBack() { return Boolean(this.options.enableHistory) && this.history.length > 1; } goBack() { if (!this.canGoBack()) { throw new Error("Cannot go back"); } this.history.pop(); const previousStep = this.history[this.history.length - 1]; if (!previousStep) { throw new Error("No previous step available"); } this.stateManager.reset(previousStep.state); this.currentNode = previousStep.node.node; return Promise.resolve(this.createExecutionResult()); } reset() { this.currentNode = null; this.history = []; this.stateManager.reset( JSON.parse(JSON.stringify(this.flow.globalState || {})) ); } getState() { return this.stateManager.getState(); } getStateManager() { return this.stateManager; } async transitionToNode(node, choiceId) { if (this.currentNode) { this.emit("nodeExit", { node: this.currentNode, choice: choiceId, state: this.stateManager.getState() }); } if (choiceId && this.currentNode) { const outletResult = this.findOutletById(choiceId); if (outletResult?.outlet.actions) { this.stateManager.executeActions(outletResult.outlet.actions); } } this.currentNode = node; if (node.actions) { this.stateManager.executeActions(node.actions); } if (this.options.enableHistory) { const nodeType = this.nodeTypes[node.id]; if (!nodeType) { throw new Error(`Node type not found for node: ${node.id}`); } const step = { node: { node, type: nodeType }, choice: choiceId, timestamp: /* @__PURE__ */ new Date(), state: this.stateManager.getState() }; this.history.push(step); const maxSize = this.options.maxHistorySize || 100; if (this.history.length > maxSize) { this.history.shift(); } } this.emit("nodeEnter", { node, state: this.stateManager.getState() }); const availableOutlets = this.getOutlets(node.id); if (this.shouldAutoAdvance(node, availableOutlets)) { const selectedOutlet = this.selectAutoAdvanceOutlet( node, availableOutlets ); if (!selectedOutlet) { this.emit("error", { error: new Error( `No valid outlet found for auto-advance from node "${node.id}"` ), context: { node, availableOutlets } }); return this.createExecutionResult(); } const nodeTo = this.flow.nodes.find((n) => n.id === selectedOutlet.to); if (!nodeTo) { this.emit("error", { error: new Error(`Node with id "${selectedOutlet.to}" not found`), context: { node, outlet: selectedOutlet } }); return this.createExecutionResult(); } this.emit("autoAdvance", { from: node, to: nodeTo, condition: selectedOutlet.condition }); return this.transitionToNode(nodeTo, selectedOutlet.id); } return this.createExecutionResult(); } evaluateAutomaticDecision(node) { const availableOutlets = this.getOutlets(node.id); const sortedOutlets = [...availableOutlets]; for (const outlet of sortedOutlets) { if (this.evaluateOutletCondition(outlet)) { return this.findNodeById(outlet.to); } } return null; } evaluateOutletCondition(outlet) { if (!outlet.condition) return true; try { return this.stateManager.evaluateCondition(outlet.condition); } catch (error) { console.warn( `Failed to evaluate outlet condition: ${outlet.condition}`, error ); return false; } } /** * Selects the appropriate outlet for auto-advance using if-else style logic. * Evaluates outlets in order, returning the first outlet whose condition is true. * If no conditional outlets match, returns the default outlet (one without condition). */ selectAutoAdvanceOutlet(_node, availableOutlets) { const conditionalOutlets = availableOutlets.filter( (outlet) => outlet.condition ); const defaultOutlets = availableOutlets.filter( (outlet) => !outlet.condition ); for (const outlet of conditionalOutlets) { if (this.evaluateOutletCondition(outlet)) { return outlet; } } if (defaultOutlets.length > 0 && defaultOutlets[0]) { return defaultOutlets[0]; } return null; } /** * Get a node with interpolated content and title */ getInterpolatedNode(node) { const currentState = this.stateManager.getState(); let interpolatedNode = { ...node }; let hasAnyInterpolations = false; const allErrors = []; if (node.title && this.contentInterpolator.hasInterpolations(node.title)) { const titleResult = this.contentInterpolator.interpolate( node.title, currentState ); interpolatedNode.title = titleResult.content; hasAnyInterpolations = true; allErrors.push(...titleResult.errors); } if (node.content && this.contentInterpolator.hasInterpolations(node.content)) { const contentResult = this.contentInterpolator.interpolate( node.content, currentState ); interpolatedNode.content = contentResult.content; hasAnyInterpolations = true; allErrors.push(...contentResult.errors); } if (allErrors.length > 0 && this.options.enableLogging) { console.warn(`Interpolation errors for node "${node.id}":`, allErrors); } if (!hasAnyInterpolations) { return node; } return interpolatedNode; } createExecutionResult() { if (!this.currentNode) { throw new Error("No current node. Call start() first."); } const nodeType = this.nodeTypes[this.currentNode.id]; if (!nodeType) { throw new Error(`Node type not found for node: ${this.currentNode.id}`); } return { node: { node: this.getInterpolatedNode(this.currentNode), type: nodeType }, choices: this.getAvailableChoices(), isComplete: this.isComplete(), canGoBack: this.canGoBack(), state: this.stateManager.getState() }; } handleForcedTransition(targetNodeId) { const targetNode = this.findNodeById(targetNodeId); if (!targetNode) { this.emit("error", { error: new Error( `Target node "${targetNodeId}" not found for forced transition` ), context: { type: "forcedTransition", targetNodeId } }); return; } this.transitionToNode(targetNode).catch((error) => { this.emit("error", { error, context: { type: "forcedTransition", targetNodeId } }); }); } shouldAutoAdvance(node, availableOutlets) { if (this.flow.autoAdvance === "never" || this.options.autoAdvance === "never") { return false; } if (node.isAutoAdvance) { return this.selectAutoAdvanceOutlet(node, availableOutlets) !== null; } else if (this.flow.autoAdvance === "always" || this.options.autoAdvance === "always") { return this.selectAutoAdvanceOutlet(node, availableOutlets) !== null; } else { return false; } } findNodeById(id) { return this.flow.nodes.find((node) => node.id === id) || null; } getOutlets(nodeId) { const node = this.findNodeById(nodeId); return node?.outlets || []; } findOutletById(outletId) { for (const node of this.flow.nodes) { if (node.outlets) { const outlet = node.outlets.find((p) => p.id === outletId); if (outlet) { return { outlet, fromNodeId: node.id }; } } } return null; } }; // src/utils/performance-cache.ts var LRUCache = class { constructor(options = {}) { this.cache = /* @__PURE__ */ new Map(); this.maxSize = options.maxSize ?? 100; this.ttl = options.ttl ?? 3e5; } get(key) { const entry = this.cache.get(key); if (!entry) return void 0; if (Date.now() - entry.timestamp > this.ttl) { this.cache.delete(key); return void 0; } this.cache.delete(key); this.cache.set(key, entry); return entry.value; } set(key, value) { this.cache.delete(key); if (this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value; if (firstKey !== void 0) { this.cache.delete(firstKey); } } this.cache.set(key, { value, timestamp: Date.now() }); } clear() { this.cache.clear(); } size() { return this.cache.size; } }; // src/utils/graph-utils.ts var FlowGraphUtils = class { constructor(flow) { this.flow = flow; this.nodeMap = /* @__PURE__ */ new Map(); this.pathMap = /* @__PURE__ */ new Map(); this.reachabilityCache = new LRUCache({ maxSize: 50, ttl: 3e5 }); this.depthCache = new LRUCache({ maxSize: 50, ttl: 3e5 }); for (const node of flow.nodes) { this.nodeMap.set(node.id, node); } for (const node of flow.nodes) { if (node.outlets) { for (const path of node.outlets) { this.pathMap.set(path.id, { path, fromNodeId: node.id }); } } } } /** * Find a node by ID */ findNode(nodeId) { return this.nodeMap.get(nodeId) || null; } /** * Find a path by ID with its source node */ findPath(pathId) { return this.pathMap.get(pathId) || null; } /** * Get all outgoing paths from a node */ getOutgoingPaths(nodeId) { const node = this.nodeMap.get(nodeId); return node?.outlets || []; } /** * Get all incoming paths to a node */ getIncomingPaths(nodeId) { const incoming = []; for (const node of this.flow.nodes) { if (node.outlets) { for (const path of node.outlets) { if (path.to === nodeId) { incoming.push({ path, fromNodeId: node.id }); } } } } return incoming; } /** * Find all nodes reachable from a given start node (cached) */ findReachableNodes(startNodeId) { const start = startNodeId || this.flow.startNodeId; const cacheKey = `reachable:${start}`; const cached = this.reachabilityCache.get(cacheKey); if (cached) { return new Set(cached); } const reachable = /* @__PURE__ */ new Set(); const queue = [start]; while (queue.length > 0) { const nodeId = queue.shift(); if (!nodeId || reachable.has(nodeId)) continue; reachable.add(nodeId); const outgoingPaths = this.getOutgoingPaths(nodeId); for (const path of outgoingPaths) { if (!reachable.has(path.to)) { queue.push(path.to); } } } this.reachabilityCache.set(cacheKey, reachable); return reachable; } /** * Find unreachable nodes */ findUnreachableNodes(startNodeId) { const reachable = this.findReachableNodes(startNodeId); const unreachable = /* @__PURE__ */ new Set(); for (const node of this.flow.nodes) { if (!reachable.has(node.id)) { unreachable.add(node.id); } } return unreachable; } /** * Check if a specific node is reachable from start */ isNodeReachable(targetNodeId, startNodeId) { const start = startNodeId || this.flow.startNodeId; const visited = /* @__PURE__ */ new Set(); const queue = [start]; while (queue.length > 0) { const nodeId = queue.shift(); if (!nodeId || visited.has(nodeId)) continue; visited.add(nodeId); if (nodeId === targetNodeId) return true; const outgoingPaths = this.getOutgoingPaths(nodeId); for (const path of outgoingPaths) { if (!visited.has(path.to)) { queue.push(path.to); } } } return false; } /** * Calculate the maximum depth from start node */ calculateMaxDepth(startNodeId) { const start = startNodeId || this.flow.startNodeId; const visited = /* @__PURE__ */ new Set(); const dfs = (nodeId, depth) => { if (visited.has(nodeId)) return depth; visited.add(nodeId); const node = this.findNode(nodeId); if (!node) return depth; let maxDepth = depth; const outgoingPaths = this.getOutgoingPaths(nodeId); for (const path of outgoingPaths) { maxDepth = Math.max(maxDepth, dfs(path.to, depth + 1)); } return maxDepth; }; return dfs(start, 1); } /** * Calculate node depths from start (cached) */ calculateNodeDepths(startNodeId) { const start = startNodeId || this.flow.startNodeId; const cacheKey = `depths:${start}`; const cached = this.depthCache.get(cacheKey); if (cached) { return new Map(cached); } const depths = /* @__PURE__ */ new Map(); const queue = [{ nodeId: start, depth: 0 }]; while (queue.length > 0) { const current = queue.shift(); if (!current) break; const { nodeId, depth } = current; if (depths.has(nodeId)) continue; depths.set(nodeId, depth); const outgoingPaths = this.getOutgoingPaths(nodeId); for (const path of outgoingPaths) { if (!depths.has(path.to)) { queue.push({ nodeId: path.to, depth: depth + 1 }); } } } this.depthCache.set(cacheKey, depths); return depths; } /** * Detect cycles in the flow graph */ hasCycles(startNodeId) { const start = startNodeId || this.flow.startNodeId; const visiting = /* @__PURE__ */ new Set(); const visited = /* @__PURE__ */ new Set(); const dfs = (nodeId) => { if (visiting.has(nodeId)) return true; if (visited.has(nodeId)) return false; visiting.add(nodeId); const outgoingPaths = this.getOutgoingPaths(nodeId); for (const path of outgoingPaths) { if (dfs(path.to)) return true; } visiting.delete(nodeId); visited.add(nodeId); return false; }; return dfs(start); } /** * Comprehensive graph traversal with detailed results */ performTraversal(startNodeId) { const start = startNodeId || this.flow.startNodeId; const reachableNodes = this.findReachableNodes(start); const nodeDepths = this.calculateNodeDepths(start); const visitedPaths = /* @__PURE__ */ new Set(); const pathsFromNode = /* @__PURE__ */ new Map(); for (const nodeId of reachableNodes) { const outgoingPaths = this.getOutgoingPaths(nodeId); pathsFromNode.set(nodeId, outgoingPaths); for (const path of outgoingPaths) { if (reachableNodes.has(path.to)) { visitedPaths.add(path.id); } } } return { reachableNodes, visitedPaths, nodeDepths, pathsFromNode }; } /** * Explore all possible paths through the flow with optional state tracking */ exploreAllPaths(options = {}) { const { maxDepth = 100, maxPaths = 1e3, includeState = false, conditionEvaluator } = options; const allPaths = []; const queue = [ { nodeId: this.flow.startNodeId, path: [this.flow.startNodeId], pathIds: [], depth: 1, state: includeState ? { ...this.flow.globalState } : void 0 } ]; const reachableNodes = /* @__PURE__ */ new Set(); let currentMaxDepth = 0; let hasCycles = false; while (queue.length > 0 && allPaths.length < maxPaths) { const current = queue.shift(); if (!current) break; reachableNodes.add(current.nodeId); currentMaxDepth = Math.max(currentMaxDepth, current.depth); const nodeCount = current.path.filter( (id) => id === current.nodeId ).length; if (nodeCount > 1) { hasCycles = true; } const node = this.findNode(current.nodeId); if (!node) continue; const outgoingPaths = this.getOutgoingPaths(current.nodeId); if (outgoingPaths.length === 0 || current.depth >= maxDepth) { allPaths.push({ nodeIds: current.path, pathIds: current.pathIds, depth: current.depth, state: current.state }); continue; } for (const path of outgoingPaths) { if (conditionEvaluator && path.condition && current.state) { try { if (!conditionEvaluator(path.condition, current.state)) { continue; } } catch { continue; } } const newPath = [...current.path, path.to]; const newPathIds = [...current.pathIds, path.id]; queue.push({ nodeId: path.to, path: newPath, pathIds: newPathIds, depth: current.depth + 1, state: current.state ? { ...current.state } : void 0 }); } } const allNodeIds = new Set(this.flow.nodes.map((n) => n.id)); const unreachableNodes = /* @__PURE__ */ new Set(); for (const nodeId of allNodeIds) { if (!reachableNodes.has(nodeId)) { unreachableNodes.add(nodeId); } } return { allPaths, reachableNodes, unreachableNodes, maxDepth: currentMaxDepth, hasCycles }; } /** * Get flow statistics */ getFlowStats() { const traversal = this.performTraversal(); const pathExploration = this.exploreAllPaths({ maxPaths: 100 }); return { totalNodes: this.flow.nodes.length, reachableNodes: traversal.reachableNodes.size, unreachableNodes: this.flow.nodes.length - traversal.reachableNodes.size, totalPaths: traversal.visitedPaths.size, maxDepth: this.calculateMaxDepth(), hasCycles: this.hasCycles(), possiblePaths: pathExploration.allPaths.length }; } }; // src/analysis/flow-validator.ts var FlowValidator = class { constructor(evaluators) { this.evaluators = evaluators ?? EvaluatorFactory.getDefaultEvaluators(); } validate(flow) { const errors = []; const warnings = []; const nodeTypes = inferNodeTypes(flow.nodes); const graphUtils = new Flo