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
240 lines
7.4 KiB
Go
240 lines
7.4 KiB
Go
// Package packeval is the held-out retrieval-precision harness for
|
|
// context packing. It scores the real retrieval stack against curated
|
|
// gold fixtures on Precision@K / Recall@K / MRR, and A/Bs the pluggable
|
|
// pack strategies (top-k / density / file-grouped) under a fixed token
|
|
// budget — the offline measurement Gortex previously lacked (it tuned
|
|
// rerank weights only from online feedback telemetry).
|
|
//
|
|
// The harness is provider-driven: a RankedProvider returns the ranked,
|
|
// token-costed candidate set for a query (wired to the live engine +
|
|
// rerank pipeline by the `gortex eval pack` CLI). For each strategy the
|
|
// harness packs the candidates into the budget, scores the delivered
|
|
// top-K against the fixture gold, and aggregates overall and per-tier.
|
|
package packeval
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/eval/recall"
|
|
"github.com/zzet/gortex/internal/search/packstrategy"
|
|
)
|
|
|
|
// RankedProvider returns the ranked candidate items for a query, best
|
|
// first, each carrying its file and token cost so a strategy can pack
|
|
// it. limit caps how many candidates to gather before packing.
|
|
type RankedProvider func(query string, limit int) []packstrategy.Item
|
|
|
|
// Metrics holds the standard retrieval-precision numbers for one slice
|
|
// of cases.
|
|
type Metrics struct {
|
|
Cases int `json:"cases"`
|
|
PrecisionAtK float64 `json:"precision_at_k"`
|
|
RecallAtK float64 `json:"recall_at_k"`
|
|
MRR float64 `json:"mrr"`
|
|
}
|
|
|
|
// StrategyResult aggregates a strategy's run over the whole fixture.
|
|
type StrategyResult struct {
|
|
Strategy string `json:"strategy"`
|
|
Overall Metrics `json:"overall"`
|
|
PerTier map[recall.Tier]Metrics `json:"per_tier"`
|
|
MeanSelected float64 `json:"mean_selected"` // avg symbols packed
|
|
MeanTokens float64 `json:"mean_tokens"` // avg tokens packed
|
|
}
|
|
|
|
// Report bundles the sweep.
|
|
type Report struct {
|
|
Fixture string `json:"fixture"`
|
|
K int `json:"k"`
|
|
TokenBudget int `json:"token_budget"`
|
|
FetchLimit int `json:"fetch_limit"`
|
|
Cases int `json:"cases"`
|
|
Strategies []StrategyResult `json:"strategies"`
|
|
}
|
|
|
|
// Options configure a sweep.
|
|
type Options struct {
|
|
Strategies []packstrategy.Strategy // default: packstrategy.All()
|
|
K int // precision/recall cutoff (default 10)
|
|
TokenBudget int // pack budget (default 8000)
|
|
FetchLimit int // candidates gathered before packing (default 50)
|
|
}
|
|
|
|
func (o Options) withDefaults() Options {
|
|
if len(o.Strategies) == 0 {
|
|
o.Strategies = packstrategy.All()
|
|
}
|
|
if o.K <= 0 {
|
|
o.K = 10
|
|
}
|
|
if o.TokenBudget <= 0 {
|
|
o.TokenBudget = 8000
|
|
}
|
|
if o.FetchLimit <= 0 {
|
|
o.FetchLimit = 50
|
|
}
|
|
return o
|
|
}
|
|
|
|
// Run sweeps every strategy over the fixture and returns the report.
|
|
// The provider is invoked once per case per — its result is reused
|
|
// across strategies (packing is pure), so retrieval cost is paid once.
|
|
func Run(fixture recall.Fixture, provider RankedProvider, opts Options) Report {
|
|
opts = opts.withDefaults()
|
|
rep := Report{
|
|
Fixture: fixture.Name,
|
|
K: opts.K,
|
|
TokenBudget: opts.TokenBudget,
|
|
FetchLimit: opts.FetchLimit,
|
|
Cases: len(fixture.Cases),
|
|
}
|
|
|
|
// Gather candidates once per case (retrieval is the expensive part;
|
|
// packing each strategy over the cached candidates is cheap).
|
|
cands := make([][]packstrategy.Item, len(fixture.Cases))
|
|
for i, c := range fixture.Cases {
|
|
cands[i] = provider(c.Query, opts.FetchLimit)
|
|
}
|
|
|
|
for _, strat := range opts.Strategies {
|
|
rep.Strategies = append(rep.Strategies, runStrategy(fixture, cands, strat, opts))
|
|
}
|
|
return rep
|
|
}
|
|
|
|
func runStrategy(fixture recall.Fixture, cands [][]packstrategy.Item, strat packstrategy.Strategy, opts Options) StrategyResult {
|
|
type acc struct {
|
|
cases int
|
|
sumP, sumR, sumMR float64
|
|
}
|
|
overall := &acc{}
|
|
perTier := map[recall.Tier]*acc{}
|
|
var selectedSum, tokensSum float64
|
|
|
|
for i, c := range fixture.Cases {
|
|
selected := packstrategy.Select(strat, cands[i], opts.TokenBudget)
|
|
selectedSum += float64(len(selected))
|
|
toks := 0
|
|
for _, it := range selected {
|
|
toks += it.Tokens
|
|
}
|
|
tokensSum += float64(toks)
|
|
|
|
p, r, mrr := scoreCase(selected, c.Expected, opts.K)
|
|
overall.cases++
|
|
overall.sumP += p
|
|
overall.sumR += r
|
|
overall.sumMR += mrr
|
|
|
|
tier := c.Tier
|
|
if tier == "" {
|
|
tier = recall.TierExact
|
|
}
|
|
a := perTier[tier]
|
|
if a == nil {
|
|
a = &acc{}
|
|
perTier[tier] = a
|
|
}
|
|
a.cases++
|
|
a.sumP += p
|
|
a.sumR += r
|
|
a.sumMR += mrr
|
|
}
|
|
|
|
res := StrategyResult{
|
|
Strategy: string(packstrategy.Normalize(string(strat))),
|
|
Overall: finalize(overall.cases, overall.sumP, overall.sumR, overall.sumMR),
|
|
PerTier: make(map[recall.Tier]Metrics, len(perTier)),
|
|
}
|
|
for tier, a := range perTier {
|
|
res.PerTier[tier] = finalize(a.cases, a.sumP, a.sumR, a.sumMR)
|
|
}
|
|
if n := float64(len(fixture.Cases)); n > 0 {
|
|
res.MeanSelected = selectedSum / n
|
|
res.MeanTokens = tokensSum / n
|
|
}
|
|
return res
|
|
}
|
|
|
|
func finalize(cases int, sumP, sumR, sumMR float64) Metrics {
|
|
m := Metrics{Cases: cases}
|
|
if cases > 0 {
|
|
n := float64(cases)
|
|
m.PrecisionAtK = sumP / n
|
|
m.RecallAtK = sumR / n
|
|
m.MRR = sumMR / n
|
|
}
|
|
return m
|
|
}
|
|
|
|
// scoreCase computes Precision@K, Recall@K, and reciprocal rank for one
|
|
// case against its gold expected set. A case is relevant-at-rank when a
|
|
// delivered item's ID appears in the gold set.
|
|
func scoreCase(selected []packstrategy.Item, expected []string, k int) (precision, recallV, mrr float64) {
|
|
gold := make(map[string]struct{}, len(expected))
|
|
for _, id := range expected {
|
|
gold[id] = struct{}{}
|
|
}
|
|
if len(gold) == 0 {
|
|
return 0, 0, 0
|
|
}
|
|
hits := 0
|
|
firstHit := 0
|
|
for i, it := range selected {
|
|
if i >= k {
|
|
break
|
|
}
|
|
if _, ok := gold[it.ID]; ok {
|
|
hits++
|
|
if firstHit == 0 {
|
|
firstHit = i + 1
|
|
}
|
|
}
|
|
}
|
|
precision = float64(hits) / float64(k)
|
|
recallV = float64(hits) / float64(len(gold))
|
|
if recallV > 1 {
|
|
recallV = 1
|
|
}
|
|
if firstHit > 0 {
|
|
mrr = 1.0 / float64(firstHit)
|
|
}
|
|
return precision, recallV, mrr
|
|
}
|
|
|
|
// Markdown renders the sweep as a diffable report.
|
|
func Markdown(rep Report) string {
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "# Pack-strategy retrieval eval\n\n")
|
|
fmt.Fprintf(&b, "_Fixture: `%s` · %d cases · K=%d · token_budget=%d · fetch_limit=%d_\n\n",
|
|
rep.Fixture, rep.Cases, rep.K, rep.TokenBudget, rep.FetchLimit)
|
|
|
|
b.WriteString("## Overall\n\n")
|
|
fmt.Fprintf(&b, "| strategy | P@%d | R@%d | MRR | mean symbols | mean tokens |\n", rep.K, rep.K)
|
|
b.WriteString("|----------|------|------|-----|--------------|-------------|\n")
|
|
strats := append([]StrategyResult(nil), rep.Strategies...)
|
|
sort.SliceStable(strats, func(i, j int) bool {
|
|
return strats[i].Overall.PrecisionAtK > strats[j].Overall.PrecisionAtK
|
|
})
|
|
for _, s := range strats {
|
|
fmt.Fprintf(&b, "| %s | %5.1f%% | %5.1f%% | %.3f | %.1f | %.0f |\n",
|
|
s.Strategy, s.Overall.PrecisionAtK*100, s.Overall.RecallAtK*100, s.Overall.MRR,
|
|
s.MeanSelected, s.MeanTokens)
|
|
}
|
|
|
|
b.WriteString("\n## Per tier (P@K)\n\n")
|
|
b.WriteString("| strategy | exact | concept | multi_hop |\n")
|
|
b.WriteString("|----------|-------|---------|-----------|\n")
|
|
for _, s := range strats {
|
|
fmt.Fprintf(&b, "| %s | %5.1f%% | %5.1f%% | %5.1f%% |\n",
|
|
s.Strategy,
|
|
s.PerTier[recall.TierExact].PrecisionAtK*100,
|
|
s.PerTier[recall.TierConcept].PrecisionAtK*100,
|
|
s.PerTier[recall.TierMultiHop].PrecisionAtK*100,
|
|
)
|
|
}
|
|
return b.String()
|
|
}
|