boost / docs / evaluation system
Quality · shipped in PR #112 · #142 · #145 · #147boost grades its own retrieval quality.
A package manager for AI skills is only as good as the skill it hands back for "the best thing for <task>." So boost measures that — with a required, deterministic golden-set gate that fails the build on a retrieval regression, plus opt-in tiers for statistical significance and LLM-judged faithfulness. Everything degrades gracefully; the required gate has zero dependencies.
Search quality is a number, not a vibe.
boost has several ways to find a skill — a frontmatter heuristic, a full-content BM25 index, an opt-in dense-vector backend, and an LLM reranker. Which one actually returns the right skill for a real question? The eval system answers that empirically, so a change that quietly degrades retrieval can't ship.
The core idea is the classic information-retrieval one: a golden set of
query → the skill(s) that should come back judgments, scored with standard
IR metrics (recall@k, hit@1, MRR, nDCG). It's the same shape as a TREC
qrels file — hand-labelled ground truth that turns "seems good" into "recall
went from 0.92 to 0.71, block the merge."
One golden set, four engines, five verdicts
Every tier reuses the same golden judgments and the same retrieval stack. Data flows left to right; the golden set feeds the scoring stage from above.
The required gate (Tier 1) is pure-stdlib and offline. Everything green-lit beyond it is opt-in.
Five tiers, one rule: never block on a maybe
Only Tier 1 gates the build. It is deterministic and dependency-free precisely so it can be required without ever flaking. Everything that needs a network, a key, or a heavy library is opt-in and skips cleanly when its prerequisites are absent.
| Tier | What it grades | Metric / gate | Needs | |
|---|---|---|---|---|
| 1 · retrieval | Does the right skill come back for a real query? | recall@k, hit@1, MRR and nDCG@k floors vs the pinned corpus |
nothing — pure stdlib, offline | required |
| 1b · significance | Is engine A really better than B, or just lucky? | ranx paired Student's t-test (p<0.05) |
[eval] extra (ranx) |
opt-in |
| 2a · rerank | How much does the LLM reranker improve ordering? | hit@1 / MRR / nDCG lift over raw BM25 | claude CLI or ANTHROPIC_API_KEY |
opt-in |
| 2b · recommend | Are boost recommend's AI picks relevant and grounded? |
precision@k + hard grounding gate (0 hallucinated picks) | claude CLI or ANTHROPIC_API_KEY |
opt-in |
| 2c · explain | Does boost explain stay true to the SKILL.md? |
ragas faithfulness (claims verified vs source) |
[eval] extra + judge key |
opt-in |
What the numbers actually say
Two findings that only appeared once the instrument could see them. Both are
reproducible: scripts/eval_retrieval.py --build -k 10 over the pinned corpus.
1 · The score was mostly about the corpus
The gate reported near-perfect retrieval for a long time. Growing the pinned corpus from six registries to twenty — same golden set, same queries, nothing else changed — moved every metric:
| metric | 6 taps | 20 taps | floor |
|---|---|---|---|
recall@10 | 0.978 | 0.863 | 0.78 |
hit@1 | 0.791 | 0.473 | 0.40 |
MRR | 0.854 | 0.607 | 0.52 |
nDCG@10 | 0.882 | 0.662 | 0.58 |
Three of the four floors in force before that change fail outright on the larger corpus. A
floor calibrated where BM25 scores 1.000 is a floor about the corpus, not about retrieval.
hit@1 0.473 is the honest headline: at twenty taps, fewer than half of
these queries put the right item first. That is not a regression — it is what users
already had, now visible.
2 · The score was also about the shape of the question
The keyword golden set grades catalog items by name, so its queries carry the
vocabulary of the thing they are looking for. That is BM25's home ground. The 50 queries in
golden-natural.jsonl describe a problem instead — each written
from its target's own description, with the target's distinctive name tokens deliberately
stripped — and over identical data BM25 scores recall 0.690 / hit@1 0.240
against 1.000 / 0.780 on the name-shaped set.
Same index, same corpus, same engine. The gap is entirely the phrasing of the question, which is why one query set cannot floor a retrieval system on its own.
3 · Which engine actually wins
The hybrid path fuses BM25 and dense by reciprocal rank rather than preferring either. That
was a design decision, so here it is measured — every engine over the same corpus, both
query sets, k=10. Voyage is absent: it needs an API key, and these runs were
keyless, which is the configuration most users are in.
Keyword golden set — 91 queries that grade items by name:
| engine | recall@10 | hit@1 | MRR | nDCG@10 |
|---|---|---|---|---|
catalog.search (frontmatter) | 0.918 | 0.714 | 0.783 | 0.810 |
| BM25 full-content | 0.978 | 0.791 | 0.854 | 0.882 |
| dense vectors (local, keyless) | 0.956 | 0.780 | 0.853 | 0.876 |
| hybrid RRF | 0.978 | 0.780 | 0.864 | 0.891 |
Natural-language set — 50 queries describing a problem, name tokens stripped:
| engine | recall@10 | hit@1 | MRR | nDCG@10 |
|---|---|---|---|---|
catalog.search (frontmatter) | 0.330 | 0.080 | 0.170 | 0.197 |
| BM25 full-content | 0.750 | 0.340 | 0.474 | 0.524 |
| dense vectors (local, keyless) | 0.760 | 0.420 | 0.541 | 0.580 |
| hybrid RRF | 0.820 | 0.440 | 0.578 | 0.623 |
Fusing beats choosing. Hybrid wins or ties on both sets, and on the natural-language
queries it beats both of its own components on every metric — which is the
case for rank fusion rather than picking a favourite engine. The two components fail in
opposite directions: BM25 takes hit@1 on name-shaped queries (0.791 vs 0.780),
dense takes it decisively on human-phrased ones (0.420 vs 0.340). Preferring either one alone
would hand half the queries to the engine that is worse at them.
Note also how far catalog.search — frontmatter only, no body index —
falls on real questions: hit@1 0.080. Indexing the body is most of what makes
search work, and a name-graded query set alone would never have shown it.
4 · Embedding locally is slower than the plan assumed
Building the keyless dense store over this corpus — 743 entries, 3,740 chunks, ONNX
bge-small-en-v1.5 on CPU — took 4,431 s (74 minutes), about 1.2 s
per chunk. Extrapolated to a full catalogue of ~28k items that is on the order of
days, not the “hour-plus” the roadmap estimated. It does not weaken the
keyless tier — queries embed in milliseconds and the store is built once — but it
does mean shipping prebuilt per-registry vectors is a requirement rather than an
optimisation.
What this changed
Both findings came from making the instrument sharper before trusting it: the gate now
floors four metrics rather than recall@k alone, baselines are keyed to the query
set that produced them, and the corpus is a realistic size. Every retrieval change since is
reported against both query shapes, and several claims did not survive that — unchunking
the index, for one, turned out to trade a little name-query recall for a large gain on
natural-language queries, rather than being the free win it was written up as.
How Tier 1 runs on every PR
The golden set grades items by name, so the repos that hold them must be present. CI taps a pinned corpus first, builds the BM25 index, scores the golden set, and floors recall — all offline after the tap, all deterministic.
On the pinned corpus BM25 recall is 1.000, so the 0.85 floor leaves wide margin — a drift-resistant gate that still catches a broken index or a scoring regression.
Simple on the left, technical on the right
Same concept, two depths. Open the left card for the plain-English version, the right card for what actually happens in the code.
What is a "golden set"?
A hand-written answer key. Each row is a question a user might type and the skill that should come back. To grade any search engine, you run every question through it and check whether the right answer showed up near the top.
qrels + IR metrics
tests/eval/golden.jsonl holds
{query, relevant[], kind} rows — a TREC-style qrels file. The harness runs
each ranker, then computes recall@k, hit@1, MRR
and nDCG@k with binary relevance, sliced per item kind (skill / rule /
workflow).
Why not just match keywords?
The simple approach only reads a skill's short description. BM25 reads the whole skill file, so it finds matches the description never mentions. The eval proves it: BM25 finds the right skill far more often.
0.756 → 0.919 recall@10
The frontmatter heuristic (catalog.search) scores
recall@10 = 0.756; full-content BM25 (rag.retrieve) scores 0.919 on the same
43 queries. That measured +0.163 is the evidence that justifies the RAG stack's
complexity — not a hunch.
Could a better score be luck?
On only a few dozen questions, one engine can look better just by chance. A significance test asks: if you re-ran on fresh questions, would the winner keep winning? It reports "real difference" or "too close to call."
ranx paired t-test
--stats feeds the per-query scores to ranx,
which runs a paired Student's t-test between engines and reports a p-value plus
win/tie/loss. Deterministic, offline. On the small pinned corpus the gap is not
yet significant (p≈0.11–0.26) — an honest signal to widen the set.
Can explain make things up?
boost explain writes a plain-English summary of a skill.
The risk is it invents abilities the skill doesn't have. This tier checks every sentence
against the real skill file and scores how much of the summary is actually supported.
ragas faithfulness
scripts/eval_explain.py generates the explanation with
boost's own ai.ask, then scores it with ragas faithfulness:
the answer is decomposed into claims and each is verified against the SKILL.md context.
LLM-judged, so it's key-gated and never part of make check.
Where do the skills come from?
To grade "did the right skill come back," the skills have to exist. CI starts empty, so the gate first downloads a fixed, hand-picked list of skill repos — always the same list, so the score means the same thing every run.
taps.txt + ensure_eval_corpus.sh
tests/eval/taps.txt is the minimal repo set covering all 44
golden targets (boost tap --defaults omits every rule/workflow repo).
ensure_eval_corpus.sh taps it into $BOOST_HOME — idempotent via
a sentinel. Regression-vs-baseline is relaxed (--regression-eps 1) so
upstream drift can't flake the required gate.
One command per tier
# Tier 1 — required, deterministic, part of `make check` $ make eval GATE recall@10 = 1.000 (min 0.850) -> PASS # Tier 1b — is the difference statistically significant? (needs [eval]) $ make eval-stats best engine: BM25 full-content vs catalog.search ndcg@10 p=0.1127 not significant W/T/L=7/34/2 # Tier 2a / 2b — LLM rerank lift & recommendation grounding (key-gated) $ make eval-ai # rerank: hit@1 +0.163 over raw BM25 $ make eval-rec # recommend: grounded 1.000, 0 hallucinated picks # Tier 2c — explain faithfulness via ragas (needs [eval] + a judge key) $ make eval-explain skill faithfulness pdf 0.94 MEAN 0.91 (n=7)
Illustrative output. Opt-in tiers print a one-line hint and exit 0 when a dependency or key is missing — they never block.