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
193 lines
6.4 KiB
Go
193 lines
6.4 KiB
Go
package indexer
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// Incremental resolution reuse for the single-file edit path.
|
|
//
|
|
// A normal save evicts the whole file, re-parses it (every edge comes back
|
|
// unresolved), and re-resolves all of them — on a large file that means
|
|
// fetching tens of thousands of candidate nodes and scoring thousands of
|
|
// edges, even for a one-line change. But only the edited region's references
|
|
// actually changed; the rest resolve to exactly what they did before.
|
|
//
|
|
// So before eviction we snapshot the file's already-resolved outgoing edges,
|
|
// keyed by their *source-side shape* (origin symbol, kind, receiver type,
|
|
// referenced name) — independent of line number and of the resolved target.
|
|
// After the re-parse, any freshly extracted edge whose shape matches a unique
|
|
// captured resolution is pre-pointed at that target before it enters the
|
|
// graph, so the resolver skips it and only touches genuinely-new edges.
|
|
//
|
|
// Correctness is conservative by construction: a shape that resolved to two
|
|
// different targets (the key cannot tell them apart) is dropped and re-resolved
|
|
// from scratch, and a captured target that no longer exists is ignored. The
|
|
// reuse therefore can only ever reproduce a prior resolution or fall back to
|
|
// the full resolver — never invent a wrong target.
|
|
|
|
type reuseKey struct {
|
|
from string
|
|
kind graph.EdgeKind
|
|
recv string // receiver type for method calls; "" otherwise
|
|
name string // referenced identifier
|
|
}
|
|
|
|
type reuseVal struct {
|
|
to string
|
|
confidence float64
|
|
confLabel string
|
|
origin string
|
|
tier string
|
|
}
|
|
|
|
// captureIncrementalState snapshots, in one walk of the file's outgoing edges:
|
|
// - reuse: resolved in-repo edges keyed by source shape, so an unchanged
|
|
// reference recovers its exact target (ambiguous keys poisoned to nil).
|
|
// - priorUnresolved: shallow copies of the edges that were still unresolved,
|
|
// so the resolver's forward pass can skip re-trying them (they point at
|
|
// stubs the incoming pass binds when a target appears).
|
|
//
|
|
// external:: edges are neither reused nor skipped here: they are not
|
|
// IsUnresolvedTarget, so the resolver already leaves them alone.
|
|
func captureIncrementalState(g graph.Store, graphPath string) (reuse map[reuseKey]*reuseVal, priorUnresolved []*graph.Edge) {
|
|
reuse = map[reuseKey]*reuseVal{}
|
|
nodes := g.GetFileNodes(graphPath)
|
|
ids := make([]string, 0, len(nodes))
|
|
for _, n := range nodes {
|
|
if n != nil {
|
|
ids = append(ids, n.ID)
|
|
}
|
|
}
|
|
byNode := graph.OutEdgesForNodes(g, ids)
|
|
for _, n := range nodes {
|
|
if n == nil {
|
|
continue
|
|
}
|
|
for _, e := range byNode[n.ID] {
|
|
if e == nil || e.To == "" {
|
|
continue
|
|
}
|
|
if graph.IsUnresolvedTarget(e.To) {
|
|
priorUnresolved = append(priorUnresolved, &graph.Edge{
|
|
From: e.From, Kind: e.Kind, To: e.To, Meta: e.Meta,
|
|
})
|
|
continue
|
|
}
|
|
if !reusableResolvedEdge(e) {
|
|
continue
|
|
}
|
|
tgt := g.GetNode(e.To)
|
|
if tgt == nil || tgt.Name == "" {
|
|
continue
|
|
}
|
|
k := reuseKey{from: e.From, kind: e.Kind, recv: edgeReceiverType(e), name: tgt.Name}
|
|
if cur, seen := reuse[k]; seen {
|
|
if cur != nil && cur.to != e.To {
|
|
reuse[k] = nil // ambiguous -> never reuse
|
|
}
|
|
continue
|
|
}
|
|
reuse[k] = &reuseVal{
|
|
to: e.To,
|
|
confidence: e.Confidence,
|
|
confLabel: e.ConfidenceLabel,
|
|
origin: e.Origin,
|
|
tier: e.Tier,
|
|
}
|
|
}
|
|
}
|
|
return reuse, priorUnresolved
|
|
}
|
|
|
|
// applyResolvedOutEdges pre-resolves freshly extracted unresolved edges that
|
|
// match a captured resolution, BEFORE they are added to the graph (so no edge
|
|
// re-keying is needed). Returns how many edges were reused.
|
|
//
|
|
// newIDs holds the IDs of the nodes about to be re-added by the caller's
|
|
// AddBatch. It exists for the same-file case: when a call's target lives in the
|
|
// re-parsed file, eviction removed the target node before this runs, so a bare
|
|
// GetNode(v.to) lookup would miss and the edge would fall through to a full
|
|
// re-resolve — which rebinds the target correctly but drops its origin/tier to
|
|
// the resolver's heuristic default. Because node IDs are file::Name (line- and
|
|
// content-independent), a same-file target re-appears under an identical ID, so
|
|
// "present in newIDs" recovers exactly the reuse that a not-yet-added target
|
|
// would otherwise lose. A target absent from BOTH the graph and newIDs was
|
|
// genuinely deleted and is still skipped.
|
|
func applyResolvedOutEdges(g graph.Store, edges []*graph.Edge, idx map[reuseKey]*reuseVal, newIDs map[string]struct{}) int {
|
|
if len(idx) == 0 {
|
|
return 0
|
|
}
|
|
reused := 0
|
|
for _, e := range edges {
|
|
if e == nil || !graph.IsUnresolvedTarget(e.To) {
|
|
continue
|
|
}
|
|
name := reuseIdentifier(graph.UnresolvedName(e.To))
|
|
if name == "" {
|
|
continue // import/pyrel/grpc targets are owned by global passes
|
|
}
|
|
k := reuseKey{from: e.From, kind: e.Kind, recv: edgeReceiverType(e), name: name}
|
|
v := idx[k]
|
|
if v == nil {
|
|
continue // miss, or poisoned ambiguous key
|
|
}
|
|
if g.GetNode(v.to) == nil {
|
|
if _, reAdded := newIDs[v.to]; !reAdded {
|
|
continue // captured target genuinely deleted since the snapshot
|
|
}
|
|
// Same-file target: evicted above, re-added by AddBatch below under
|
|
// an identical ID — reuse its prior resolution and provenance tier.
|
|
}
|
|
e.To = v.to
|
|
e.Confidence = v.confidence
|
|
e.ConfidenceLabel = v.confLabel
|
|
e.Origin = v.origin
|
|
e.Tier = v.tier
|
|
reused++
|
|
}
|
|
return reused
|
|
}
|
|
|
|
func reusableResolvedEdge(e *graph.Edge) bool {
|
|
if e == nil || e.To == "" {
|
|
return false
|
|
}
|
|
if graph.IsUnresolvedTarget(e.To) || strings.HasPrefix(e.To, "external::") {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func edgeReceiverType(e *graph.Edge) string {
|
|
if e == nil || e.Meta == nil {
|
|
return ""
|
|
}
|
|
if rt, ok := e.Meta["receiver_type"].(string); ok {
|
|
return rt
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// reuseIdentifier mirrors resolver.identifierFromTarget for the reuse key:
|
|
// it extracts the bare referenced name and returns "" for targets a dedicated
|
|
// global pass owns (import/pyrel/grpc), so those are never reused here.
|
|
func reuseIdentifier(target string) string {
|
|
switch {
|
|
case strings.HasPrefix(target, "*."):
|
|
return strings.TrimPrefix(target, "*.")
|
|
case strings.HasPrefix(target, "extern::"):
|
|
spec := strings.TrimPrefix(target, "extern::")
|
|
if sep := strings.LastIndex(spec, "::"); sep >= 0 {
|
|
return spec[sep+2:]
|
|
}
|
|
return ""
|
|
case strings.HasPrefix(target, "import::"),
|
|
strings.HasPrefix(target, "pyrel::"),
|
|
strings.HasPrefix(target, "grpc::"):
|
|
return ""
|
|
}
|
|
return target
|
|
}
|