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

154 lines
4.9 KiB
Go

package analysis
import (
"math"
"sort"
"strconv"
"strings"
)
// Content-addressed cache keying for seeded random walks.
//
// A Personalized-PageRank result over a seed set depends on the
// reachable subgraph. Recomputing it on every query is wasteful when
// the graph — or the part of it the walk touches — has not changed.
// These helpers derive a per-package Merkle root for the snapshot and a
// content-addressed cache key for a walk, so the MCP server's walk cache
// (see internal/mcp/ppr_cache.go) can serve an unchanged walk instantly
// and only recompute when a package the walk depends on actually
// changed — the incremental-RWR property a whole-graph version number
// (NodeCount/EdgeCount) cannot provide.
const (
fnvOffset64 uint64 = 14695981039346656037
fnvPrime64 uint64 = 1099511628211
)
// fnvStr folds a string into a running FNV-1a hash.
func fnvStr(h uint64, s string) uint64 {
for i := 0; i < len(s); i++ {
h ^= uint64(s[i])
h *= fnvPrime64
}
return h
}
// fnvU64 folds a uint64 into a running FNV-1a hash.
func fnvU64(h, v uint64) uint64 {
for i := 0; i < 8; i++ {
h ^= (v >> (8 * i)) & 0xff
h *= fnvPrime64
}
return h
}
// packageOfID returns the package directory for a node ID — the
// directory of the file-path portion (everything before "::"). For
// "gortex/internal/mcp/server.go::NewServer" that is
// "gortex/internal/mcp". A path with no slash maps to "" (repo root).
// This is the granularity at which the walk cache is invalidated.
func packageOfID(id string) string {
path := id
if i := strings.Index(path, "::"); i >= 0 {
path = path[:i]
}
if i := strings.LastIndexByte(path, '/'); i >= 0 {
return path[:i]
}
return ""
}
// computePackageRoots builds the per-package content roots from the CSR.
// For each node it folds the node's stable ID plus its out-edges
// (neighbour string IDs + weights) into the node's content hash, then
// sums those hashes per package. Summation is commutative, so the root
// is independent of node iteration order; it uses string IDs (not dense
// indices), so a package's root is invariant to index shifts caused by
// edits in OTHER packages — the property that makes the cache truly
// incremental.
func computePackageRoots(ids []string, offsets []int32, neighbors []int32, weights []float64) map[string]uint64 {
roots := make(map[string]uint64, len(ids)/8+1)
for i := 0; i < len(ids); i++ {
h := fnvStr(fnvOffset64, ids[i])
h ^= 0 // separator marker folded below
h = fnvU64(h, 0x1f)
start, end := offsets[i], offsets[i+1]
for k := start; k < end; k++ {
h = fnvStr(h, ids[neighbors[k]])
h = fnvU64(h, math.Float64bits(weights[k]))
}
roots[packageOfID(ids[i])] += h
}
return roots
}
// WalkCacheKey derives a content-addressed cache key for a seeded walk.
// The key folds: the resolved (in-snapshot) seed IDs, the restart
// probability, and the per-package roots of the packages the walk
// depends on — the seed packages plus their 1-hop out-neighbour
// packages. Two walks with the same seeds and restart over graph states
// whose relevant packages are byte-for-byte identical produce the same
// key (cache hit); a change to any depended-on package changes that
// package's root and therefore the key (cache miss → recompute).
//
// Returns "" when the snapshot has no package roots (cache disabled) or
// no seed resolves to a snapshot node — the caller then computes
// uncached. The resolved-seed set matches exactly what
// PersonalizedPageRank walks, so the key and the result stay coherent.
func (a *AdjacencySnapshot) WalkCacheKey(seeds []string, restart float64) string {
if a == nil || len(a.pkgRoots) == 0 || len(seeds) == 0 {
return ""
}
if restart <= 0 || restart >= 1 {
restart = pprDefaultRestart
}
resolved := make([]string, 0, len(seeds))
seen := make(map[int]bool, len(seeds))
pkgSet := make(map[string]struct{}, len(seeds)*4)
for _, s := range seeds {
i, ok := a.index[s]
if !ok || seen[i] {
continue
}
seen[i] = true
resolved = append(resolved, s)
pkgSet[packageOfID(s)] = struct{}{}
// 1-hop forward neighbours: their packages also influence the
// walk's first step, so fold them into the dependency set.
for k := a.offsets[i]; k < a.offsets[i+1]; k++ {
pkgSet[packageOfID(a.ids[a.neighbors[k]])] = struct{}{}
}
}
if len(resolved) == 0 {
return ""
}
sort.Strings(resolved)
pkgs := make([]string, 0, len(pkgSet))
for p := range pkgSet {
pkgs = append(pkgs, p)
}
sort.Strings(pkgs)
h := fnvStr(fnvOffset64, "ppr\x00")
for _, s := range resolved {
h = fnvStr(h, s)
h = fnvU64(h, 0x1f)
}
h = fnvU64(h, math.Float64bits(restart))
for _, p := range pkgs {
h = fnvStr(h, p)
h = fnvU64(h, a.pkgRoots[p])
}
return strconv.FormatUint(h, 16)
}
// PackageRootCount returns the number of distinct packages with a
// content root. Exposed for diagnostics / tests.
func (a *AdjacencySnapshot) PackageRootCount() int {
if a == nil {
return 0
}
return len(a.pkgRoots)
}