Small fixed recall bench over the demo bundle (demo/, ~11 non-reserved concepts).
Not a public leaderboard. Not BrainBench / MTEB / BEIR.
Purpose: show that local BM25 (zero deps, no embeddings) already beats paste-into-grep
on natural paraphrases — and document the numbers honestly.
OKF_ROOT=demo node tools/bench-recall.mjs
# or simply (defaults OKF_ROOT to ./demo):
node tools/bench-recall.mjsStdout = summary table + per-query top-3. Exit code is always 0 (observational, not a CI gate).
CI still runs node --test tools/*.test.mjs only.
| Piece | Detail |
|---|---|
| Corpus | demo/**/*.md concepts/entities/projects (reserved index.md / log.md excluded from BM25) |
| Queries | 12 fixed natural-language paraphrases (no exact title tokens — no «Lumen», «Nova», «Context budget», …) |
| Golden | One concept id per query (hand-labeled) |
| BM25 | rankByKeywords → lib/bm25.mjs over title+description+tags+body (docText), top-3 |
| grep-phrase | single grep -il '<full query string>' over demo markdown (no tokenization) |
| grep-terms | per-word grep -il, rank files by count of matching query terms (no IDF) |
| Metrics | hit@1 / hit@3 = golden appears in the first 1 / 3 ranked ids |
Why both greps? Pasting a multi-word paraphrase into grep -il is the real naive baseline
(phrase almost never occurs verbatim). Per-word term-count is a slightly stronger
hand-rolled baseline — still no IDF or length norm, and it often ranks index.md high.
Recorded on auto/n7-ci against the demo bundle shipped in-tree (re-run the script to refresh).
demo-harness fix 20.07:
tools/bench-recall.mjshad a pre-existing bug (not a regression, not in the BM25 engine) that made a plainnode tools/bench-recall.mjsrun print BM25 hit@1/3 = 0% instead of the numbers below. Root cause: the file's top-levelimport ... from './lib/recall.mjs'transitively (vialib/hygiene.mjs) loadedlib/okf.mjs— whoseROOTexport is a top-levelconstfrozen at first import — before the script ever setOKF_ROOTto the demo bundle, sookf.load()silently read the wrong (real, near-empty) package root instead ofdemo/, leaving BM25's doc pool empty for every query.naiveGrepTerms/naiveGrepPhrasewere unaffected (they walkbundleRootdirectly, nookf.mjsdependency), which is why grep-terms still read 100% while BM25 read 0% — the tell that the bug was in the harness's import order, not the ranker. Confirmed via--synthetic(95% recall@1, N=1000, unaffected — no filesystem/okf.mjspath) and the liveokf-recall.mjs(finds facts correctly) both working throughout. Fixed by deferring the./lib/recall.mjsimport behind a lazygetRecall()(dynamicimport(), same pattern already used for./lib/okf.mjs), resolved only afterOKF_ROOTis set — no change tolib/bm25.mjs,lib/recall.mjs, orlib/hygiene.mjs. The table below reflects the harness's correct output, matching what it was always meant to document.
| Method | hit@1 | hit@3 |
|---|---|---|
| BM25 | 100% (12/12) | 100% (12/12) |
| grep-phrase | 0% (0/12) | 0% (0/12) |
| grep-terms | 100% (12/12) | 100% (12/12) |
finite window high-signal nodes truncate→concepts/context-budgetcosine embedding index keyword when endpoint unavailable→concepts/retrieval-strategylocal-first notes clean graph of backlinks→projects/lumensearchable graph of sources claims and notes→projects/atlassmall fictional research lab sponsors internal tools→entities/acme-labsdesigner frequent collaborator owns the UX→entities/iris-valereads edits runs and verifies directly in the repo→concepts/engine-claude-codereceives chat requests dispatches work reports back→concepts/engine-openclawlonger autonomous coding passes worked to completion→concepts/engine-opencodesoftware engineer power user of LLMs prefers evenings→entities/alex-doesame mind across engines dry wit no filler phrases→concepts/novaprefer fewer denser higher-signal nodes over shallow→concepts/context-budget
- Phrase grep fails completely — none of the paraphrases appear as a contiguous
substring in any demo file. BM25 tokenizes and ranks; that is the whole point of a
keyword ranker over raw
grep. - Term-count grep also hits 100% on this micro-corpus when queries reuse body words.
On ~11 docs, overlapping content words + simple count is enough. Ranking quality still
differs (grep-terms frequently elevates
index/ generic files into top-3; BM25 keeps concept nodes on top via IDF + length norm). - Do not cite these % as general IR quality. A dozen synthetic concepts is not a retrieval benchmark. For real evaluation you want a larger labeled set (and, when embeddings are available, a semantic lane). This file exists so the demo claim «BM25 works out of the box» is backed by a reproducible micro-check, not vibes.
- Micro-corpus. Demo size by design. Percentages inflate.
- Not BrainBench / not semantic. No embeddings path in this bench (
--mode bm25only). - Goldens are author-chosen for the demo story (Nova, Lumen, engines, …), not sampled from production memory.
- No statistical significance. n=12, one run, deterministic.
- Secret / mirror tiers are out of scope here (see
tools/secret-isolation.test.mjs).
- Implementation:
tools/bench-recall.mjs - BM25:
tools/lib/bm25.mjs - Recall API:
tools/lib/recall.mjs(rankByKeywords)