init: fuzzygraph app from fn_registry
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user