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
144 lines
4.5 KiB
Go
144 lines
4.5 KiB
Go
package indexer
|
|
|
|
import (
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// Reference-facts sidecar persistence. After resolution settles, each resolved
|
|
// reference edge becomes a durable, auditable fact (from → to + provenance
|
|
// tier) persisted per source file in the backend's ref_facts table. Only the
|
|
// on-disk store implements graph.RefFactsWriter; the in-memory backend has no
|
|
// durable layer to seed, so persistence is a no-op there (the live edges ARE
|
|
// the facts).
|
|
|
|
// refFactsWriter returns the backend's reference-facts persistence capability
|
|
// if it implements one.
|
|
func (idx *Indexer) refFactsWriter() (graph.RefFactsWriter, bool) {
|
|
w, ok := idx.graph.(graph.RefFactsWriter)
|
|
return w, ok
|
|
}
|
|
|
|
// collectRefFacts derives the resolved-reference facts originating in the given
|
|
// nodes: every resolvable reference edge whose target is a concrete (non-stub,
|
|
// non-unresolved) node.
|
|
func collectRefFacts(g graph.Store, nodes []*graph.Node) []graph.RefFact {
|
|
var facts []graph.RefFact
|
|
for _, n := range nodes {
|
|
if n == nil {
|
|
continue
|
|
}
|
|
for _, e := range g.GetOutEdges(n.ID) {
|
|
if e == nil || !graph.IsResolvableRefEdge(e.Kind) {
|
|
continue
|
|
}
|
|
if e.To == "" || graph.IsUnresolvedTarget(e.To) || graph.IsStub(e.To) {
|
|
continue
|
|
}
|
|
refName := ""
|
|
if t := g.GetNode(e.To); t != nil {
|
|
refName = t.Name
|
|
}
|
|
origin := e.Origin
|
|
if origin == "" {
|
|
sem, _ := e.Meta["semantic_source"].(string)
|
|
origin = graph.DefaultOriginFor(e.Kind, e.Confidence, sem)
|
|
}
|
|
facts = append(facts, graph.RefFact{
|
|
RepoPrefix: n.RepoPrefix,
|
|
FromID: e.From,
|
|
ToID: e.To,
|
|
Kind: string(e.Kind),
|
|
RefName: refName,
|
|
Line: e.Line,
|
|
Origin: origin,
|
|
Tier: graph.ResolvedBy(origin),
|
|
FilePath: n.FilePath,
|
|
Lang: n.Language,
|
|
})
|
|
}
|
|
}
|
|
return facts
|
|
}
|
|
|
|
// persistRefFactsForFiles re-derives and persists the resolved-reference facts
|
|
// for the given graph file paths (delete-then-set per file so stale facts from
|
|
// removed references don't linger). Every requested file is deleted even when
|
|
// it yields no fresh facts — a file whose last resolvable reference just
|
|
// degraded to an unresolved stub must drop its stale rows, not keep them
|
|
// because there is nothing new to write. No-op when the backend has no
|
|
// durable layer or the file list is empty.
|
|
func (idx *Indexer) persistRefFactsForFiles(graphPaths []string) {
|
|
w, ok := idx.refFactsWriter()
|
|
if !ok || len(graphPaths) == 0 {
|
|
return
|
|
}
|
|
byRepo := map[string][]graph.RefFact{}
|
|
filesByRepo := map[string]map[string]struct{}{}
|
|
addFile := func(repo, file string) {
|
|
if filesByRepo[repo] == nil {
|
|
filesByRepo[repo] = map[string]struct{}{}
|
|
}
|
|
filesByRepo[repo][file] = struct{}{}
|
|
}
|
|
for _, p := range graphPaths {
|
|
nodes := idx.graph.GetFileNodes(p)
|
|
// Register the file for deletion under its nodes' repo prefix
|
|
// (falling back to the indexer's own) regardless of whether any
|
|
// fresh facts come out of it below.
|
|
repo := idx.repoPrefix
|
|
for _, n := range nodes {
|
|
if n != nil && n.RepoPrefix != "" {
|
|
repo = n.RepoPrefix
|
|
break
|
|
}
|
|
}
|
|
addFile(repo, p)
|
|
for _, f := range collectRefFacts(idx.graph, nodes) {
|
|
byRepo[f.RepoPrefix] = append(byRepo[f.RepoPrefix], f)
|
|
addFile(f.RepoPrefix, f.FilePath)
|
|
}
|
|
}
|
|
for repo, fileSet := range filesByRepo {
|
|
files := make([]string, 0, len(fileSet))
|
|
for f := range fileSet {
|
|
files = append(files, f)
|
|
}
|
|
if err := w.DeleteRefFactsByFiles(repo, files); err != nil {
|
|
idx.logger.Debug("ref-facts: delete failed", zap.Error(err))
|
|
continue
|
|
}
|
|
if err := w.BulkSetRefFacts(repo, byRepo[repo]); err != nil {
|
|
idx.logger.Debug("ref-facts: persist failed", zap.Error(err))
|
|
}
|
|
}
|
|
}
|
|
|
|
// persistAllRefFacts persists reference facts for every indexed file. Called
|
|
// once after a full resolve so a cold index seeds the durable sidecar.
|
|
func (idx *Indexer) persistAllRefFacts() {
|
|
if _, ok := idx.refFactsWriter(); !ok {
|
|
return
|
|
}
|
|
var files []string
|
|
for n := range idx.graph.NodesByKind(graph.KindFile) {
|
|
if n != nil {
|
|
files = append(files, n.ID)
|
|
}
|
|
}
|
|
idx.persistRefFactsForFiles(files)
|
|
}
|
|
|
|
// deleteRefFactsForFiles drops persisted facts sourced in the given graph file
|
|
// paths (used when a file is evicted/deleted). No-op without a durable backend.
|
|
func (idx *Indexer) deleteRefFactsForFiles(repoPrefix string, graphPaths []string) {
|
|
w, ok := idx.refFactsWriter()
|
|
if !ok || len(graphPaths) == 0 {
|
|
return
|
|
}
|
|
if err := w.DeleteRefFactsByFiles(repoPrefix, graphPaths); err != nil {
|
|
idx.logger.Debug("ref-facts: delete-on-evict failed", zap.Error(err))
|
|
}
|
|
}
|