feat(graph): wire graphsync into MCP write/ingest/tunnel handlers
All checks were successful
CI / Lint / Test / Vet (push) Successful in 13s
CI / Mirror to GitHub (push) Successful in 4s

Commit 2 of Track A. Service stays a no-op until BRAIN_GRAPH_ENABLED=
true; flipping it on creates the schema (idempotent), starts indexing
every successful write, and optionally backfills the existing brain
dir.

- internal/graphsync: best-effort wrapper around graph.Extract +
  graphstore. IndexDoc reads docPath under brainDir, parses, upserts
  entity + replaces edges. BackfillFromBrainDir walks wiki/ +
  knowledge/. Both are no-ops on nil store so callers wire
  unconditionally.

- mcp.Server gains WithGraph builder + graphsync.Store field.
  brain_write, brain_ingest, brain_ingest_raw, brain_tunnel call
  indexInGraph after success — failures slog.Warn but never
  propagate (graph is augmentation, not correctness).

- cmd/server gates the wiring on BRAIN_GRAPH_ENABLED=true (default
  off so first rollout doesn't surprise). BRAIN_GRAPH_BACKFILL=true
  triggers a one-shot walk of the brain dir on boot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mathias
2026-05-23 15:21:33 +02:00
parent f53ee18cb6
commit f43e0bccbf
5 changed files with 318 additions and 1 deletions

View File

@@ -12,6 +12,7 @@ import (
"github.com/mathiasbq/hyperguild/ingestion/internal/api"
"github.com/mathiasbq/hyperguild/ingestion/internal/brain"
"github.com/mathiasbq/hyperguild/ingestion/internal/extract"
"github.com/mathiasbq/hyperguild/ingestion/internal/graphsync"
"github.com/mathiasbq/hyperguild/ingestion/internal/pipeline"
"github.com/mathiasbq/hyperguild/ingestion/internal/search"
"github.com/mathiasbq/hyperguild/ingestion/internal/session"
@@ -194,9 +195,23 @@ func (s *Server) brainWrite(ctx context.Context, args json.RawMessage) (json.Raw
slog.Warn("brain_write: auto-tunnel failed", "src", relPath, "err", err)
}
}
s.indexInGraph(ctx, "brain_write", relPath)
return json.Marshal(map[string]string{"path": relPath})
}
// indexInGraph is a best-effort wrapper around graphsync.IndexDoc that
// logs failures but never propagates them — the underlying write/ingest
// has already succeeded and the graph is an augmentation, not a
// correctness invariant.
func (s *Server) indexInGraph(ctx context.Context, op, relPath string) {
if s.graph == nil || relPath == "" {
return
}
if err := graphsync.IndexDoc(ctx, s.graph, s.brainDir, relPath); err != nil {
slog.Warn(op+": graph index failed", "path", relPath, "err", err)
}
}
type brainTunnelArgs struct {
Source string `json:"source"`
Target string `json:"target"`
@@ -213,6 +228,8 @@ func (s *Server) brainTunnel(ctx context.Context, args json.RawMessage) (json.Ra
if err := brain.WriteTunnel(s.brainDir, a.Source, a.Target); err != nil {
return nil, fmt.Errorf("tunnel: %w", err)
}
s.indexInGraph(ctx, "brain_tunnel", a.Source)
s.indexInGraph(ctx, "brain_tunnel", a.Target)
return json.Marshal(map[string]string{"status": "ok"})
}
@@ -268,6 +285,11 @@ func (s *Server) brainIngestRaw(ctx context.Context, args json.RawMessage) (json
if warnings == nil {
warnings = []string{}
}
if !a.DryRun {
for _, p := range pages {
s.indexInGraph(ctx, "brain_ingest_raw", p)
}
}
return json.Marshal(map[string]any{"pages": pages, "warnings": warnings})
}
@@ -358,6 +380,11 @@ func (s *Server) runIngest(ctx context.Context, content, source string, dryRun b
if pages == nil {
pages = []string{}
}
if !dryRun {
for _, p := range pages {
s.indexInGraph(ctx, "brain_ingest", p)
}
}
warnings := result.Warnings
if warnings == nil {
warnings = []string{}