package resolver import ( "sort" "github.com/zzet/gortex/internal/graph" ) // mediatrVia is the Meta["via"] tag the C# extractor stamps on a MediatR // Send/Publish placeholder. const mediatrVia = "mediatr-dispatch" // ResolveMediatRCalls binds .NET MediatR dispatches to their handler's // Handle method by request type: `_mediator.Send(new CreateOrder())` → // `CreateOrderHandler.Handle` (one handler), and `_bus.Publish(new // OrderPlaced())` → every `INotificationHandler` (fan-out). // Type-keyed via the handler base list, so edges land at the typed // framework tier. // // Returns the number of caller → handler edges synthesized. func ResolveMediatRCalls(g graph.Store) int { if g == nil { return 0 } // index: kind → request type → handler Handle methods. index := map[string]map[string][]*graph.Node{"request": {}, "notification": {}} for _, n := range nodesByKindsOrAll(g, graph.KindMethod, graph.KindFunction) { if n == nil || n.Meta == nil { continue } reqType, _ := n.Meta["mediatr_request_type"].(string) kind, _ := n.Meta["mediatr_kind"].(string) if reqType == "" || index[kind] == nil { continue } index[kind][reqType] = append(index[kind][reqType], n) } if len(index["request"]) == 0 && len(index["notification"]) == 0 { return 0 } resolved := 0 var reindex []graph.EdgeReindex var batch []*graph.Edge for e := range g.EdgesByKind(graph.EdgeCalls) { if e == nil || e.Meta == nil { continue } if v, _ := e.Meta["via"].(string); v != mediatrVia { continue } reqType, _ := e.Meta["mediatr_request_type"].(string) kind, _ := e.Meta["mediatr_kind"].(string) if reqType == "" || index[kind] == nil { continue } cands := index[kind][reqType] if kind == "request" { // A request has a single handler; never guess on ambiguity. target := pickStoreAction(g, e, sameBoundaryCandidates(g, e.From, cands)) resolved += mediatrRebind(e, target, &reindex) continue } // A notification fans out to every handler. handlers := sameBoundaryCandidates(g, e.From, cands) sort.Slice(handlers, func(i, j int) bool { return handlers[i].ID < handlers[j].ID }) if len(handlers) == 0 { resolved += mediatrRebind(e, nil, &reindex) continue } resolved += mediatrRebind(e, handlers[0], &reindex) for _, h := range handlers[1:] { batch = append(batch, mediatrFanoutEdge(e, h, reqType)) resolved++ } } if len(reindex) > 0 { g.ReindexEdges(reindex) } for _, ne := range batch { g.AddEdge(ne) } return resolved } // mediatrRebind points the placeholder e at target (typed tier) or // re-orphans it. Returns 1 when it lands on a real handler, else 0. func mediatrRebind(e *graph.Edge, target *graph.Node, reindex *[]graph.EdgeReindex) int { want := "unresolved::*.Handle" if target != nil { want = target.ID } if e.To == want { if target != nil { return 1 } return 0 } oldTo := e.To e.To = want hit := 0 if target != nil { e.Origin = graph.OriginASTInferred e.Confidence = ConfidenceTyped e.ConfidenceLabel = graph.ConfidenceLabelFor(graph.EdgeCalls, ConfidenceTyped) StampSynthesizedTyped(e, SynthMediatR) hit = 1 } else { e.Origin = graph.OriginASTInferred e.Confidence = 0 e.ConfidenceLabel = "" UnstampSynthesized(e) } *reindex = append(*reindex, graph.EdgeReindex{Edge: e, OldTo: oldTo}) return hit } func mediatrFanoutEdge(e *graph.Edge, handler *graph.Node, reqType string) *graph.Edge { return &graph.Edge{ From: e.From, To: handler.ID, Kind: graph.EdgeCalls, FilePath: e.FilePath, Line: e.Line, Origin: graph.OriginASTInferred, Confidence: ConfidenceTyped, ConfidenceLabel: graph.ConfidenceLabelFor(graph.EdgeCalls, ConfidenceTyped), Meta: map[string]any{ "via": mediatrVia, "mediatr_request_type": reqType, "mediatr_kind": "notification", MetaSynthesizedBy: SynthMediatR, MetaProvenance: ProvenanceFramework, }, } }