chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
package analysis
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/zzet/gortex/internal/graph"
|
||||
)
|
||||
|
||||
// ComponentResult is one connected component returned by
|
||||
// ComputeWCC / ComputeSCC. Members are sorted ascending so the
|
||||
// output is deterministic across runs.
|
||||
type ComponentResult struct {
|
||||
ID int `json:"id"`
|
||||
Members []string `json:"members"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
|
||||
// ComponentOptions filters the working set the algorithm runs
|
||||
// against. Empty NodeKinds / EdgeKinds means "all kinds".
|
||||
type ComponentOptions struct {
|
||||
NodeKinds []graph.NodeKind
|
||||
EdgeKinds []graph.EdgeKind
|
||||
// MinSize trims trivial singleton components from the
|
||||
// response — common for SCC where every non-cyclic symbol
|
||||
// is its own 1-element SCC.
|
||||
MinSize int
|
||||
}
|
||||
|
||||
// ComputeWCC returns the weakly connected components of g — pairs
|
||||
// of nodes reachable from each other when every edge is treated
|
||||
// as undirected. Components are sorted by size descending; ties
|
||||
// broken by member ID for determinism.
|
||||
//
|
||||
// O(V + E). Used as the fallback when the backing graph.Store
|
||||
// does not implement graph.ComponentFinder.
|
||||
func ComputeWCC(g graph.Store, opts ComponentOptions) []ComponentResult {
|
||||
if g == nil {
|
||||
return nil
|
||||
}
|
||||
nodeAllow := makeComponentKindAllow(opts.NodeKinds)
|
||||
edgeAllow := makeComponentEdgeAllow(opts.EdgeKinds)
|
||||
|
||||
// Build a dense int index over allowed nodes.
|
||||
nodes := g.AllNodes()
|
||||
idx := make(map[string]int, len(nodes))
|
||||
dense := make([]string, 0, len(nodes))
|
||||
for _, n := range nodes {
|
||||
if n == nil || !nodeAllow(n.Kind) {
|
||||
continue
|
||||
}
|
||||
idx[n.ID] = len(dense)
|
||||
dense = append(dense, n.ID)
|
||||
}
|
||||
if len(dense) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Undirected adjacency over allowed edges.
|
||||
adj := make([][]int, len(dense))
|
||||
for _, e := range g.AllEdges() {
|
||||
if e == nil || !edgeAllow(e.Kind) {
|
||||
continue
|
||||
}
|
||||
i, ok1 := idx[e.From]
|
||||
j, ok2 := idx[e.To]
|
||||
if !ok1 || !ok2 || i == j {
|
||||
continue
|
||||
}
|
||||
adj[i] = append(adj[i], j)
|
||||
adj[j] = append(adj[j], i)
|
||||
}
|
||||
|
||||
// Union-find equivalence: BFS from each unseen node, mark
|
||||
// every reachable node with the same component label.
|
||||
comp := make([]int, len(dense))
|
||||
for i := range comp {
|
||||
comp[i] = -1
|
||||
}
|
||||
next := 0
|
||||
queue := make([]int, 0, 64)
|
||||
for i := range dense {
|
||||
if comp[i] != -1 {
|
||||
continue
|
||||
}
|
||||
label := next
|
||||
next++
|
||||
comp[i] = label
|
||||
queue = append(queue[:0], i)
|
||||
for len(queue) > 0 {
|
||||
cur := queue[0]
|
||||
queue = queue[1:]
|
||||
for _, nb := range adj[cur] {
|
||||
if comp[nb] == -1 {
|
||||
comp[nb] = label
|
||||
queue = append(queue, nb)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return collectComponents(dense, comp, opts.MinSize)
|
||||
}
|
||||
|
||||
// ComputeSCC returns the strongly connected components of g —
|
||||
// pairs of nodes mutually reachable along directed edges. Uses
|
||||
// an iterative Tarjan's algorithm to avoid blowing the recursion
|
||||
// stack on a deep call graph. O(V + E).
|
||||
func ComputeSCC(g graph.Store, opts ComponentOptions) []ComponentResult {
|
||||
if g == nil {
|
||||
return nil
|
||||
}
|
||||
nodeAllow := makeComponentKindAllow(opts.NodeKinds)
|
||||
edgeAllow := makeComponentEdgeAllow(opts.EdgeKinds)
|
||||
|
||||
nodes := g.AllNodes()
|
||||
idx := make(map[string]int, len(nodes))
|
||||
dense := make([]string, 0, len(nodes))
|
||||
for _, n := range nodes {
|
||||
if n == nil || !nodeAllow(n.Kind) {
|
||||
continue
|
||||
}
|
||||
idx[n.ID] = len(dense)
|
||||
dense = append(dense, n.ID)
|
||||
}
|
||||
if len(dense) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Directed adjacency. Only out-edges — SCC walks one way.
|
||||
adj := make([][]int, len(dense))
|
||||
for _, e := range g.AllEdges() {
|
||||
if e == nil || !edgeAllow(e.Kind) {
|
||||
continue
|
||||
}
|
||||
i, ok1 := idx[e.From]
|
||||
j, ok2 := idx[e.To]
|
||||
if !ok1 || !ok2 {
|
||||
continue
|
||||
}
|
||||
adj[i] = append(adj[i], j)
|
||||
}
|
||||
|
||||
// Iterative Tarjan. State arrays sized to the dense node
|
||||
// count; the call stack is replaced by an explicit (node,
|
||||
// neighbour-iteration-index) stack.
|
||||
n := len(dense)
|
||||
const undefined = -1
|
||||
idxArr := make([]int, n)
|
||||
lowlink := make([]int, n)
|
||||
onStack := make([]bool, n)
|
||||
for i := range idxArr {
|
||||
idxArr[i] = undefined
|
||||
}
|
||||
stack := make([]int, 0, n)
|
||||
type frame struct {
|
||||
v int
|
||||
ni int // next-neighbour index to visit
|
||||
}
|
||||
work := make([]frame, 0, n)
|
||||
|
||||
var index int
|
||||
comp := make([]int, n)
|
||||
for i := range comp {
|
||||
comp[i] = -1
|
||||
}
|
||||
nextComp := 0
|
||||
|
||||
for start := 0; start < n; start++ {
|
||||
if idxArr[start] != undefined {
|
||||
continue
|
||||
}
|
||||
// Initialise the explicit DFS for this root.
|
||||
idxArr[start] = index
|
||||
lowlink[start] = index
|
||||
index++
|
||||
stack = append(stack, start)
|
||||
onStack[start] = true
|
||||
work = append(work, frame{v: start, ni: 0})
|
||||
|
||||
for len(work) > 0 {
|
||||
top := &work[len(work)-1]
|
||||
v := top.v
|
||||
neighbors := adj[v]
|
||||
if top.ni < len(neighbors) {
|
||||
w := neighbors[top.ni]
|
||||
top.ni++
|
||||
if idxArr[w] == undefined {
|
||||
// Descend into w.
|
||||
idxArr[w] = index
|
||||
lowlink[w] = index
|
||||
index++
|
||||
stack = append(stack, w)
|
||||
onStack[w] = true
|
||||
work = append(work, frame{v: w, ni: 0})
|
||||
} else if onStack[w] {
|
||||
if idxArr[w] < lowlink[v] {
|
||||
lowlink[v] = idxArr[w]
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
// All neighbours consumed; pop the frame and propagate
|
||||
// the lowlink upward.
|
||||
work = work[:len(work)-1]
|
||||
if len(work) > 0 {
|
||||
parent := &work[len(work)-1]
|
||||
if lowlink[v] < lowlink[parent.v] {
|
||||
lowlink[parent.v] = lowlink[v]
|
||||
}
|
||||
}
|
||||
// Emit an SCC if v is its lowlink root.
|
||||
if lowlink[v] == idxArr[v] {
|
||||
label := nextComp
|
||||
nextComp++
|
||||
for {
|
||||
w := stack[len(stack)-1]
|
||||
stack = stack[:len(stack)-1]
|
||||
onStack[w] = false
|
||||
comp[w] = label
|
||||
if w == v {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return collectComponents(dense, comp, opts.MinSize)
|
||||
}
|
||||
|
||||
// collectComponents groups dense node IDs by component label,
|
||||
// applies MinSize, sorts members for determinism, and returns
|
||||
// the slice ordered by size descending.
|
||||
func collectComponents(dense []string, comp []int, minSize int) []ComponentResult {
|
||||
groups := make(map[int][]string)
|
||||
for i, id := range dense {
|
||||
c := comp[i]
|
||||
if c < 0 {
|
||||
continue
|
||||
}
|
||||
groups[c] = append(groups[c], id)
|
||||
}
|
||||
out := make([]ComponentResult, 0, len(groups))
|
||||
for c, members := range groups {
|
||||
if minSize > 0 && len(members) < minSize {
|
||||
continue
|
||||
}
|
||||
sort.Strings(members)
|
||||
out = append(out, ComponentResult{ID: c, Members: members, Size: len(members)})
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].Size != out[j].Size {
|
||||
return out[i].Size > out[j].Size
|
||||
}
|
||||
if len(out[i].Members) > 0 && len(out[j].Members) > 0 {
|
||||
return out[i].Members[0] < out[j].Members[0]
|
||||
}
|
||||
return out[i].ID < out[j].ID
|
||||
})
|
||||
// Renumber sequentially so the output IDs are 0..N-1 in
|
||||
// size-descending order. Stable for snapshot tests.
|
||||
for i := range out {
|
||||
out[i].ID = i
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func makeComponentKindAllow(kinds []graph.NodeKind) func(graph.NodeKind) bool {
|
||||
if len(kinds) == 0 {
|
||||
return func(graph.NodeKind) bool { return true }
|
||||
}
|
||||
set := make(map[graph.NodeKind]struct{}, len(kinds))
|
||||
for _, k := range kinds {
|
||||
set[k] = struct{}{}
|
||||
}
|
||||
return func(k graph.NodeKind) bool {
|
||||
_, ok := set[k]
|
||||
return ok
|
||||
}
|
||||
}
|
||||
|
||||
func makeComponentEdgeAllow(kinds []graph.EdgeKind) func(graph.EdgeKind) bool {
|
||||
if len(kinds) == 0 {
|
||||
return func(graph.EdgeKind) bool { return true }
|
||||
}
|
||||
set := make(map[graph.EdgeKind]struct{}, len(kinds))
|
||||
for _, k := range kinds {
|
||||
set[k] = struct{}{}
|
||||
}
|
||||
return func(k graph.EdgeKind) bool {
|
||||
_, ok := set[k]
|
||||
return ok
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user