Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

126 lines
4.1 KiB
Go

package analysis
import "github.com/zzet/gortex/internal/graph"
// weightedLink is one adjacency entry carrying the provenance weight of
// the edge it represents. Shared by ComputeHITS and ComputePageRank so
// both centrality measures attenuate over-represented LSP-dispatch
// edges the same way.
type weightedLink struct {
id string
w float64
}
// PageRankResult holds per-node PageRank centrality scores.
type PageRankResult struct {
// Scores maps node ID to its PageRank value. The values sum to
// ~1 across all nodes; individual scores are small and best read
// relative to Max.
Scores map[string]float64
// Max is the largest score in Scores — the normaliser callers use
// to project centrality onto a 0..1 / 0..100 scale.
Max float64
}
// ScoreOf returns the PageRank score for a node, or 0 when absent.
func (r *PageRankResult) ScoreOf(id string) float64 {
if r == nil {
return 0
}
return r.Scores[id]
}
// PageRank tuning. Damping 0.85 is the canonical web-graph value;
// iterations are fixed rather than convergence-tested because the
// graph is small enough that 40 power-iteration steps are well past
// the point the ranking order stabilises.
const (
pageRankDamping = 0.85
pageRankIterations = 40
)
// ComputePageRank runs PageRank centrality over the call / reference
// graph. Rank flows backwards along call edges: a function is central
// when central functions call it, so a heavily-depended-on symbol
// accumulates score. Only EdgeCalls and EdgeReferences participate —
// structural edges (defines, member_of, imports) would drown the
// dependency signal.
//
// Dangling nodes (no outgoing call/reference edge — leaf utilities)
// redistribute their mass uniformly each iteration so the scores stay
// a proper probability distribution.
func ComputePageRank(g graph.Store) *PageRankResult {
if g == nil {
return &PageRankResult{Scores: map[string]float64{}}
}
nodes := excludeProxyNodes(g.AllNodes())
n := len(nodes)
if n == 0 {
return &PageRankResult{Scores: map[string]float64{}}
}
// Provenance-weighted adjacency: each edge contributes its
// graph.ProvenanceWeight to the source's out-weight and rides that
// weight on the in-link. Score then flows along an edge in
// proportion to w/outWeight, so the transition matrix columns
// still sum to 1 (mass is conserved) but an abundant LSP-dispatch
// fan-out no longer hands a leaf utility outsized centrality. With
// uniform weights the w/outWeight ratio reduces to 1/outDegree —
// identical to the unweighted PageRank.
outWeight := make(map[string]float64, n)
inLinks := make(map[string][]weightedLink)
// Meta-less kind-scoped scan: this pass reads only e.Kind, endpoints, and
// graph.ProvenanceWeight — never arbitrary Meta — so it must not pay to decode
// every edge's meta blob on a warm-restart whole-graph run.
for _, e := range graph.EdgesForKindsLight(g, graph.EdgeCalls, graph.EdgeReferences) {
if e.Kind != graph.EdgeCalls && e.Kind != graph.EdgeReferences {
continue
}
if edgeTouchesProxy(e) {
continue
}
w := graph.ProvenanceWeight(e)
outWeight[e.From] += w
inLinks[e.To] = append(inLinks[e.To], weightedLink{e.From, w})
}
score := make(map[string]float64, n)
initial := 1.0 / float64(n)
for _, nd := range nodes {
score[nd.ID] = initial
}
base := (1 - pageRankDamping) / float64(n)
for iter := 0; iter < pageRankIterations; iter++ {
// Dangling nodes have nowhere to send their score; pool it
// and spread it across every node so no mass leaks.
var dangling float64
for _, nd := range nodes {
if outWeight[nd.ID] == 0 {
dangling += score[nd.ID]
}
}
danglingShare := pageRankDamping * dangling / float64(n)
next := make(map[string]float64, n)
for _, nd := range nodes {
var sum float64
for _, src := range inLinks[nd.ID] {
if d := outWeight[src.id]; d > 0 {
sum += score[src.id] * src.w / d
}
}
next[nd.ID] = base + danglingShare + pageRankDamping*sum
}
score = next
}
var max float64
for _, v := range score {
if v > max {
max = v
}
}
return &PageRankResult{Scores: score, Max: max}
}