package main import ( "fmt" "math" "math/rand/v2" ops "fn-registry/fn_operations" ) // buildGraphData converts entities and relations from operations.db into sigma.js-compatible GraphData. func buildGraphData(db *ops.DB) (GraphData, error) { entities, err := db.ListEntities("", "") if err != nil { return GraphData{}, fmt.Errorf("listing entities: %w", err) } relations, err := db.ListRelations("") if err != nil { return GraphData{}, fmt.Errorf("listing relations: %w", err) } // Compute degree per node degree := map[string]int{} for _, r := range relations { degree[r.FromEntity]++ degree[r.ToEntity]++ } // Build nodes nodes := make([]GraphNode, 0, len(entities)) for _, e := range entities { color, ok := entityTypeColors[e.TypeRef] if !ok { color = "#95a5a6" // default grey } // Size: base + degree factor + risk_score factor size := 8.0 size += math.Min(float64(degree[e.ID])*2.0, 20.0) if rs, ok := e.Metadata["risk_score"]; ok { if v, ok := toFloat64(rs); ok { size += v * 0.1 // risk_score contributes up to ~10 } } nodes = append(nodes, GraphNode{ ID: e.ID, Label: e.Name, Type: e.TypeRef, Color: color, Size: size, X: rand.Float64()*100 - 50, Y: rand.Float64()*100 - 50, Extra: e.Metadata, }) } // Build edges edges := make([]GraphEdge, 0, len(relations)) for _, r := range relations { w := 1.0 if r.Weight != nil { w = *r.Weight } edges = append(edges, GraphEdge{ ID: r.ID, Source: r.FromEntity, Target: r.ToEntity, Label: r.Name, Color: "#ffffff30", Size: math.Max(w*3, 0.5), Type: "arrow", }) } return GraphData{Nodes: nodes, Edges: edges}, nil } // buildEgoGraph returns a subgraph centered on entityID up to depth hops. func buildEgoGraph(db *ops.DB, entityID string, depth int) (GraphData, error) { if depth < 1 { depth = 1 } if depth > 5 { depth = 5 } relations, err := db.ListRelations("") if err != nil { return GraphData{}, err } // BFS to collect entity IDs within depth visited := map[string]bool{entityID: true} frontier := []string{entityID} for d := 0; d < depth; d++ { var next []string for _, id := range frontier { for _, r := range relations { if r.FromEntity == id && !visited[r.ToEntity] { visited[r.ToEntity] = true next = append(next, r.ToEntity) } if r.ToEntity == id && !visited[r.FromEntity] { visited[r.FromEntity] = true next = append(next, r.FromEntity) } } } frontier = next } // Filter entities and relations to subgraph entities, err := db.ListEntities("", "") if err != nil { return GraphData{}, err } degree := map[string]int{} var subRels []ops.Relation for _, r := range relations { if visited[r.FromEntity] && visited[r.ToEntity] { subRels = append(subRels, r) degree[r.FromEntity]++ degree[r.ToEntity]++ } } nodes := make([]GraphNode, 0) for _, e := range entities { if !visited[e.ID] { continue } color, ok := entityTypeColors[e.TypeRef] if !ok { color = "#95a5a6" } size := 8.0 + math.Min(float64(degree[e.ID])*2.0, 20.0) if rs, ok := e.Metadata["risk_score"]; ok { if v, ok := toFloat64(rs); ok { size += v * 0.1 } } nodes = append(nodes, GraphNode{ ID: e.ID, Label: e.Name, Type: e.TypeRef, Color: color, Size: size, X: rand.Float64()*100 - 50, Y: rand.Float64()*100 - 50, Extra: e.Metadata, }) } edges := make([]GraphEdge, 0) for _, r := range subRels { w := 1.0 if r.Weight != nil { w = *r.Weight } edges = append(edges, GraphEdge{ ID: r.ID, Source: r.FromEntity, Target: r.ToEntity, Label: r.Name, Color: "#ffffff30", Size: math.Max(w*3, 0.5), Type: "arrow", }) } return GraphData{Nodes: nodes, Edges: edges}, nil } func toFloat64(v any) (float64, bool) { switch n := v.(type) { case float64: return n, true case float32: return float64(n), true case int: return float64(n), true case int64: return float64(n), true default: return 0, false } }