package platform import ( "database/sql" "encoding/json" "net/http" "sort" "strings" ) type canvasAIReader struct { Load func(string) (canvasSnapshot, error) Parent func(string) (subgraphRelation, error) snapshots map[string]canvasSnapshot documents map[string]map[string]any parents map[string]subgraphRelation parentKnown map[string]bool used map[string]map[string]bool } func (reader *canvasAIReader) snapshot(board string) (canvasSnapshot, map[string]any, error) { if snapshot, ok := reader.snapshots[board]; ok { return snapshot, reader.documents[board], nil } snapshot, err := reader.Load(board) if err != nil { return canvasSnapshot{}, nil, err } var doc map[string]any if err = json.Unmarshal(snapshot.Document, &doc); err != nil { return canvasSnapshot{}, nil, err } reader.snapshots[board] = snapshot reader.documents[board] = doc return snapshot, doc, nil } func (reader *canvasAIReader) parent(board string) (subgraphRelation, error) { if reader.parentKnown[board] { return reader.parents[board], nil } relation, err := reader.Parent(board) if err == sql.ErrNoRows { err = nil relation = subgraphRelation{} } if err != nil { return relation, err } reader.parentKnown[board] = true reader.parents[board] = relation return relation, nil } func (reader *canvasAIReader) node(board, node string) (map[string]any, error) { _, doc, err := reader.snapshot(board) if err != nil { return nil, err } for _, value := range doc["nodes"].([]any) { n := value.(map[string]any) if n["id"] == node { return n["data"].(map[string]any), nil } } return nil, errGraphOwnership } func (reader *canvasAIReader) resolve(board, node, handle, direction string, depth int) (string, error) { if depth > 8 { return "", errGraphCapacity } data, err := reader.node(board, node) if err != nil { return "", err } if data["kind"] != "frame" { if !canvasChoice(data["kind"], "text", "script", "plain-text", "image", "plain-image") { return "", errGraphOwnership } return canvasNodeKey(board, node), nil } sub, _ := data["subgraph"].(map[string]any) child, _ := sub["boardId"].(string) relation, err := reader.parent(child) if err != nil { return "", err } if relation.Parent != board || relation.Frame != node || relation.Archived { return "", errGraphOwnership } ports, _ := data["ports"].([]any) for _, value := range ports { p := value.(map[string]any) if p["direction"] == direction && "port:"+p["id"].(string) == handle { return reader.resolve(child, p["nodeId"].(string), p["handle"].(string), direction, depth+1) } } return "", errGraphOwnership } // Follow only real edges and the port chains exposing this node. Empty or // unrelated child boards are never traversed merely because their frame exists. func (reader *canvasAIReader) edges(board, node string, outgoing bool) ([]canvasAIEdgePlan, error) { type exposure struct { Board, Node, Handle string Depth int } queue := []exposure{{Board: board, Node: node}} seen := map[string]bool{} found := map[string]canvasAIEdgePlan{} end, direction := "target", "input" if outgoing { end, direction = "source", "output" } key := canvasNodeKey(board, node) for len(queue) > 0 { current := queue[0] queue = queue[1:] identity := current.Board + "/" + current.Node + "/" + current.Handle if seen[identity] { continue } seen[identity] = true if current.Depth > 8 { return nil, errGraphCapacity } _, doc, err := reader.snapshot(current.Board) if err != nil { return nil, err } for _, value := range doc["edges"].([]any) { edge := value.(map[string]any) if edge[end] != current.Node { continue } handle, _ := edge[end+"Handle"].(string) if current.Handle != "" && handle != current.Handle { continue } data, _ := edge["data"].(map[string]any) execution, _ := data["execution"].(map[string]any) if execution["enabled"] == false { continue } sh, _ := edge["sourceHandle"].(string) th, _ := edge["targetHandle"].(string) source, err := reader.resolve(current.Board, edge["source"].(string), sh, "output", 0) if err != nil { return nil, err } target, err := reader.resolve(current.Board, edge["target"].(string), th, "input", 0) if err != nil { return nil, err } if (outgoing && source != key) || (!outgoing && target != key) { continue } id := edge["id"].(string) if reader.used[current.Board] == nil { reader.used[current.Board] = map[string]bool{} } reader.used[current.Board][id] = true logical := canvasAIEdgePlan{ID: canvasNodeKey(current.Board, id), Source: source, Target: target} found[logical.ID] = logical } relation, err := reader.parent(current.Board) if err != nil { return nil, err } if relation.Parent == "" { continue } if relation.Archived { return nil, errGraphOwnership } frame, err := reader.node(relation.Parent, relation.Frame) if err != nil { return nil, err } sub, _ := frame["subgraph"].(map[string]any) if sub["boardId"] != current.Board { return nil, errGraphOwnership } ports, _ := frame["ports"].([]any) for _, value := range ports { p := value.(map[string]any) if p["nodeId"] == current.Node && p["direction"] == direction && (current.Handle == "" || p["handle"] == current.Handle) { queue = append(queue, exposure{Board: relation.Parent, Node: relation.Frame, Handle: "port:" + p["id"].(string), Depth: current.Depth + 1}) } } } result := []canvasAIEdgePlan{} for _, edge := range found { result = append(result, edge) } sort.Slice(result, func(i, j int) bool { return result[i].ID < result[j].ID }) return result, nil } func (reader *canvasAIReader) plan(board, node string, propagate bool) (canvasAIPlan, error) { reader.snapshots = map[string]canvasSnapshot{} reader.documents = map[string]map[string]any{} reader.parents = map[string]subgraphRelation{} reader.parentKnown = map[string]bool{} reader.used = map[string]map[string]bool{} queue := []string{canvasNodeKey(board, node)} selected := map[string]bool{} for len(queue) > 0 { key := queue[0] queue = queue[1:] if selected[key] { continue } selected[key] = true if len(selected) > 64 { return canvasAIPlan{}, errGraphCapacity } parts := strings.SplitN(key, "/", 2) data, err := reader.node(parts[0], parts[1]) if err != nil { return canvasAIPlan{}, err } ai, _ := data["ai"].(map[string]any) model, _ := ai["modelId"].(string) if key != canvasNodeKey(board, node) && (ai["enabled"] != true || model == "") { continue } if _, err = reader.edges(parts[0], parts[1], false); err != nil { return canvasAIPlan{}, err } if propagate { edges, err := reader.edges(parts[0], parts[1], true) if err != nil { return canvasAIPlan{}, err } for _, edge := range edges { queue = append(queue, edge.Target) } } } filtered := map[string]canvasSnapshot{} for id, snapshot := range reader.snapshots { doc := reader.documents[id] copy := map[string]any{} for key, value := range doc { copy[key] = value } edges := []any{} for _, value := range doc["edges"].([]any) { edge := value.(map[string]any) if reader.used[id][edge["id"].(string)] { edges = append(edges, value) } } copy["edges"] = edges snapshot.Document, _ = json.Marshal(copy) filtered[id] = snapshot } return buildCanvasAIPlan(filtered, board, node, propagate) } func loadCanvasAIPlan(r *http.Request, tx *sql.Tx, project, workspace, account string, parent canvasSnapshot, node string, propagate bool) (canvasAIPlan, error) { reader := canvasAIReader{Load: func(board string) (canvasSnapshot, error) { _, role, err := lockMaterial(r.Context(), tx, project, workspace, board, account) if err != nil { return canvasSnapshot{}, err } if role == "viewer" { return canvasSnapshot{}, sql.ErrNoRows } if board == parent.BoardID { return parent, nil } return loadCanvas(r, tx, project, workspace, board) }, Parent: func(board string) (subgraphRelation, error) { return readSubgraphRelation(r, tx, project, workspace, board) }} return reader.plan(parent.BoardID, node, propagate) }