package platform import ( "encoding/json" "errors" "sort" "strings" ) type canvasAIStaticInput struct { EdgeID string `json:"logical_edge_id"` BoardID string `json:"board_id"` NodeID string `json:"node_id"` Kind string `json:"kind"` Content map[string]any `json:"content"` } type canvasAIStepPlan struct { CanonicalRoot string `json:"canonical_root,omitempty"` TargetIncarnation int64 `json:"target_incarnation,omitempty"` StaticInputs []canvasAIStaticInput `json:"static_inputs"` MaxExecutionMS int `json:"max_execution_ms"` ID string `json:"step_id"` BoardID string `json:"board_id"` NodeID string `json:"node_id"` Kind string `json:"kind"` ModelID string `json:"model_id"` Config map[string]any `json:"config"` FrozenContent map[string]any `json:"frozen_content"` Fingerprint string `json:"target_fingerprint"` Status string `json:"status"` Dependencies []string `json:"dependencies"` } type canvasAIEdgePlan struct { ID string `json:"logical_edge_id"` Source string `json:"source_step_id"` Target string `json:"target_step_id"` } type canvasAIPlan struct { Steps []canvasAIStepPlan `json:"steps"` Edges []canvasAIEdgePlan `json:"edges"` Versions map[string]int64 `json:"frozen_versions"` } func canvasNodeKey(board, node string) string { return board + "/" + node } func buildCanvasAIPlan(snapshots map[string]canvasSnapshot, board, node string, propagate bool) (canvasAIPlan, error) { plan := canvasAIPlan{Steps: []canvasAIStepPlan{}, Edges: []canvasAIEdgePlan{}, Versions: map[string]int64{}} data := map[string]map[string]any{} for board, snapshot := range snapshots { plan.Versions[board] = snapshot.Version for node, d := range canvasNodeData(snapshot.Document) { data[canvasNodeKey(board, node)] = d } } var resolve func(string, string, string, string, int) (string, error) resolve = func(board, node, handle, direction string, depth int) (string, error) { if depth > 8 { return "", errors.New("graph_limit_exceeded") } d := data[canvasNodeKey(board, node)] if d == nil { return "", errGraphOwnership } if d["kind"] != "frame" { if !canvasChoice(d["kind"], "text", "script", "plain-text", "image", "plain-image") { return "", errGraphOwnership } return canvasNodeKey(board, node), nil } if !strings.HasPrefix(handle, "port:") { return "", errGraphOwnership } sub, _ := d["subgraph"].(map[string]any) child, _ := sub["boardId"].(string) ports, _ := d["ports"].([]any) for _, v := range ports { p := v.(map[string]any) if p["id"] == strings.TrimPrefix(handle, "port:") && p["direction"] == direction { target, _ := p["nodeId"].(string) h, _ := p["handle"].(string) return resolve(child, target, h, direction, depth+1) } } return "", errGraphOwnership } outgoing := map[string][]canvasAIEdgePlan{} allIncoming := map[string][]canvasAIEdgePlan{} for board, snapshot := range snapshots { var doc map[string]any _ = json.Unmarshal(snapshot.Document, &doc) for _, v := range doc["edges"].([]any) { e := v.(map[string]any) d, _ := e["data"].(map[string]any) execution, _ := d["execution"].(map[string]any) if execution["enabled"] == false { continue } source, _ := e["source"].(string) sh, _ := e["sourceHandle"].(string) target, _ := e["target"].(string) th, _ := e["targetHandle"].(string) a, err := resolve(board, source, sh, "output", 0) if err != nil { return plan, err } b, err := resolve(board, target, th, "input", 0) if err != nil { return plan, err } edge := canvasAIEdgePlan{ID: canvasNodeKey(board, e["id"].(string)), Source: a, Target: b} outgoing[a] = append(outgoing[a], edge) allIncoming[b] = append(allIncoming[b], edge) } } root := canvasNodeKey(board, node) if data[root] == nil || !canvasChoice(data[root]["kind"], "text", "script", "plain-text", "image", "plain-image") { return plan, errGraphOwnership } for key := range outgoing { sort.Slice(outgoing[key], func(i, j int) bool { return outgoing[key][i].ID < outgoing[key][j].ID }) } selected := map[string]bool{} skipped := map[string]bool{} queue := []string{root} for len(queue) > 0 { id := queue[0] queue = queue[1:] if selected[id] { continue } selected[id] = true if len(selected) > 64 { return plan, errGraphCapacity } if id != root { ai, _ := data[id]["ai"].(map[string]any) model, _ := ai["modelId"].(string) if ai["enabled"] != true || model == "" { skipped[id] = true continue } } if !propagate { continue } for _, edge := range outgoing[id] { plan.Edges = append(plan.Edges, edge) queue = append(queue, edge.Target) } } // Kahn's algorithm waits for every participating predecessor at a diamond. indegree := map[string]int{} incoming := map[string][]string{} depths := map[string]int{} for _, edge := range plan.Edges { indegree[edge.Target]++ incoming[edge.Target] = append(incoming[edge.Target], edge.Source) } ready := []string{} for id := range selected { if indegree[id] == 0 { ready = append(ready, id) } } for len(ready) > 0 { sort.Strings(ready) id := ready[0] ready = ready[1:] parts := strings.SplitN(id, "/", 2) d := data[id] ai, _ := d["ai"].(map[string]any) model, _ := ai["modelId"].(string) status := "queued" if skipped[id] { status = "skipped" } content := canvasFrozenContent(d) static := []canvasAIStaticInput{} sort.Slice(allIncoming[id], func(i, j int) bool { return allIncoming[id][i].ID < allIncoming[id][j].ID }) for _, edge := range allIncoming[id] { if propagate && selected[edge.Source] && !skipped[edge.Source] { continue } origin := strings.SplitN(edge.Source, "/", 2) source := data[edge.Source] static = append(static, canvasAIStaticInput{EdgeID: edge.ID, BoardID: origin[0], NodeID: origin[1], Kind: source["kind"].(string), Content: canvasFrozenContent(source)}) } dependencies := incoming[id] if dependencies == nil { dependencies = []string{} } sort.Strings(dependencies) plan.Steps = append(plan.Steps, canvasAIStepPlan{StaticInputs: static, ID: id, BoardID: parts[0], NodeID: parts[1], Kind: d["kind"].(string), ModelID: model, Config: ai, FrozenContent: content, Fingerprint: canvasTargetFingerprint(parts[0], parts[1], d), Status: status, Dependencies: dependencies}) plan.Versions[parts[0]] = snapshots[parts[0]].Version for _, edge := range outgoing[id] { if !propagate || skipped[id] { continue } depth := depths[id] + 1 if depth > depths[edge.Target] { depths[edge.Target] = depth } if depth > 32 { return plan, errGraphCapacity } indegree[edge.Target]-- if indegree[edge.Target] == 0 { ready = append(ready, edge.Target) } } } if len(plan.Steps) != len(selected) { return plan, errors.New("execution_cycle") } sort.Slice(plan.Edges, func(i, j int) bool { return plan.Edges[i].ID < plan.Edges[j].ID }) return plan, nil } func canvasFrozenContent(data map[string]any) map[string]any { if canvasChoice(data["kind"], "image", "plain-image") { return map[string]any{"src": data["src"], "assetId": data["assetId"]} } return map[string]any{"text": data["text"]} }