Files
foundry-core/internal/platform/subgraph_migration.go
codex-bot d6ee8942f1
Foundry Go checks / check (push) Successful in 2m40s
Foundry Go checks / check (pull_request) Successful in 2m36s
Implement Canvas V2 subgraphs, assets and asynchronous AI execution
2026-11-06 18:36:00 +08:00

196 lines
6.4 KiB
Go

package platform
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"sort"
)
// A plan contains only explicit legacy memberships. Coordinates are local to
// the containing canvas; the viewport is never part of this transformation.
type legacyFramePlan struct {
FrameID string `json:"frame_id"`
NodeIDs []string `json:"node_ids"`
InternalEdges []string `json:"internal_edges"`
BoundaryEdges []string `json:"boundary_edges"`
}
type legacyMigrationPlan struct {
PlanID string `json:"plan_id"`
ParentVersion int64 `json:"parent_version"`
SourceDigest string `json:"source_digest"`
ChildCount int `json:"child_count"`
Frames []legacyFramePlan `json:"frames"`
Issues []string `json:"issues"`
}
func legacyPlan(project, workspace string, parent canvasSnapshot) legacyMigrationPlan {
var doc map[string]any
_ = json.Unmarshal(parent.Document, &doc)
canonical, _ := json.Marshal(doc)
digest := sha256.Sum256(canonical)
p := legacyMigrationPlan{ParentVersion: parent.Version, SourceDigest: hex.EncodeToString(digest[:]), Frames: []legacyFramePlan{}, Issues: []string{}}
nodes, _ := doc["nodes"].([]any)
edges, _ := doc["edges"].([]any)
membership := map[string]string{}
for _, v := range nodes {
n := v.(map[string]any)
d := n["data"].(map[string]any)
if f, ok := d["frameId"].(string); ok {
membership[n["id"].(string)] = f
}
}
for _, v := range nodes {
n := v.(map[string]any)
d := n["data"].(map[string]any)
if d["kind"] != "frame" || d["subgraph"] != nil {
continue
}
id := n["id"].(string)
f := legacyFramePlan{FrameID: id, NodeIDs: []string{}, InternalEdges: []string{}, BoundaryEdges: []string{}}
for node, frame := range membership {
if frame == id {
f.NodeIDs = append(f.NodeIDs, node)
}
}
sort.Strings(f.NodeIDs)
for _, v := range edges {
e := v.(map[string]any)
a, b := membership[e["source"].(string)] == id, membership[e["target"].(string)] == id
if a && b {
f.InternalEdges = append(f.InternalEdges, e["id"].(string))
} else if a || b {
f.BoundaryEdges = append(f.BoundaryEdges, e["id"].(string))
}
}
p.Frames = append(p.Frames, f)
}
sort.Slice(p.Frames, func(i, j int) bool { return p.Frames[i].FrameID < p.Frames[j].FrameID })
p.ChildCount = len(p.Frames)
identity := fmt.Sprintf("%s/%s/%s/%d/%s", project, workspace, parent.BoardID, parent.Version, p.SourceDigest)
id := sha256.Sum256([]byte(identity))
p.PlanID = hex.EncodeToString(id[:])
return p
}
// migrateLegacyDocument mutates only a freshly decoded snapshot. Boundary edges
// stay in the parent and are redirected through stable, deduplicated ports.
func migrateLegacyDocument(raw json.RawMessage, children map[string]string) (json.RawMessage, map[string]json.RawMessage, error) {
var doc map[string]any
if err := json.Unmarshal(raw, &doc); err != nil {
return nil, nil, err
}
nodes := doc["nodes"].([]any)
edges := doc["edges"].([]any)
frames := map[string]map[string]any{}
owner := map[string]string{}
childDocs := map[string]map[string]any{}
for _, v := range nodes {
n := v.(map[string]any)
id := n["id"].(string)
if _, ok := children[id]; ok {
frames[id] = n
childDocs[id] = map[string]any{"schema_version": 2, "nodes": []any{}, "edges": []any{}, "viewport": map[string]any{"x": 0, "y": 0, "zoom": 1}}
}
}
parentNodes := []any{}
for _, v := range nodes {
n := v.(map[string]any)
d := n["data"].(map[string]any)
frame, _ := d["frameId"].(string)
if f := frames[frame]; f != nil {
p := f["position"].(map[string]any)
pos := n["position"].(map[string]any)
pos["x"] = pos["x"].(float64) - p["x"].(float64) - 24
pos["y"] = pos["y"].(float64) - p["y"].(float64) - 36
delete(d, "frameId")
owner[n["id"].(string)] = frame
childDocs[frame]["nodes"] = append(childDocs[frame]["nodes"].([]any), n)
} else {
parentNodes = append(parentNodes, n)
}
}
for id, f := range frames {
d := f["data"].(map[string]any)
d["subgraph"] = map[string]any{"boardId": children[id]}
d["ports"] = []any{}
d["presentation"] = map[string]any{"collapsed": false, "contentScale": 1, "expandedSize": map[string]any{"width": f["width"], "height": f["height"]}, "collapsedSize": map[string]any{"width": 240, "height": 140}}
}
parentEdges := []any{}
for _, v := range edges {
e := v.(map[string]any)
a, b := owner[e["source"].(string)], owner[e["target"].(string)]
if a != "" && a == b {
// Only knot positions change coordinate spaces. Control vectors do not.
p := frames[a]["position"].(map[string]any)
translateEdgeKnots(e, -p["x"].(float64)-24, -p["y"].(float64)-36)
childDocs[a]["edges"] = append(childDocs[a]["edges"].([]any), e)
continue
}
for _, end := range []string{"source", "target"} {
node := e[end].(string)
frame := owner[node]
if frame == "" {
continue
}
handle, _ := e[end+"Handle"].(string)
if handle == "" {
if end == "source" {
handle = "right"
} else {
handle = "left"
}
}
direction := "input"
if end == "source" {
direction = "output"
}
sum := sha256.Sum256([]byte(direction + "/" + node + "/" + handle))
portID := "p-" + hex.EncodeToString(sum[:16])
data := frames[frame]["data"].(map[string]any)
ports := data["ports"].([]any)
found := false
for _, v := range ports {
if v.(map[string]any)["id"] == portID {
found = true
}
}
if !found {
data["ports"] = append(ports, map[string]any{"id": portID, "direction": direction, "nodeId": node, "handle": handle})
}
e[end] = frame
e[end+"Handle"] = "port:" + portID
}
parentEdges = append(parentEdges, e)
}
doc["nodes"] = parentNodes
doc["edges"] = parentEdges
doc["schema_version"] = 2
encoded, _ := json.Marshal(doc)
parent, ok := normalizeCanvas(encoded)
if !ok {
return nil, nil, fmt.Errorf("invalid migrated parent")
}
result := map[string]json.RawMessage{}
for frame, d := range childDocs {
encoded, _ := json.Marshal(d)
normalized, ok := normalizeCanvas(encoded)
if !ok {
return nil, nil, fmt.Errorf("invalid migrated child")
}
result[frame] = normalized
}
return parent, result, nil
}
func translateEdgeKnots(e map[string]any, x, y float64) {
data, _ := e["data"].(map[string]any)
path, _ := data["path"].(map[string]any)
knots, _ := path["knots"].([]any)
for _, v := range knots {
p := v.(map[string]any)["position"].(map[string]any)
p["x"] = p["x"].(float64) + x
p["y"] = p["y"].(float64) + y
}
}