Files
jbergner a6bc71fb3a
Some checks failed
release-tag / Resolve release metadata (push) Successful in 30s
release-tag / Build knowledge (push) Failing after 4m51s
release-tag / Build control (push) Failing after 5m0s
release-tag / Build agent (push) Failing after 5m0s
release-tag / Build agent-data-init (push) Failing after 5m5s
release-tag / Build neuroforge-worker (push) Failing after 5m7s
release-tag / Build neuroforge (push) Failing after 5m9s
Init
2026-08-26 18:34:41 +02:00

426 lines
14 KiB
Go

package main
import (
"embed"
"encoding/json"
"fmt"
"io"
"net/http"
"sort"
"strconv"
"strings"
"sync"
)
//go:embed engineering-graph.json
var engineeringFS embed.FS
type graphNode struct {
ID string `json:"id"`
Kind string `json:"kind"`
Label string `json:"label"`
Group string `json:"group,omitempty"`
Community string `json:"community,omitempty"`
Status string `json:"status,omitempty"`
Score float64 `json:"score,omitempty"`
Meta map[string]any `json:"meta,omitempty"`
}
type graphEdge struct {
ID string `json:"id"`
From string `json:"from"`
To string `json:"to"`
Kind string `json:"kind"`
Label string `json:"label,omitempty"`
Status string `json:"status,omitempty"`
Weight float64 `json:"weight,omitempty"`
Meta map[string]any `json:"meta,omitempty"`
}
type graphPayload struct {
Scope string `json:"scope"`
Title string `json:"title"`
Nodes []graphNode `json:"nodes"`
Edges []graphEdge `json:"edges"`
Meta map[string]any `json:"meta,omitempty"`
}
var engineeringOnce sync.Once
var engineeringGraph graphPayload
var engineeringErr error
func loadEngineeringGraph() (graphPayload, error) {
engineeringOnce.Do(func() {
b, err := engineeringFS.ReadFile("engineering-graph.json")
if err != nil {
engineeringErr = err
return
}
engineeringErr = json.Unmarshal(b, &engineeringGraph)
})
return engineeringGraph, engineeringErr
}
func (s *server) handleGraphRuns(w http.ResponseWriter, r *http.Request) {
s.proxyJSON(w, r, s.agentURL+"/api/control/runs?limit="+strconv.Itoa(boundInt(r.URL.Query().Get("limit"), 40, 1, 100)), bearerHeader(s.agentReadToken))
}
func (s *server) handleTicketGraph(w http.ResponseWriter, r *http.Request) {
id := strings.TrimSpace(r.URL.Query().Get("run_id"))
if id == "" {
http.Error(w, "run_id required", http.StatusBadRequest)
return
}
s.proxyJSON(w, r, s.agentURL+"/api/control/graph/runs/"+urlPathSegment(id), bearerHeader(s.agentReadToken))
}
func (s *server) handleLearningGraph(w http.ResponseWriter, r *http.Request) {
limit := boundInt(r.URL.Query().Get("limit"), 180, 1, 500)
s.proxyJSON(w, r, s.agentURL+"/api/control/graph/learning?limit="+strconv.Itoa(limit), bearerHeader(s.agentReadToken))
}
func (s *server) handleResearchGraph(w http.ResponseWriter, r *http.Request) {
runs := boundInt(r.URL.Query().Get("runs"), 6, 1, 20)
events := boundInt(r.URL.Query().Get("max_events"), 320, 20, 800)
s.proxyJSON(w, r, fmt.Sprintf("%s/api/v1/integrations/graph/research?runs=%d&max_events=%d", s.neuroforgeURL, runs, events), bearerHeader(s.neuroforgeKey))
}
func (s *server) handleBrainGraph(w http.ResponseWriter, r *http.Request) {
max := boundInt(r.URL.Query().Get("max_nodes"), 320, 50, 700)
s.proxyJSON(w, r, fmt.Sprintf("%s/api/v1/integrations/graph/brain?max_nodes=%d", s.neuroforgeURL, max), bearerHeader(s.neuroforgeKey))
}
func (s *server) proxyJSON(w http.ResponseWriter, r *http.Request, url string, auth string) {
if strings.TrimSpace(url) == "" {
http.Error(w, "backend not configured", http.StatusServiceUnavailable)
return
}
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, url, nil)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
if auth != "" {
req.Header.Set("Authorization", auth)
}
resp, err := s.http.Do(req)
if err != nil {
http.Error(w, "graph backend unavailable: "+err.Error(), http.StatusBadGateway)
return
}
defer resp.Body.Close()
b, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(resp.StatusCode)
_, _ = w.Write(b)
}
func (s *server) handleRuntimeGraph(w http.ResponseWriter, r *http.Request) {
statuses := s.statusSnapshot(r.Context())
statusByName := map[string]status{}
for _, st := range statuses {
statusByName[st.Name] = st
}
g := graphPayload{Scope: "runtime", Title: "Runtime & Trust Boundaries", Meta: map[string]any{"read_only": true}}
add := func(id, kind, label, group, community string, meta map[string]any) {
statusText := "configured"
for _, t := range s.targets {
if t.ID == id {
if st, ok := statusByName[t.Name]; ok {
if st.OK {
statusText = "online"
} else {
statusText = "problem"
}
if meta == nil {
meta = map[string]any{}
}
meta["latency_ms"] = st.LatencyMS
meta["public_url"] = st.PublicURL
}
break
}
}
g.Nodes = append(g.Nodes, graphNode{ID: id, Kind: kind, Label: label, Group: group, Community: community, Status: statusText, Meta: meta})
}
add("glpi", "external", "GLPI", "external", "external", nil)
add("agent", "service", "GLPI AI Agent", "operations", "operations", map[string]any{"authority": "policy + GLPI writes"})
add("knowledge", "service", "Knowledgebase", "governance", "knowledge", map[string]any{"authority": "authoring + staging + promotion"})
add("neuroforge", "service", "NeuroForge Brain", "brain", "brain", map[string]any{"authority": "memory + retrieval + research"})
add("ollama", "model_runtime", "Ollama Pool", "runtime", "ai-runtime", nil)
add("searxng", "research_runtime", "SearXNG", "runtime", "research", map[string]any{"optional": true, "enabled": s.searxngEnabled})
add("control", "service", "Control Center", "observability", "control", map[string]any{"authority": "read-only"})
if s.codebaseMemoryPublicURL != "" || s.codebaseMemoryURL != "" {
add("codebase-memory", "engineering", "Codebase Memory MCP", "engineering", "engineering", map[string]any{"optional": true, "public_url": s.codebaseMemoryPublicURL})
}
edges := []graphEdge{
{From: "glpi", To: "agent", Kind: "tickets_api", Label: "OAuth/API"}, {From: "glpi", To: "knowledge", Kind: "kb_sync", Label: "KnowbaseItem + relations"},
{From: "agent", To: "neuroforge", Kind: "knowledge_and_outcomes", Label: "App-key scoped"}, {From: "agent", To: "ollama", Kind: "inference"},
{From: "knowledge", To: "ollama", Kind: "draft_inference"}, {From: "knowledge", To: "neuroforge", Kind: "activity_events"},
{From: "neuroforge", To: "ollama", Kind: "inference"}, {From: "neuroforge", To: "searxng", Kind: "research", Status: boolStatus(s.researchEnabled == "true" && s.searxngEnabled == "true")},
{From: "control", To: "agent", Kind: "read_only_graph", Label: "CONTROL_READ_TOKEN"}, {From: "control", To: "knowledge", Kind: "health_read"}, {From: "control", To: "neuroforge", Kind: "read_only_graph", Label: "App key"},
}
if s.codebaseMemoryPublicURL != "" || s.codebaseMemoryURL != "" {
edges = append(edges, graphEdge{From: "control", To: "codebase-memory", Kind: "engineering_link", Status: "optional"})
}
for i := range edges {
edges[i].ID = edges[i].From + "->" + edges[i].To + ":" + edges[i].Kind
}
g.Edges = edges
writeJSON(w, 200, g)
}
func (s *server) handleEngineeringGraph(w http.ResponseWriter, r *http.Request) {
base, err := loadEngineeringGraph()
if err != nil {
http.Error(w, "engineering graph unavailable: "+err.Error(), 500)
return
}
max := boundInt(r.URL.Query().Get("max_nodes"), 650, 50, 1400)
q := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q")))
kinds := csvSet(r.URL.Query().Get("kinds"))
edgeKinds := csvSet(r.URL.Query().Get("edge_kinds"))
selected := map[string]bool{}
matches := func(n graphNode) bool {
if len(kinds) > 0 && !kinds[n.Kind] {
return false
}
if q == "" {
return engineeringPriority(n.Kind) <= 4
}
blob := strings.ToLower(n.ID + " " + n.Label + " " + fmt.Sprint(n.Meta))
return strings.Contains(blob, q)
}
candidates := append([]graphNode(nil), base.Nodes...)
sort.SliceStable(candidates, func(i, j int) bool {
pi, pj := engineeringPriority(candidates[i].Kind), engineeringPriority(candidates[j].Kind)
if pi != pj {
return pi < pj
}
return strings.ToLower(candidates[i].Label) < strings.ToLower(candidates[j].Label)
})
for _, n := range candidates {
if matches(n) && len(selected) < max {
selected[n.ID] = true
}
}
// Expand one hop around explicit search hits, then fill with structural nodes.
if q != "" {
for pass := 0; pass < 2 && len(selected) < max; pass++ {
for _, e := range base.Edges {
if !(selected[e.From] || selected[e.To]) {
continue
}
if !selected[e.From] && len(selected) < max {
selected[e.From] = true
}
if !selected[e.To] && len(selected) < max {
selected[e.To] = true
}
}
}
}
if len(selected) < max {
for _, n := range candidates {
if len(kinds) > 0 && !kinds[n.Kind] {
continue
}
selected[n.ID] = true
if len(selected) >= max {
break
}
}
}
out := graphPayload{Scope: "engineering", Title: "Engineering Graph", Meta: map[string]any{"total_nodes": len(base.Nodes), "total_edges": len(base.Edges), "node_budget": max, "query": q, "codebase_memory_url": s.codebaseMemoryPublicURL}}
for _, n := range base.Nodes {
if selected[n.ID] {
out.Nodes = append(out.Nodes, n)
}
}
for _, e := range base.Edges {
if !selected[e.From] || !selected[e.To] {
continue
}
if len(edgeKinds) > 0 && !edgeKinds[e.Kind] {
continue
}
out.Edges = append(out.Edges, e)
}
writeJSON(w, 200, out)
}
// handleEngineeringImpact returns a bounded structural blast-radius graph for
// a file, package, route or symbol query. The analysis is deliberately static
// and read-only: it expresses architectural reachability, not production risk
// certainty. Incoming and outgoing dependencies are traversed so callers can
// see both what a symbol uses and what may depend on it.
func (s *server) handleEngineeringImpact(w http.ResponseWriter, r *http.Request) {
base, err := loadEngineeringGraph()
if err != nil {
http.Error(w, "engineering graph unavailable: "+err.Error(), http.StatusInternalServerError)
return
}
q := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("q")))
if q == "" {
http.Error(w, "q required", http.StatusBadRequest)
return
}
depth := boundInt(r.URL.Query().Get("depth"), 2, 1, 4)
max := boundInt(r.URL.Query().Get("max_nodes"), 450, 30, 1200)
nodeByID := make(map[string]graphNode, len(base.Nodes))
selected := map[string]bool{}
frontier := []string{}
for _, n := range base.Nodes {
nodeByID[n.ID] = n
blob := strings.ToLower(n.ID + " " + n.Label + " " + fmt.Sprint(n.Meta))
if strings.Contains(blob, q) && len(selected) < max {
selected[n.ID] = true
frontier = append(frontier, n.ID)
}
}
if len(frontier) == 0 {
writeJSON(w, http.StatusOK, graphPayload{Scope: "impact", Title: "Engineering Change Impact", Meta: map[string]any{"query": q, "depth": depth, "risk": "none", "matches": 0, "node_budget": max}})
return
}
seedCount := len(frontier)
adj := make(map[string][]string, len(base.Nodes))
for _, e := range base.Edges {
if !impactEdgeKind(e.Kind) {
continue
}
adj[e.From] = append(adj[e.From], e.To)
adj[e.To] = append(adj[e.To], e.From)
}
for step := 0; step < depth && len(frontier) > 0 && len(selected) < max; step++ {
next := make([]string, 0)
for _, id := range frontier {
for _, other := range adj[id] {
if selected[other] || len(selected) >= max {
continue
}
selected[other] = true
next = append(next, other)
}
}
frontier = next
}
out := graphPayload{Scope: "impact", Title: "Engineering Change Impact"}
components := map[string]bool{}
routes, services := 0, 0
for _, n := range base.Nodes {
if !selected[n.ID] {
continue
}
if n.Group != "" {
components[n.Group] = true
}
if n.Kind == "route" {
routes++
}
if n.Kind == "service" {
services++
}
out.Nodes = append(out.Nodes, n)
}
for _, e := range base.Edges {
if selected[e.From] && selected[e.To] && impactEdgeKind(e.Kind) {
out.Edges = append(out.Edges, e)
}
}
risk := impactRisk(len(out.Nodes), len(components), routes, services)
out.Meta = map[string]any{
"query": q, "depth": depth, "node_budget": max, "matches": seedCount,
"affected_nodes": len(out.Nodes), "affected_components": sortedBoolKeys(components),
"routes": routes, "services": services, "risk": risk,
"interpretation": "static structural reachability; validate with tests and runtime evidence before deployment",
}
writeJSON(w, http.StatusOK, out)
}
func impactEdgeKind(kind string) bool {
switch kind {
case "calls", "calls_package", "imports", "handles", "defines_route", "depends_on", "defines", "contains_file", "contains_package":
return true
default:
return false
}
}
func impactRisk(nodes, components, routes, services int) string {
switch {
case services > 1 || components > 2 || routes > 4 || nodes >= 120:
return "high"
case services > 0 || components > 1 || routes > 0 || nodes >= 35:
return "medium"
default:
return "low"
}
}
func sortedBoolKeys(m map[string]bool) []string {
out := make([]string, 0, len(m))
for k := range m {
if strings.TrimSpace(k) != "" {
out = append(out, k)
}
}
sort.Strings(out)
return out
}
func boundInt(raw string, def, min, max int) int {
n, err := strconv.Atoi(strings.TrimSpace(raw))
if err != nil || n < min {
return def
}
if n > max {
return max
}
return n
}
func csvSet(v string) map[string]bool {
m := map[string]bool{}
for _, x := range strings.Split(v, ",") {
x = strings.TrimSpace(x)
if x != "" {
m[x] = true
}
}
return m
}
func bearerHeader(v string) string {
v = strings.TrimSpace(v)
if v == "" {
return ""
}
return "Bearer " + v
}
func urlPathSegment(v string) string {
r := strings.NewReplacer("%", "%25", "/", "%2F", "?", "%3F", "#", "%23", " ", "%20")
return r.Replace(v)
}
func engineeringPriority(k string) int {
switch k {
case "component", "service":
return 0
case "route":
return 1
case "package":
return 2
case "file":
return 3
case "function":
return 4
default:
return 5
}
}
func boolStatus(v bool) string {
if v {
return "enabled"
}
return "disabled"
}