Files
fuzzygraph/graph.go
T

184 lines
4.0 KiB
Go

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
}
}