import { createLogger } from '@sim/logger' import { isMetadataOnlyBlockType, isTriggerBlockType } from '@/executor/constants' import { extractBaseBlockId } from '@/executor/utils/subflow-utils' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' const logger = createLogger('PathConstructor') export class PathConstructor { execute( workflow: SerializedWorkflow, triggerBlockId?: string, includeAllBlocks?: boolean ): Set { // For run-from-block mode, include all enabled blocks regardless of trigger reachability if (includeAllBlocks) { return this.getAllEnabledBlocks(workflow) } const resolvedTriggerId = this.findTriggerBlock(workflow, triggerBlockId) if (!resolvedTriggerId) { logger.warn('No trigger block found, including all enabled blocks as fallback') return this.getAllEnabledBlocks(workflow) } const adjacency = this.buildAdjacencyMap(workflow) const reachable = this.performBFS(resolvedTriggerId, adjacency) return reachable } private findTriggerBlock( workflow: SerializedWorkflow, triggerBlockId?: string ): string | undefined { if (triggerBlockId) { const block = workflow.blocks.find((b) => b.id === triggerBlockId) if (block) { if (!block.enabled) { logger.error('Provided triggerBlockId is disabled, finding alternative', { triggerBlockId, blockEnabled: block.enabled, }) // Try to find an alternative enabled trigger instead of failing const alternativeTrigger = this.findExplicitTrigger(workflow) if (alternativeTrigger) { logger.info('Using alternative enabled trigger', { disabledTriggerId: triggerBlockId, alternativeTriggerId: alternativeTrigger, }) return alternativeTrigger } throw new Error( `Trigger block ${triggerBlockId} is disabled and no alternative enabled trigger found` ) } return triggerBlockId } const fallbackTriggerId = this.resolveResumeTriggerFallback(triggerBlockId, workflow) if (fallbackTriggerId) { return fallbackTriggerId } logger.error('Provided triggerBlockId not found in workflow', { triggerBlockId, availableBlocks: workflow.blocks.map((b) => ({ id: b.id, type: b.metadata?.id })), }) throw new Error(`Trigger block not found: ${triggerBlockId}`) } const explicitTrigger = this.findExplicitTrigger(workflow) if (explicitTrigger) { return explicitTrigger } const rootBlock = this.findRootBlock(workflow) if (rootBlock) { return rootBlock } return undefined } private findExplicitTrigger(workflow: SerializedWorkflow): string | undefined { for (const block of workflow.blocks) { if (block.enabled && this.isTriggerBlock(block)) { return block.id } } return undefined } private findRootBlock(workflow: SerializedWorkflow): string | undefined { const hasIncoming = new Set(workflow.connections.map((c) => c.target)) for (const block of workflow.blocks) { if ( !hasIncoming.has(block.id) && block.enabled && !isMetadataOnlyBlockType(block.metadata?.id) ) { return block.id } } return undefined } private isTriggerBlock(block: SerializedBlock): boolean { return isTriggerBlockType(block.metadata?.id) } private getAllEnabledBlocks(workflow: SerializedWorkflow): Set { return new Set(workflow.blocks.filter((b) => b.enabled).map((b) => b.id)) } private buildAdjacencyMap(workflow: SerializedWorkflow): Map { const adjacency = new Map() const enabledBlocks = new Set(workflow.blocks.filter((b) => b.enabled).map((b) => b.id)) for (const connection of workflow.connections) { if (!enabledBlocks.has(connection.source) || !enabledBlocks.has(connection.target)) { continue } const neighbors = adjacency.get(connection.source) ?? [] neighbors.push(connection.target) adjacency.set(connection.source, neighbors) } return adjacency } private performBFS(triggerBlockId: string, adjacency: Map): Set { const reachable = new Set([triggerBlockId]) const queue = [triggerBlockId] while (queue.length > 0) { const currentBlockId = queue.shift() if (!currentBlockId) break const neighbors = adjacency.get(currentBlockId) ?? [] for (const neighborId of neighbors) { if (!reachable.has(neighborId)) { reachable.add(neighborId) queue.push(neighborId) } } } return reachable } private resolveResumeTriggerFallback( triggerBlockId: string, workflow: SerializedWorkflow ): string | undefined { if (!triggerBlockId.endsWith('__trigger')) { return undefined } const baseId = triggerBlockId.replace(/__trigger$/, '') const normalizedBaseId = extractBaseBlockId(baseId) const candidates = baseId === normalizedBaseId ? [baseId] : [baseId, normalizedBaseId] for (const candidate of candidates) { const block = workflow.blocks.find((b) => b.id === candidate) if (block) { return candidate } } return undefined } }