package routing import ( "fmt" "github.com/rockliang/poimen/workflows/pkg/db" ) // CanvasConverter converts visual canvas to executable WorkflowSpec type CanvasConverter struct { validator *CanvasValidator } // NewCanvasConverter creates a converter func NewCanvasConverter() *CanvasConverter { return &CanvasConverter{ validator: NewCanvasValidator(), } } // CanvasToWorkflowSpec converts canvas to WorkflowSpec func (cc *CanvasConverter) CanvasToWorkflowSpec(canvas *db.Canvas) (*WorkflowSpec, error) { // Validate first if err := cc.validator.ValidateCanvas(canvas); err != nil { return nil, fmt.Errorf("canvas validation failed: %w", err) } // Get topological order sortedNodes, err := cc.validator.TopoSort(canvas.Nodes, canvas.Edges) if err != nil { return nil, fmt.Errorf("topological sort failed: %w", err) } // Build states from sorted nodes states := []State{} nodeToState := make(map[string]int) // node ID to state index for i, node := range sortedNodes { state := cc.nodeToState(node, canvas.Edges) states = append(states, state) nodeToState[node.ID] = i } // Wire up transitions for i, node := range sortedNodes { outgoing := cc.getOutgoingEdges(node.ID, canvas.Edges) if len(outgoing) == 0 { // Last state - no transitions continue } if len(outgoing) == 1 { // Single outgoing edge targetNode := outgoing[0] targetIdx := nodeToState[targetNode] if targetIdx > i { states[i].Next = states[targetIdx].Name } } else { // Multiple outgoing edges - parallel states[i].Type = "Parallel" branches := []interface{}{} for _, targetNode := range outgoing { branches = append(branches, map[string]string{ "state": states[nodeToState[targetNode]].Name, }) } if states[i].Branches == nil { states[i].Branches = branches } } } spec := &WorkflowSpec{ Name: canvas.Name, Input: map[string]interface{}{}, States: states, } return spec, nil } // nodeToState converts a canvas node to a workflow state func (cc *CanvasConverter) nodeToState(node db.WorkflowNode, edges []db.WorkflowEdge) State { // Map node type to activity name activityName := cc.mapActivityType(node.Type) state := State{ Name: node.ID, Type: TaskActivity, Activity: activityName, Retry: &RetryPolicy{MaxAttempts: 3, BackoffSeconds: 2}, Timeout: "300s", Parameters: node.Data, } return state } // mapActivityType maps canvas activity type to Poimen activity func (cc *CanvasConverter) mapActivityType(canvasType string) string { typeMap := map[string]string{ "clone-repo": "CloneRepoActivity", "analyze-code": "AnalyzeCodeActivity", "security-scan": "SecurityScanActivity", "generate-report": "GenerateReportActivity", "deployment-precheck": "DeploymentPreCheckActivity", "notify-status": "NotifyStatusActivity", "approve-workflow": "ApproveWorkflowActivity", "archive-results": "ArchiveResultsActivity", "retrieve-memory": "RetrieveMemoryActivity", "assume-role": "AssumeRoleActivity", "llm-inference": "LLMInferenceActivity", "llm-batch-inference": "LLMBatchInferenceActivity", } if mapped, ok := typeMap[canvasType]; ok { return mapped } return canvasType // fallback to type as-is } // getOutgoingEdges returns target node IDs for a given source node func (cc *CanvasConverter) getOutgoingEdges(nodeID string, edges []db.WorkflowEdge) []string { targets := []string{} seen := make(map[string]bool) for _, edge := range edges { if edge.Source == nodeID && !seen[edge.Target] { targets = append(targets, edge.Target) seen[edge.Target] = true } } return targets } // CanvasToExecutionPlan converts canvas to sequential activity list func (cc *CanvasConverter) CanvasToExecutionPlan(canvas *db.Canvas) ([]ExecutionStep, error) { // Validate first if err := cc.validator.ValidateCanvas(canvas); err != nil { return nil, fmt.Errorf("canvas validation failed: %w", err) } // Get topological order sortedNodes, err := cc.validator.TopoSort(canvas.Nodes, canvas.Edges) if err != nil { return nil, fmt.Errorf("topological sort failed: %w", err) } steps := []ExecutionStep{} for i, node := range sortedNodes { step := ExecutionStep{ Index: i, NodeID: node.ID, ActivityName: cc.mapActivityType(node.Type), Label: node.Label, Parameters: node.Data, Timeout: "300s", } steps = append(steps, step) } return steps, nil } // ExecutionStep represents one activity in execution plan type ExecutionStep struct { Index int `json:"index"` NodeID string `json:"node_id"` ActivityName string `json:"activity_name"` Label string `json:"label"` Parameters map[string]interface{} `json:"parameters"` Timeout string `json:"timeout"` DependsOn []int `json:"depends_on,omitempty"` // Indices of predecessor steps }