a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
677 lines
21 KiB
Go
677 lines
21 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
|
|
mcp "github.com/mark3labs/mcp-go/mcp"
|
|
|
|
"github.com/zzet/gortex/internal/contracts"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// rrfK is the standard reciprocal-rank-fusion smoothing constant: the
|
|
// fused contribution of an item ranked r in one signal is 1/(k+r).
|
|
// k=60 keeps single-signal rank-1 hits from drowning out items that
|
|
// place moderately well across several signals.
|
|
const rrfK = 60.0
|
|
|
|
// bridgeSideEntry is one provider- or consumer-side participant of a
|
|
// contract bridge group: where the contract lives and which symbol
|
|
// carries it.
|
|
type bridgeSideEntry struct {
|
|
ContractID string `json:"contract_id"`
|
|
Repo string `json:"repo,omitempty"`
|
|
SymbolID string `json:"symbol_id,omitempty"`
|
|
FilePath string `json:"file_path,omitempty"`
|
|
Line int `json:"line,omitempty"`
|
|
}
|
|
|
|
// bridgeGroupResult is one ranked contract-bridge group: the bridge
|
|
// node plus its provider side, consumer side, and (in rank mode) the
|
|
// fused score with per-signal ranks.
|
|
type bridgeGroupResult struct {
|
|
BridgeID string `json:"bridge_id"`
|
|
CanonicalKey string `json:"canonical_key"`
|
|
ContractType string `json:"contract_type"`
|
|
Repos []string `json:"repos,omitempty"`
|
|
CrossRepo bool `json:"cross_repo,omitempty"`
|
|
ProviderCount int `json:"provider_count"`
|
|
ConsumerCount int `json:"consumer_count"`
|
|
Providers []bridgeSideEntry `json:"providers"`
|
|
Consumers []bridgeSideEntry `json:"consumers"`
|
|
FusedScore float64 `json:"fused_score,omitempty"`
|
|
SignalRanks map[string]int `json:"signal_ranks,omitempty"`
|
|
// MatchedVia lists the anchor contract IDs that reached this
|
|
// bridge in impact mode.
|
|
MatchedVia []string `json:"matched_via,omitempty"`
|
|
}
|
|
|
|
// handleContractBridges serves `contracts action=bridge`: queries the
|
|
// persisted contract-bridge subgraph (KindContractBridge nodes +
|
|
// EdgeBridges fan-out materialised by the indexer's contract
|
|
// reconcile).
|
|
//
|
|
// Two modes:
|
|
//
|
|
// - rank (default): order bridge groups by reciprocal rank fusion
|
|
// over independent signal rankings — text match on the canonical
|
|
// key/contract names, path+repo match, graph adjacency to the
|
|
// given symbol, and bridge consumer degree. Pass `query` and/or
|
|
// `symbol`.
|
|
// - impact: given `symbol`, return every bridge reachable from the
|
|
// symbol's contracts (its own and its file's) — the cross-service
|
|
// blast radius of changing that symbol.
|
|
func (s *Server) handleContractBridges(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
mode := req.GetString("mode", "rank")
|
|
query := strings.TrimSpace(req.GetString("query", ""))
|
|
symbolID := strings.TrimSpace(req.GetString("symbol", ""))
|
|
limit := req.GetInt("limit", 10)
|
|
if limit <= 0 {
|
|
limit = 10
|
|
}
|
|
|
|
allowed, err := s.resolveRepoFilter(ctx, req)
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
|
|
groups := s.collectBridgeGroups(allowed)
|
|
if len(groups) == 0 {
|
|
return mcp.NewToolResultError("no contract bridges materialized — index repositories with matched provider/consumer contracts first"), nil
|
|
}
|
|
|
|
switch mode {
|
|
case "impact":
|
|
return s.bridgeImpact(ctx, req, groups, symbolID)
|
|
case "rank", "":
|
|
return s.bridgeRank(ctx, req, groups, query, symbolID, limit)
|
|
default:
|
|
return mcp.NewToolResultError("unknown bridge mode: " + mode + " (expected: rank or impact)"), nil
|
|
}
|
|
}
|
|
|
|
// collectBridgeGroups materialises the queryable view of every
|
|
// persisted bridge node, resolving the provider/consumer sides
|
|
// through the live contract registry (with the contract node's own
|
|
// Meta as fallback so the view survives a daemon restart that hasn't
|
|
// rehydrated the registry yet). A non-nil allowed set scopes the
|
|
// result to bridges touching at least one allowed repo.
|
|
func (s *Server) collectBridgeGroups(allowed map[string]bool) []*bridgeGroupResult {
|
|
registry := s.effectiveContractRegistry()
|
|
var out []*bridgeGroupResult
|
|
for n := range s.graph.NodesByKind(graph.KindContractBridge) {
|
|
if n == nil || n.Meta == nil {
|
|
continue
|
|
}
|
|
grp := &bridgeGroupResult{
|
|
BridgeID: n.ID,
|
|
CanonicalKey: bridgeMetaString(n.Meta, "canonical_key"),
|
|
ContractType: bridgeMetaString(n.Meta, "contract_type"),
|
|
Repos: bridgeMetaStringSlice(n.Meta, "repos"),
|
|
}
|
|
if v, ok := n.Meta["cross_repo"].(bool); ok {
|
|
grp.CrossRepo = v
|
|
}
|
|
grp.ProviderCount = bridgeMetaInt(n.Meta, "provider_count")
|
|
grp.ConsumerCount = bridgeMetaInt(n.Meta, "consumer_count")
|
|
|
|
// The bridge node is pinned to one (workspace, project) match
|
|
// boundary; the registry lookup below must be scoped to it so a
|
|
// same-ID contract record in an unrelated workspace doesn't leak
|
|
// into this bridge's participant list. WorkspaceID lives on the
|
|
// node; project rides in Meta. Both default to the node's repo
|
|
// prefix when unset (the same "missing → repo-name" rule the
|
|
// matcher uses), so older nodes without the Meta keys still scope.
|
|
bnd := bridgeBoundary{
|
|
workspace: bridgeBoundarySlug(n.WorkspaceID, n.Meta, "workspace", n.RepoPrefix),
|
|
project: bridgeBoundarySlug("", n.Meta, "project", n.RepoPrefix),
|
|
}
|
|
|
|
for _, e := range s.graph.GetOutEdges(n.ID) {
|
|
if e.Kind != graph.EdgeBridges {
|
|
continue
|
|
}
|
|
side := "provider"
|
|
if e.Meta != nil {
|
|
if v, _ := e.Meta["side"].(string); v != "" {
|
|
side = v
|
|
}
|
|
}
|
|
provs, cons := s.bridgeSideEntries(registry, e.To, side, bnd)
|
|
grp.Providers = append(grp.Providers, provs...)
|
|
grp.Consumers = append(grp.Consumers, cons...)
|
|
}
|
|
sortBridgeSide(grp.Providers)
|
|
sortBridgeSide(grp.Consumers)
|
|
|
|
if allowed != nil && !bridgeTouchesRepos(grp, allowed) {
|
|
continue
|
|
}
|
|
out = append(out, grp)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].BridgeID < out[j].BridgeID })
|
|
return out
|
|
}
|
|
|
|
// bridgeBoundary is the (workspace, project) the matcher paired a
|
|
// bridge's contracts inside. bridgeSideEntries scopes its registry
|
|
// lookup to it so a same-ID record in an unrelated workspace can't be
|
|
// listed as a participant of this bridge.
|
|
type bridgeBoundary struct {
|
|
workspace string
|
|
project string
|
|
}
|
|
|
|
// matches reports whether a contract belongs to the boundary. An empty
|
|
// boundary slug (older bridge node without the Meta keys, or a
|
|
// genuinely empty workspace) matches everything so the read path stays
|
|
// backward-compatible.
|
|
func (b bridgeBoundary) matches(c contracts.Contract) bool {
|
|
if b.workspace != "" && c.EffectiveWorkspace() != b.workspace {
|
|
return false
|
|
}
|
|
if b.project != "" && c.EffectiveProject() != b.project {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// bridgeBoundarySlug resolves a boundary slug from the bridge node:
|
|
// prefer the explicit value, then Meta[key], then the repo-prefix
|
|
// default the matcher falls back to when the slug is unset.
|
|
func bridgeBoundarySlug(explicit string, meta map[string]any, key, repoPrefix string) string {
|
|
if explicit != "" {
|
|
return explicit
|
|
}
|
|
if v := bridgeMetaString(meta, key); v != "" {
|
|
return v
|
|
}
|
|
return repoPrefix
|
|
}
|
|
|
|
// bridgeSideEntries resolves the participant records for one
|
|
// EdgeBridges endpoint. side="both" expands to records on both roles
|
|
// (an exact-ID match collapses provider and consumer into one
|
|
// contract node). Records outside the bridge's match boundary are
|
|
// filtered out so a same-ID contract in an unrelated workspace is not
|
|
// listed as a participant.
|
|
func (s *Server) bridgeSideEntries(registry *contracts.Registry, contractID, side string, bnd bridgeBoundary) (provs, cons []bridgeSideEntry) {
|
|
wantProv := side == "provider" || side == "both"
|
|
wantCons := side == "consumer" || side == "both"
|
|
|
|
if registry != nil {
|
|
records := registry.ByID(contractID)
|
|
for _, c := range records {
|
|
if !bnd.matches(c) {
|
|
continue
|
|
}
|
|
entry := bridgeSideEntry{
|
|
ContractID: contractID,
|
|
Repo: c.RepoPrefix,
|
|
SymbolID: c.SymbolID,
|
|
FilePath: c.FilePath,
|
|
Line: c.Line,
|
|
}
|
|
switch {
|
|
case c.Role == contracts.RoleProvider && wantProv:
|
|
provs = append(provs, entry)
|
|
case c.Role == contracts.RoleConsumer && wantCons:
|
|
cons = append(cons, entry)
|
|
}
|
|
}
|
|
if len(provs) > 0 || len(cons) > 0 {
|
|
return provs, cons
|
|
}
|
|
}
|
|
|
|
// Fallback: the contract node itself. It carries a single role's
|
|
// Meta (same-ID records collapse), so this is best-effort — the
|
|
// registry path above is authoritative whenever it has data.
|
|
n := s.graph.GetNode(contractID)
|
|
if n == nil {
|
|
return nil, nil
|
|
}
|
|
entry := bridgeSideEntry{
|
|
ContractID: contractID,
|
|
Repo: n.RepoPrefix,
|
|
FilePath: n.FilePath,
|
|
}
|
|
if n.Meta != nil {
|
|
entry.SymbolID = bridgeMetaString(n.Meta, "symbol_id")
|
|
entry.Line = bridgeMetaInt(n.Meta, "line")
|
|
}
|
|
if wantProv {
|
|
provs = append(provs, entry)
|
|
}
|
|
if wantCons {
|
|
cons = append(cons, entry)
|
|
}
|
|
return provs, cons
|
|
}
|
|
|
|
// bridgeRank orders bridge groups by reciprocal rank fusion over the
|
|
// independent signal rankings that apply to the request.
|
|
func (s *Server) bridgeRank(ctx context.Context, req mcp.CallToolRequest, groups []*bridgeGroupResult, query, symbolID string, limit int) (*mcp.CallToolResult, error) {
|
|
rankings := make(map[string][]string)
|
|
byID := make(map[string]*bridgeGroupResult, len(groups))
|
|
for _, g := range groups {
|
|
byID[g.BridgeID] = g
|
|
}
|
|
|
|
if query != "" {
|
|
tokens := bridgeQueryTokens(query)
|
|
rankings["text"] = rankBridges(groups, func(g *bridgeGroupResult) float64 {
|
|
return bridgeTextScore(g, tokens)
|
|
})
|
|
rankings["path_repo"] = rankBridges(groups, func(g *bridgeGroupResult) float64 {
|
|
return bridgePathRepoScore(g, tokens)
|
|
})
|
|
}
|
|
|
|
if symbolID != "" {
|
|
symNode := s.graph.GetNode(symbolID)
|
|
if symNode == nil {
|
|
return mcp.NewToolResultError("symbol not found: " + symbolID), nil
|
|
}
|
|
anchors := s.bridgeAnchorContracts(symNode)
|
|
rankings["adjacency"] = rankBridges(groups, func(g *bridgeGroupResult) float64 {
|
|
return bridgeAdjacencyScore(g, symNode, anchors)
|
|
})
|
|
}
|
|
|
|
// Degree always participates: a heavily-consumed contract group is
|
|
// the more load-bearing answer at equal text/graph relevance.
|
|
rankings["degree"] = rankBridges(groups, func(g *bridgeGroupResult) float64 {
|
|
return float64(g.ConsumerCount)
|
|
})
|
|
|
|
fused, perSignal := reciprocalRankFusion(rankings, rrfK)
|
|
|
|
ranked := make([]*bridgeGroupResult, 0, len(groups))
|
|
for id, score := range fused {
|
|
g := byID[id]
|
|
if g == nil {
|
|
continue
|
|
}
|
|
g.FusedScore = score
|
|
g.SignalRanks = perSignal[id]
|
|
ranked = append(ranked, g)
|
|
}
|
|
sort.Slice(ranked, func(i, j int) bool {
|
|
if ranked[i].FusedScore != ranked[j].FusedScore {
|
|
return ranked[i].FusedScore > ranked[j].FusedScore
|
|
}
|
|
return ranked[i].BridgeID < ranked[j].BridgeID
|
|
})
|
|
total := len(ranked)
|
|
if len(ranked) > limit {
|
|
ranked = ranked[:limit]
|
|
}
|
|
|
|
if isCompact(req) {
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "bridges: %d (showing %d)\n", total, len(ranked))
|
|
for _, g := range ranked {
|
|
fmt.Fprintf(&b, " %.4f %s [%s] providers=%d consumers=%d repos=%s\n",
|
|
g.FusedScore, g.CanonicalKey, g.ContractType,
|
|
g.ProviderCount, g.ConsumerCount, strings.Join(g.Repos, ","))
|
|
}
|
|
return mcp.NewToolResultText(b.String()), nil
|
|
}
|
|
|
|
payload := map[string]any{
|
|
"mode": "rank",
|
|
"groups": ranked,
|
|
"total": total,
|
|
"signals": signalNames(rankings),
|
|
}
|
|
return s.respondJSONOrTOON(ctx, req, payload)
|
|
}
|
|
|
|
// bridgeImpact returns every bridge reachable from the symbol's
|
|
// contract surface: contracts attached to the symbol itself plus the
|
|
// contracts declared in its file, expanded through the persisted
|
|
// EdgeBridges in-edges.
|
|
func (s *Server) bridgeImpact(ctx context.Context, req mcp.CallToolRequest, groups []*bridgeGroupResult, symbolID string) (*mcp.CallToolResult, error) {
|
|
if symbolID == "" {
|
|
return mcp.NewToolResultError("symbol is required for bridge impact mode"), nil
|
|
}
|
|
symNode := s.graph.GetNode(symbolID)
|
|
if symNode == nil {
|
|
return mcp.NewToolResultError("symbol not found: " + symbolID), nil
|
|
}
|
|
anchors := s.bridgeAnchorContracts(symNode)
|
|
if len(anchors) == 0 {
|
|
payload := map[string]any{
|
|
"mode": "impact",
|
|
"symbol": symbolID,
|
|
"groups": []*bridgeGroupResult{},
|
|
"total": 0,
|
|
"note": "no contracts attached to this symbol or its file",
|
|
}
|
|
return s.respondJSONOrTOON(ctx, req, payload)
|
|
}
|
|
|
|
byID := make(map[string]*bridgeGroupResult, len(groups))
|
|
for _, g := range groups {
|
|
byID[g.BridgeID] = g
|
|
}
|
|
|
|
matchedVia := make(map[string][]string)
|
|
for contractID := range anchors {
|
|
for _, e := range s.graph.GetInEdges(contractID) {
|
|
if e.Kind != graph.EdgeBridges {
|
|
continue
|
|
}
|
|
if _, ok := byID[e.From]; !ok {
|
|
continue
|
|
}
|
|
matchedVia[e.From] = append(matchedVia[e.From], contractID)
|
|
}
|
|
}
|
|
|
|
impacted := make([]*bridgeGroupResult, 0, len(matchedVia))
|
|
for bridgeID, via := range matchedVia {
|
|
g := byID[bridgeID]
|
|
sort.Strings(via)
|
|
g.MatchedVia = dedupeSortedStrings(via)
|
|
impacted = append(impacted, g)
|
|
}
|
|
sort.Slice(impacted, func(i, j int) bool {
|
|
if impacted[i].ConsumerCount != impacted[j].ConsumerCount {
|
|
return impacted[i].ConsumerCount > impacted[j].ConsumerCount
|
|
}
|
|
return impacted[i].BridgeID < impacted[j].BridgeID
|
|
})
|
|
|
|
if isCompact(req) {
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "impacted bridges: %d (symbol %s)\n", len(impacted), symbolID)
|
|
for _, g := range impacted {
|
|
fmt.Fprintf(&b, " %s [%s] consumers=%d repos=%s via=%s\n",
|
|
g.CanonicalKey, g.ContractType, g.ConsumerCount,
|
|
strings.Join(g.Repos, ","), strings.Join(g.MatchedVia, ","))
|
|
}
|
|
return mcp.NewToolResultText(b.String()), nil
|
|
}
|
|
|
|
payload := map[string]any{
|
|
"mode": "impact",
|
|
"symbol": symbolID,
|
|
"groups": impacted,
|
|
"total": len(impacted),
|
|
}
|
|
return s.respondJSONOrTOON(ctx, req, payload)
|
|
}
|
|
|
|
// bridgeAnchorContracts returns the contract IDs anchored to a
|
|
// symbol: contracts attached to the symbol itself plus every contract
|
|
// declared in the symbol's file. This is the entry set a change to
|
|
// the symbol can reach without leaving its file.
|
|
func (s *Server) bridgeAnchorContracts(symNode *graph.Node) map[string]bool {
|
|
anchors := make(map[string]bool)
|
|
registry := s.effectiveContractRegistry()
|
|
if registry != nil {
|
|
for _, c := range registry.BySymbol(symNode.ID) {
|
|
anchors[c.ID] = true
|
|
}
|
|
if symNode.FilePath != "" {
|
|
for _, c := range registry.ByFile(symNode.FilePath) {
|
|
anchors[c.ID] = true
|
|
}
|
|
}
|
|
}
|
|
// Graph fallback: provides/consumes out-edges land on contract
|
|
// nodes directly.
|
|
for _, e := range s.graph.GetOutEdges(symNode.ID) {
|
|
if e.Kind == graph.EdgeProvides || e.Kind == graph.EdgeConsumes {
|
|
anchors[e.To] = true
|
|
}
|
|
}
|
|
return anchors
|
|
}
|
|
|
|
// reciprocalRankFusion fuses independent per-signal rankings into one
|
|
// score per item: fused(i) = Σ_s 1/(k + rank_s(i)) over the signals
|
|
// that ranked the item (1-based ranks). Items absent from a signal
|
|
// contribute nothing for it. Returns the fused scores plus each
|
|
// item's per-signal rank for explainability.
|
|
func reciprocalRankFusion(rankings map[string][]string, k float64) (map[string]float64, map[string]map[string]int) {
|
|
fused := make(map[string]float64)
|
|
perSignal := make(map[string]map[string]int)
|
|
for signal, ids := range rankings {
|
|
for i, id := range ids {
|
|
rank := i + 1
|
|
fused[id] += 1.0 / (k + float64(rank))
|
|
if perSignal[id] == nil {
|
|
perSignal[id] = make(map[string]int)
|
|
}
|
|
perSignal[id][signal] = rank
|
|
}
|
|
}
|
|
return fused, perSignal
|
|
}
|
|
|
|
// rankBridges scores every group and returns the IDs of those with a
|
|
// positive score, best-first. Ties break on bridge ID so rankings are
|
|
// deterministic.
|
|
func rankBridges(groups []*bridgeGroupResult, score func(*bridgeGroupResult) float64) []string {
|
|
type scored struct {
|
|
id string
|
|
score float64
|
|
}
|
|
var hits []scored
|
|
for _, g := range groups {
|
|
if sc := score(g); sc > 0 {
|
|
hits = append(hits, scored{g.BridgeID, sc})
|
|
}
|
|
}
|
|
sort.Slice(hits, func(i, j int) bool {
|
|
if hits[i].score != hits[j].score {
|
|
return hits[i].score > hits[j].score
|
|
}
|
|
return hits[i].id < hits[j].id
|
|
})
|
|
ids := make([]string, len(hits))
|
|
for i, h := range hits {
|
|
ids[i] = h.id
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// bridgeQueryTokens lowercases and splits a free-text query into
|
|
// alphanumeric terms.
|
|
func bridgeQueryTokens(query string) []string {
|
|
return strings.FieldsFunc(strings.ToLower(query), func(r rune) bool {
|
|
return (r < 'a' || r > 'z') && (r < '0' || r > '9')
|
|
})
|
|
}
|
|
|
|
// bridgeTextScore matches query tokens against the bridge's canonical
|
|
// key, contract type, and the contract IDs on both sides (those embed
|
|
// the service/method/path/topic vocabulary). Token-boundary hits
|
|
// weigh double a bare substring hit.
|
|
func bridgeTextScore(g *bridgeGroupResult, tokens []string) float64 {
|
|
var docParts []string
|
|
docParts = append(docParts, g.CanonicalKey, g.ContractType)
|
|
for _, e := range g.Providers {
|
|
docParts = append(docParts, e.ContractID, e.SymbolID)
|
|
}
|
|
for _, e := range g.Consumers {
|
|
docParts = append(docParts, e.ContractID, e.SymbolID)
|
|
}
|
|
doc := strings.ToLower(strings.Join(docParts, " "))
|
|
docTokens := make(map[string]bool)
|
|
for _, t := range bridgeQueryTokens(doc) {
|
|
docTokens[t] = true
|
|
}
|
|
score := 0.0
|
|
for _, tok := range tokens {
|
|
switch {
|
|
case docTokens[tok]:
|
|
score += 2
|
|
case strings.Contains(doc, tok):
|
|
score++
|
|
}
|
|
}
|
|
return score
|
|
}
|
|
|
|
// bridgePathRepoScore matches query tokens against the bridge's repo
|
|
// spread and the file paths of its participants.
|
|
func bridgePathRepoScore(g *bridgeGroupResult, tokens []string) float64 {
|
|
var docParts []string
|
|
docParts = append(docParts, g.Repos...)
|
|
for _, e := range g.Providers {
|
|
docParts = append(docParts, e.FilePath, e.Repo)
|
|
}
|
|
for _, e := range g.Consumers {
|
|
docParts = append(docParts, e.FilePath, e.Repo)
|
|
}
|
|
doc := strings.ToLower(strings.Join(docParts, " "))
|
|
docTokens := make(map[string]bool)
|
|
for _, t := range bridgeQueryTokens(doc) {
|
|
docTokens[t] = true
|
|
}
|
|
score := 0.0
|
|
for _, tok := range tokens {
|
|
switch {
|
|
case docTokens[tok]:
|
|
score += 2
|
|
case strings.Contains(doc, tok):
|
|
score++
|
|
}
|
|
}
|
|
return score
|
|
}
|
|
|
|
// bridgeAdjacencyScore scores a bridge by graph proximity to the
|
|
// query symbol: bridges directly anchored to one of the symbol's
|
|
// contracts rank above same-file participants, which rank above
|
|
// same-directory, which rank above same-repo.
|
|
func bridgeAdjacencyScore(g *bridgeGroupResult, symNode *graph.Node, anchors map[string]bool) float64 {
|
|
best := 0.0
|
|
consider := func(e bridgeSideEntry) {
|
|
score := 0.0
|
|
switch {
|
|
case anchors[e.ContractID]:
|
|
score = 8
|
|
case e.FilePath != "" && e.FilePath == symNode.FilePath:
|
|
score = 4
|
|
case e.FilePath != "" && symNode.FilePath != "" &&
|
|
path.Dir(e.FilePath) == path.Dir(symNode.FilePath):
|
|
score = 2
|
|
case e.Repo != "" && e.Repo == symNode.RepoPrefix:
|
|
score = 1
|
|
}
|
|
if score > best {
|
|
best = score
|
|
}
|
|
}
|
|
for _, e := range g.Providers {
|
|
consider(e)
|
|
}
|
|
for _, e := range g.Consumers {
|
|
consider(e)
|
|
}
|
|
return best
|
|
}
|
|
|
|
// bridgeTouchesRepos reports whether the bridge group involves at
|
|
// least one repo in the allowed set.
|
|
func bridgeTouchesRepos(g *bridgeGroupResult, allowed map[string]bool) bool {
|
|
for _, r := range g.Repos {
|
|
if allowed[r] {
|
|
return true
|
|
}
|
|
}
|
|
for _, e := range g.Providers {
|
|
if allowed[e.Repo] {
|
|
return true
|
|
}
|
|
}
|
|
for _, e := range g.Consumers {
|
|
if allowed[e.Repo] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func sortBridgeSide(entries []bridgeSideEntry) {
|
|
sort.Slice(entries, func(i, j int) bool {
|
|
if entries[i].ContractID != entries[j].ContractID {
|
|
return entries[i].ContractID < entries[j].ContractID
|
|
}
|
|
if entries[i].Repo != entries[j].Repo {
|
|
return entries[i].Repo < entries[j].Repo
|
|
}
|
|
if entries[i].FilePath != entries[j].FilePath {
|
|
return entries[i].FilePath < entries[j].FilePath
|
|
}
|
|
return entries[i].Line < entries[j].Line
|
|
})
|
|
}
|
|
|
|
func signalNames(rankings map[string][]string) []string {
|
|
names := make([]string, 0, len(rankings))
|
|
for name := range rankings {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
return names
|
|
}
|
|
|
|
func dedupeSortedStrings(in []string) []string {
|
|
out := in[:0]
|
|
var prev string
|
|
for i, s := range in {
|
|
if i > 0 && s == prev {
|
|
continue
|
|
}
|
|
out = append(out, s)
|
|
prev = s
|
|
}
|
|
return out
|
|
}
|
|
|
|
// metaString / metaInt / metaStringSlice read loosely-typed Node.Meta
|
|
// values that may have round-tripped through a persistence backend
|
|
// (gob restores []string as-is; JSON paths may yield []any / float64).
|
|
func bridgeMetaString(meta map[string]any, key string) string {
|
|
v, _ := meta[key].(string)
|
|
return v
|
|
}
|
|
|
|
func bridgeMetaInt(meta map[string]any, key string) int {
|
|
switch v := meta[key].(type) {
|
|
case int:
|
|
return v
|
|
case int64:
|
|
return int(v)
|
|
case float64:
|
|
return int(v)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func bridgeMetaStringSlice(meta map[string]any, key string) []string {
|
|
switch v := meta[key].(type) {
|
|
case []string:
|
|
return v
|
|
case []any:
|
|
out := make([]string, 0, len(v))
|
|
for _, item := range v {
|
|
if s, ok := item.(string); ok {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
return nil
|
|
}
|