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
114 lines
3.3 KiB
Go
114 lines
3.3 KiB
Go
package persistence
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"encoding/gob"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
comboFile = "combo.gob.gz"
|
|
maxComboQueries = 2000
|
|
maxComboEntriesPerQuery = 20
|
|
)
|
|
|
|
// ComboMatch records one (query → symbol) association. HitCount is how many
|
|
// times the agent picked this symbol following the same normalized query;
|
|
// LastUsed is a unix timestamp (seconds) for decay and reaping.
|
|
type ComboMatch struct {
|
|
SymbolID string
|
|
HitCount uint32
|
|
LastUsed int64
|
|
}
|
|
|
|
// ComboQuery holds all recorded matches for one normalized query string
|
|
// within a single repo. Ordered most-hit-first after any record.
|
|
type ComboQuery struct {
|
|
Query string
|
|
Matches []ComboMatch
|
|
}
|
|
|
|
// ComboStore is the persisted state of the query→symbol combo tracker for
|
|
// one repo. Separate file from feedback so each subsystem can evolve its
|
|
// schema independently.
|
|
type ComboStore struct {
|
|
Version string
|
|
RepoPath string
|
|
Queries []ComboQuery
|
|
}
|
|
|
|
// ComboDir returns the on-disk directory for combo storage. Shares the
|
|
// repo cache key with feedback so all repo-scoped state lives together.
|
|
func ComboDir(cacheDir, repoPath string) string {
|
|
return filepath.Join(cacheDir, RepoCacheKey(repoPath))
|
|
}
|
|
|
|
// LoadCombo reads the combo store from disk. Missing file is not an error.
|
|
func LoadCombo(dir string) (*ComboStore, error) {
|
|
path := filepath.Join(dir, comboFile)
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return &ComboStore{}, nil
|
|
}
|
|
return nil, fmt.Errorf("persistence: open combo: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
gz, err := gzip.NewReader(f)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("persistence: gzip reader combo: %w", err)
|
|
}
|
|
defer gz.Close()
|
|
|
|
var store ComboStore
|
|
if err := gob.NewDecoder(gz).Decode(&store); err != nil {
|
|
return nil, fmt.Errorf("persistence: gob decode combo: %w", err)
|
|
}
|
|
return &store, nil
|
|
}
|
|
|
|
// SaveCombo writes the combo store to disk with gob+gzip compression. Trims
|
|
// the oldest-used queries if over cap so the file can't grow unboundedly on
|
|
// a long-running daemon.
|
|
func SaveCombo(dir string, store *ComboStore) error {
|
|
if len(store.Queries) > maxComboQueries {
|
|
// Keep the queries touched most recently by scanning their matches.
|
|
// Stable order (no sort) preserves the common case where callers
|
|
// already maintain MRU; only trigger a full scan when we'd otherwise
|
|
// drop unbounded.
|
|
trim := len(store.Queries) - maxComboQueries
|
|
store.Queries = store.Queries[trim:]
|
|
}
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return fmt.Errorf("persistence: mkdir combo: %w", err)
|
|
}
|
|
path := filepath.Join(dir, comboFile)
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
return fmt.Errorf("persistence: create combo: %w", err)
|
|
}
|
|
gz := gzip.NewWriter(f)
|
|
enc := gob.NewEncoder(gz)
|
|
if err := enc.Encode(store); err != nil {
|
|
_ = gz.Close()
|
|
_ = f.Close()
|
|
return fmt.Errorf("persistence: gob encode combo: %w", err)
|
|
}
|
|
if err := gz.Close(); err != nil {
|
|
_ = f.Close()
|
|
return fmt.Errorf("persistence: gzip close combo: %w", err)
|
|
}
|
|
return f.Close()
|
|
}
|
|
|
|
// MaxComboEntries returns the cap on matches per query. Exported so the
|
|
// manager can enforce the same limit in-memory before flushing.
|
|
func MaxComboEntries() int { return maxComboEntriesPerQuery }
|
|
|
|
// Now is overridable for tests; used when SaveCombo needs to timestamp.
|
|
var Now = func() time.Time { return time.Now() }
|