/** * Node Swap utilities for FlowDrop * * Provides logic for swapping a workflow node with a different node type * while intelligently remapping compatible port connections. * * @module utils/nodeSwap */ import type { WorkflowNode, WorkflowEdge, NodeMetadata, NodePort, ConfigSchema, ConfigValues } from '../types/index.js'; import type { WorkflowValidationResult } from './validation.js'; import type { PortCompatibilityChecker } from './connections.js'; /** * Describes how a single port was remapped during a swap. */ export interface PortMapping { oldHandleId: string; newHandleId: string; oldPortId: string; newPortId: string; direction: 'input' | 'output'; } /** * An edge that could not be remapped and will be dropped. */ export interface DroppedEdge { edge: WorkflowEdge; reason: string; } /** * Preview of what a node swap will do before it is executed. */ export interface SwapPreview { /** Edges that will be preserved with their rewritten versions */ keptEdges: Array<{ edge: WorkflowEdge; newEdge: WorkflowEdge; }>; /** Edges that will be removed */ droppedEdges: DroppedEdge[]; /** True if any connected edges will be lost */ hasDataLoss: boolean; /** The new node ID that will be generated */ newNodeId: string; /** Config keys carried over from the old node */ configCarriedOver: string[]; /** Config keys reset to defaults on the new node */ configReset: string[]; } /** * Result of executing a node swap. */ export interface SwapResult { updatedNodes: WorkflowNode[]; updatedEdges: WorkflowEdge[]; } /** Quality annotation for how a port was matched. */ export type MatchQuality = 'id' | 'name' | 'type' | 'manual' | 'unmapped'; /** Manual override for a single port mapping. */ export interface PortMappingOverride { oldPortId: string; newPortId: string | null; direction: 'input' | 'output'; } /** Manual override for a single config mapping. */ export interface ConfigMappingOverride { key: string; action: 'carry' | 'reset' | 'set'; value?: unknown; } /** Options bag for advanced swap functions. */ export interface SwapOptions { checker?: PortCompatibilityChecker | null; portOverrides?: PortMappingOverride[]; configOverrides?: ConfigMappingOverride[]; strategies?: SwapStrategy[]; } /** Pluggable strategy passed per-call, not registered globally. */ export interface SwapStrategy { readonly id: string; readonly name: string; canHandle(ctx: SwapStrategyContext): boolean; mapPorts?(ctx: SwapStrategyContext): Record | undefined; mapConfig?(ctx: SwapStrategyContext): Record | undefined; } /** Context passed to swap strategies. */ export interface SwapStrategyContext { oldNode: WorkflowNode; newMetadata: NodeMetadata; edges: WorkflowEdge[]; allNodes: WorkflowNode[]; checker: PortCompatibilityChecker | null; } /** Stable, data-only event context for swap hooks. */ export interface SwapEventContext { oldNode: WorkflowNode; newMetadata: NodeMetadata; preview: SwapPreview; portOverrides: PortMappingOverride[]; configOverrides: ConfigMappingOverride[]; } /** Error class for swap validation failures. */ export declare class SwapValidationError extends Error { constructor(message: string); } /** Editable port mapping for the interactive mapping editor. */ export interface EditablePortMapping { oldPort: NodePort; edge: WorkflowEdge; direction: 'input' | 'output'; selectedNewPortId: string | null; matchQuality: MatchQuality; autoSuggestedPortId: string | null; isOverridden: boolean; } /** Editable config mapping for the interactive mapping editor. */ export interface EditableConfigMapping { key: string; title: string; oldValue: unknown; newDefault: unknown; carryOver: boolean; autoCarryOver: boolean; /** false for nested objects/arrays — shown as read-only, always reset */ isFlat: boolean; } /** Full interactive swap state for the mapping editor. */ export interface InteractiveSwapState { oldNode: WorkflowNode; newMetadata: NodeMetadata; newNodeId: string; portMappings: EditablePortMapping[]; configMappings: EditableConfigMapping[]; availableNewInputs: NodePort[]; availableNewOutputs: NodePort[]; } /** * Compare two semver-like version strings. * Returns positive if a > b, negative if a < b, 0 if equal. * * Handles pre-release tags: "2.0.0-beta" < "2.0.0" */ export declare function compareSemver(a: string, b: string): number; /** * Map config values from an old node to a new node's schema. * * - Keys present in both old config and new schema: carry over the old value * - Keys only in the new schema: use the schema default or newDefaults * - Keys only in the old config: discarded * - Dynamic port keys (dynamicInputs, dynamicOutputs, branches): never carried over */ export declare function mapConfig(oldConfig: ConfigValues, newConfigSchema: ConfigSchema | undefined, newDefaults?: Record): { config: ConfigValues; carriedOver: string[]; reset: string[]; }; /** * Compute a preview of what will happen when swapping oldNode with newMetadata. * * This does NOT mutate anything — it returns a preview that can be displayed * to the user for confirmation before executing the swap. */ export declare function computeSwapPreview(oldNode: WorkflowNode, newMetadata: NodeMetadata, edges: WorkflowEdge[], allNodes: WorkflowNode[], checker?: PortCompatibilityChecker | null): SwapPreview; /** * Execute a node swap using a previously computed preview. * * Returns new nodes and edges arrays ready for `workflowActions.batchUpdate()`. */ export declare function executeSwap(oldNode: WorkflowNode, newMetadata: NodeMetadata, preview: SwapPreview, allNodes: WorkflowNode[], allEdges: WorkflowEdge[]): SwapResult; /** * Check if a newer version of the same node type is available. * * Compares the node's embedded metadata.version against the same-ID entry * in the available nodes list (API returns only the latest version). * * @returns The newer NodeMetadata if an upgrade is available, null otherwise */ export declare function getVersionUpgrade(currentMetadata: NodeMetadata, allNodeTypes: NodeMetadata[]): NodeMetadata | null; /** * Compute a swap preview with full options support (strategies, overrides). * * Resolution order: * 1. Check strategies — first canHandle() match wins for mapPorts()/mapConfig() * 2. Fall through to built-in 3-pass for ports not covered by strategy * 3. Apply portOverrides on top (highest priority — user's manual overrides) * 4. Same cascade for config */ export declare function computeSwapPreviewWithOptions(oldNode: WorkflowNode, newMetadata: NodeMetadata, edges: WorkflowEdge[], allNodes: WorkflowNode[], options: SwapOptions): SwapPreview; /** * Compute interactive state for the mapping editor UI. * * Returns EditablePortMapping and EditableConfigMapping entries * with match quality annotations and isFlat flags. */ export declare function computeInteractiveState(oldNode: WorkflowNode, newMetadata: NodeMetadata, edges: WorkflowEdge[], allNodes: WorkflowNode[], options?: SwapOptions): InteractiveSwapState; /** * Convert user-edited InteractiveSwapState back into a SwapPreview * for executeSwap(). Pure function, no side effects. */ export declare function buildSwapPreviewFromState(state: InteractiveSwapState, allEdges: WorkflowEdge[]): SwapPreview; /** * Headless one-shot swap with full validation. * * Guardrails: * - Validates oldNode.id exists in allNodes * - Validates format compatibility if newMetadata.formats is set * - Computes preview → executes → validates → returns result * - Throws SwapValidationError on invalid input */ export declare function performSwap(oldNode: WorkflowNode, newMetadata: NodeMetadata, allNodes: WorkflowNode[], allEdges: WorkflowEdge[], options?: SwapOptions): SwapResult; /** * Validate a swap result for structural integrity. * * Checks: * - No dangling edge references (every edge source/target exists in nodes) * - No duplicate node IDs * - No duplicate edge IDs */ export declare function validateSwapResult(result: SwapResult): WorkflowValidationResult;