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

219 lines
6.9 KiB
Go

package query
import (
"sort"
"strings"
"github.com/zzet/gortex/internal/graph"
)
// ClosureOptions controls a multi-seed dependency-closure traversal
// (Engine.ImportClosure). It generalises WalkBudgeted from a single
// start node to a set of seeds, and ranks every reached node by its
// graph distance to the nearest seed rather than stopping on an
// encoded-size estimate.
type ClosureOptions struct {
// EdgeKinds is the set of edge kinds the closure follows. An empty
// slice falls back to dependencyEdgeKinds (imports / calls /
// references / depends_on plus the infrastructure edges), the same
// allowlist GetDependencies walks.
EdgeKinds []graph.EdgeKind
// MaxDepth caps how far the closure expands from the seed set. A
// non-positive value falls back to a built-in default.
MaxDepth int
// MaxNodes caps the size of the returned closure (the seeds always
// count toward it). A non-positive value falls back to a built-in
// default so a pathological seed set can never expand without
// bound. Nodes are admitted in breadth-first / nearest-first order,
// so the cap keeps the closest nodes.
MaxNodes int
// WorkspaceID / ProjectID scope the traversal exactly as the
// matching WalkOptions fields do — neighbours outside the scope are
// dropped along with the edge that reached them.
WorkspaceID string
ProjectID string
}
// ClosureNode is one node in a dependency closure, tagged with its
// graph distance to the nearest seed (0 for a seed itself).
type ClosureNode struct {
Node *graph.Node
Distance int
}
// ClosureResult is the outcome of a multi-seed closure traversal.
type ClosureResult struct {
// Nodes are the closure members sorted by (distance, ID) so the
// result — and any pack built from it — is deterministic regardless
// of the backend's edge enumeration order.
Nodes []ClosureNode
// Edges are the traversed edges that connect closure members. A
// cross-edge between two already-visited members is still recorded.
Edges []*graph.Edge
// SeedIDs is the de-duplicated, in-scope seed set the traversal
// actually started from (a seed outside scope or absent from the
// graph is dropped before expansion).
SeedIDs []string
// Truncated reports whether the node cap stopped the expansion
// before the closure was fully explored.
Truncated bool
// StoppedAtDepth is the deepest distance the traversal reached.
StoppedAtDepth int
}
const (
closureDefaultMaxDepth = 6
closureDefaultMaxNodes = 400
)
// closureScopeAllows reuses the WalkOptions scope rule so a closure
// enforces the same workspace/project boundary without duplicating the
// fallback logic.
func (o ClosureOptions) closureScopeAllows(n *graph.Node) bool {
return WalkOptions{WorkspaceID: o.WorkspaceID, ProjectID: o.ProjectID}.scopeAllows(n)
}
// ImportClosure walks the transitive dependency closure of a set of
// seeds. Starting from every seed at distance 0, it expands breadth
// first over the chosen edge kinds (default: the dependency allowlist —
// imports / calls / references / depends_on / infra), recording each
// reached node's distance to the NEAREST seed. Unresolved / external
// neighbours are skipped and the workspace/project scope is enforced
// exactly as WalkBudgeted does.
//
// Unlike WalkBudgeted (single seed, token-bounded) this is multi-seed
// and node-bounded: it is the substrate for a closure-context packer
// that ranks the closure by graph distance and hands it to the
// token-budgeted manifest. The returned nodes are sorted by
// (distance, ID) so the result is order-independent across backends.
func (e *Engine) ImportClosure(seedIDs []string, opts ClosureOptions) *ClosureResult {
maxDepth := opts.MaxDepth
if maxDepth <= 0 {
maxDepth = closureDefaultMaxDepth
}
maxNodes := opts.MaxNodes
if maxNodes <= 0 {
maxNodes = closureDefaultMaxNodes
}
kinds := opts.EdgeKinds
if len(kinds) == 0 {
kinds = dependencyEdgeKinds
}
kindSet := make(map[graph.EdgeKind]bool, len(kinds))
for _, k := range kinds {
kindSet[k] = true
}
distance := make(map[string]int)
nodeByID := make(map[string]*graph.Node)
var edges []*graph.Edge
seenEdge := make(map[string]bool)
type item struct {
id string
depth int
}
var queue []item
var seeds []string
// Seed the frontier. A seed that is out of scope, unresolved, or
// absent from the graph never enters the closure — the caller's
// distance ranking and budget should not be perturbed by a seed
// that cannot contribute real context.
seedSeen := make(map[string]bool)
for _, id := range seedIDs {
if id == "" || seedSeen[id] {
continue
}
if graph.IsUnresolvedTarget(id) || strings.HasPrefix(id, "external::") {
continue
}
n := e.g.GetNode(id)
if n == nil || !opts.closureScopeAllows(n) {
continue
}
seedSeen[id] = true
seeds = append(seeds, id)
distance[id] = 0
nodeByID[id] = n
queue = append(queue, item{id: id, depth: 0})
}
truncated := false
stoppedAtDepth := 0
for len(queue) > 0 {
cur := queue[0]
queue = queue[1:]
if cur.depth > stoppedAtDepth {
stoppedAtDepth = cur.depth
}
if cur.depth >= maxDepth {
continue
}
// Closure is a dependency-direction walk: follow outgoing edges
// (what the seed depends on). This mirrors GetDependencies, the
// established "what does X need" traversal.
for _, edge := range e.g.GetOutEdges(cur.id) {
if !kindSet[edge.Kind] {
continue
}
neighborID := edge.To
if graph.IsUnresolvedTarget(neighborID) ||
strings.HasPrefix(neighborID, "external::") {
continue
}
n := e.g.GetNode(neighborID)
if n == nil || !opts.closureScopeAllows(n) {
continue
}
// The edge is part of the result regardless of whether the
// neighbour is new — a cross-edge between two visited closure
// members is still a real dependency. Dedup by identity so a
// parallel edge pair is not double-counted.
ekey := string(edge.Kind) + "\x00" + edge.From + "\x00" + edge.To
if !seenEdge[ekey] {
seenEdge[ekey] = true
edges = append(edges, edge)
}
if _, seen := distance[neighborID]; seen {
continue
}
// Node cap: stop admitting new nodes once the closure is
// full. Already-queued nodes still drain so their connecting
// edges are recorded, but no deeper frontier is added.
if len(nodeByID) >= maxNodes {
truncated = true
continue
}
distance[neighborID] = cur.depth + 1
nodeByID[neighborID] = n
queue = append(queue, item{id: neighborID, depth: cur.depth + 1})
}
}
nodes := make([]ClosureNode, 0, len(nodeByID))
for id, n := range nodeByID {
nodes = append(nodes, ClosureNode{Node: n, Distance: distance[id]})
}
sort.Slice(nodes, func(i, j int) bool {
if nodes[i].Distance != nodes[j].Distance {
return nodes[i].Distance < nodes[j].Distance
}
return nodes[i].Node.ID < nodes[j].Node.ID
})
sort.Strings(seeds)
return &ClosureResult{
Nodes: nodes,
Edges: edges,
SeedIDs: seeds,
Truncated: truncated,
StoppedAtDepth: stoppedAtDepth,
}
}