@claude-flow/cli 3.10.24 → 3.10.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/memory/bge-embedder.d.ts +19 -0
- package/dist/src/memory/bge-embedder.d.ts.map +1 -0
- package/dist/src/memory/bge-embedder.js +107 -0
- package/dist/src/memory/bge-embedder.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/scripts/beir-bootstrap-significance.mjs +162 -0
- package/scripts/run-beir-bge.mjs +294 -0
- package/scripts/run-beir-nfcorpus.mjs +231 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// run-beir-nfcorpus.mjs — public benchmark harness for BEIR NFCorpus.
|
|
3
|
+
//
|
|
4
|
+
// Why NFCorpus: smallest BEIR dataset (3.6k docs, 323 test queries) with a
|
|
5
|
+
// well-published BM25 baseline and dense-retriever ceilings — runs end-to-end
|
|
6
|
+
// in <15 min on this hardware.
|
|
7
|
+
//
|
|
8
|
+
// Setup:
|
|
9
|
+
// mkdir -p /tmp/beir-nfcorpus && cd /tmp/beir-nfcorpus
|
|
10
|
+
// curl -sL -o nfcorpus.zip 'https://public.ukp.informatik.tu-darmstadt.de/thakur/BEIR/datasets/nfcorpus.zip'
|
|
11
|
+
// unzip -q nfcorpus.zip
|
|
12
|
+
//
|
|
13
|
+
// Run:
|
|
14
|
+
// cd /tmp/beir-nfcorpus && rm -rf .claude-flow
|
|
15
|
+
// node /path/to/scripts/run-beir-nfcorpus.mjs
|
|
16
|
+
//
|
|
17
|
+
// Reports:
|
|
18
|
+
// - nDCG@10 (graded, BEIR standard)
|
|
19
|
+
// - MRR@10
|
|
20
|
+
// - Recall@10, Recall@100
|
|
21
|
+
// - Published comparisons (BM25, ColBERT, SPLADE, etc.)
|
|
22
|
+
|
|
23
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
import { dirname, join, resolve } from 'node:path';
|
|
26
|
+
import { performance } from 'node:perf_hooks';
|
|
27
|
+
|
|
28
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const CLI_ROOT = resolve(SCRIPT_DIR, '..');
|
|
30
|
+
const RUFLO_ROOT = resolve(SCRIPT_DIR, '../../../..');
|
|
31
|
+
const RUNS_DIR = join(RUFLO_ROOT, 'docs', 'benchmarks', 'runs');
|
|
32
|
+
|
|
33
|
+
const DATA_DIR = process.env.BEIR_DATA_DIR || '/tmp/beir-nfcorpus/nfcorpus';
|
|
34
|
+
const TOP_K = Number(process.env.TOP_K) || 100; // top-100 for recall@100
|
|
35
|
+
const NDCG_K = 10;
|
|
36
|
+
const MAX_QUERIES = Number(process.env.MAX_QUERIES) || 0; // 0 = all
|
|
37
|
+
const SKIP_INGEST = process.env.SKIP_INGEST === '1'; // reuse existing store
|
|
38
|
+
|
|
39
|
+
// Published BEIR NFCorpus baselines (from Thakur et al. 2021 + leaderboard tracker)
|
|
40
|
+
const BASELINES_NDCG10 = {
|
|
41
|
+
'BM25': 0.325,
|
|
42
|
+
'DocT5query': 0.328,
|
|
43
|
+
'SBERT (msmarco)': 0.272, // dense bi-encoder on MS MARCO
|
|
44
|
+
'TAS-B': 0.319,
|
|
45
|
+
'GenQ': 0.319,
|
|
46
|
+
'ColBERT': 0.305,
|
|
47
|
+
'SPLADE++': 0.347, // top dense at time of paper
|
|
48
|
+
'Contriever': 0.328,
|
|
49
|
+
'GTR-XL': 0.343,
|
|
50
|
+
'BGE-large-v1.5': 0.380, // current ~top-of-class
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// nDCG with graded relevance (BEIR standard)
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
function dcg(rels, k) {
|
|
58
|
+
let s = 0;
|
|
59
|
+
for (let i = 0; i < Math.min(rels.length, k); i++) {
|
|
60
|
+
s += (Math.pow(2, rels[i]) - 1) / Math.log2(i + 2);
|
|
61
|
+
}
|
|
62
|
+
return s;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ndcg(retrievedDocIds, qrelsMap, k) {
|
|
66
|
+
const rels = retrievedDocIds.slice(0, k).map((id) => qrelsMap.get(id) ?? 0);
|
|
67
|
+
const ideal = [...qrelsMap.values()].sort((a, b) => b - a).slice(0, k);
|
|
68
|
+
const idcg = dcg(ideal, k);
|
|
69
|
+
return idcg > 0 ? dcg(rels, k) / idcg : 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function mrr(retrievedDocIds, qrelsMap, k) {
|
|
73
|
+
for (let i = 0; i < Math.min(retrievedDocIds.length, k); i++) {
|
|
74
|
+
if ((qrelsMap.get(retrievedDocIds[i]) ?? 0) > 0) return 1 / (i + 1);
|
|
75
|
+
}
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function recall(retrievedDocIds, qrelsMap, k) {
|
|
80
|
+
const totalRelevant = [...qrelsMap.values()].filter((v) => v > 0).length;
|
|
81
|
+
if (totalRelevant === 0) return 0;
|
|
82
|
+
let hit = 0;
|
|
83
|
+
for (let i = 0; i < Math.min(retrievedDocIds.length, k); i++) {
|
|
84
|
+
if ((qrelsMap.get(retrievedDocIds[i]) ?? 0) > 0) hit++;
|
|
85
|
+
}
|
|
86
|
+
return hit / totalRelevant;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Loaders
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
function loadJsonl(path) {
|
|
94
|
+
const items = [];
|
|
95
|
+
for (const line of readFileSync(path, 'utf-8').split('\n')) {
|
|
96
|
+
if (!line.trim()) continue;
|
|
97
|
+
items.push(JSON.parse(line));
|
|
98
|
+
}
|
|
99
|
+
return items;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function loadQrels(path) {
|
|
103
|
+
// Map<queryId, Map<docId, grade>>
|
|
104
|
+
const qrels = new Map();
|
|
105
|
+
const lines = readFileSync(path, 'utf-8').split('\n');
|
|
106
|
+
for (let i = 1; i < lines.length; i++) {
|
|
107
|
+
if (!lines[i].trim()) continue;
|
|
108
|
+
const [qid, did, score] = lines[i].split('\t');
|
|
109
|
+
if (!qrels.has(qid)) qrels.set(qid, new Map());
|
|
110
|
+
qrels.get(qid).set(did, Number(score));
|
|
111
|
+
}
|
|
112
|
+
return qrels;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Main
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
async function main() {
|
|
120
|
+
console.log(`# BEIR NFCorpus benchmark (ADR-085)`);
|
|
121
|
+
console.log(`Data: ${DATA_DIR}`);
|
|
122
|
+
console.log(`Cwd: ${process.cwd()}`);
|
|
123
|
+
|
|
124
|
+
const corpus = loadJsonl(join(DATA_DIR, 'corpus.jsonl'));
|
|
125
|
+
const queries = loadJsonl(join(DATA_DIR, 'queries.jsonl'));
|
|
126
|
+
const qrels = loadQrels(join(DATA_DIR, 'qrels/test.tsv'));
|
|
127
|
+
|
|
128
|
+
console.log(`Corpus: ${corpus.length} docs, ${queries.length} queries (total), ${qrels.size} eval queries (test split)`);
|
|
129
|
+
|
|
130
|
+
const neural = await import(join(CLI_ROOT, 'dist/src/mcp-tools/neural-tools.js'));
|
|
131
|
+
const tool = neural.neuralTools.find((t) => t.name === 'neural_patterns');
|
|
132
|
+
|
|
133
|
+
// §1 — Ingest corpus (id-stable pattern name = doc _id; content = title + text).
|
|
134
|
+
// Skip if SKIP_INGEST=1 and store already populated.
|
|
135
|
+
if (!SKIP_INGEST) {
|
|
136
|
+
console.log(`\nIngesting ${corpus.length} docs...`);
|
|
137
|
+
const t0 = performance.now();
|
|
138
|
+
// Batch in chunks to avoid memory pressure.
|
|
139
|
+
const CHUNK = 200;
|
|
140
|
+
for (let i = 0; i < corpus.length; i += CHUNK) {
|
|
141
|
+
const batch = corpus.slice(i, i + CHUNK);
|
|
142
|
+
await neural.storeNeuralPatterns(batch.map((d) => ({
|
|
143
|
+
name: d._id, // use _id as the stable identifier
|
|
144
|
+
type: 'beir-nfcorpus',
|
|
145
|
+
content: `${d.title || ''}\n${d.text || ''}`.slice(0, 4096),
|
|
146
|
+
})));
|
|
147
|
+
if ((i + CHUNK) % 1000 === 0 || i + CHUNK >= corpus.length) {
|
|
148
|
+
const pct = Math.min(100, ((i + CHUNK) / corpus.length * 100)).toFixed(0);
|
|
149
|
+
const elapsed = (performance.now() - t0) / 1000;
|
|
150
|
+
console.log(` ${Math.min(i + CHUNK, corpus.length)}/${corpus.length} (${pct}%) in ${elapsed.toFixed(0)}s`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
console.log(`Ingest done in ${((performance.now() - t0) / 1000).toFixed(0)}s`);
|
|
154
|
+
} else {
|
|
155
|
+
console.log('SKIP_INGEST=1 → reusing existing store');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// §2 — Build query lookup (eval queries only).
|
|
159
|
+
const queriesById = new Map(queries.map((q) => [q._id, q.text]));
|
|
160
|
+
const evalQueryIds = MAX_QUERIES
|
|
161
|
+
? [...qrels.keys()].slice(0, MAX_QUERIES)
|
|
162
|
+
: [...qrels.keys()];
|
|
163
|
+
|
|
164
|
+
// §3 — Run retrieval for each query, compute metrics.
|
|
165
|
+
console.log(`\nRunning ${evalQueryIds.length} queries...`);
|
|
166
|
+
let ndcgSum = 0, mrrSum = 0, recall10Sum = 0, recall100Sum = 0;
|
|
167
|
+
const tQ = performance.now();
|
|
168
|
+
let queryIdx = 0;
|
|
169
|
+
for (const qid of evalQueryIds) {
|
|
170
|
+
const qtext = queriesById.get(qid);
|
|
171
|
+
if (!qtext) { queryIdx++; continue; }
|
|
172
|
+
const r = await tool.handler({ action: 'search', query: qtext, mode: 'hybrid', limit: TOP_K, rerank: false });
|
|
173
|
+
const retrievedIds = (r.results || []).map((m) => m.name); // we set name = _id
|
|
174
|
+
const qrelsMap = qrels.get(qid) || new Map();
|
|
175
|
+
ndcgSum += ndcg(retrievedIds, qrelsMap, NDCG_K);
|
|
176
|
+
mrrSum += mrr(retrievedIds, qrelsMap, NDCG_K);
|
|
177
|
+
recall10Sum += recall(retrievedIds, qrelsMap, 10);
|
|
178
|
+
recall100Sum += recall(retrievedIds, qrelsMap, 100);
|
|
179
|
+
queryIdx++;
|
|
180
|
+
if (queryIdx % 50 === 0) {
|
|
181
|
+
const elapsed = (performance.now() - tQ) / 1000;
|
|
182
|
+
console.log(` ${queryIdx}/${evalQueryIds.length} in ${elapsed.toFixed(0)}s — running nDCG@10=${(ndcgSum / queryIdx).toFixed(4)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const queryMs = performance.now() - tQ;
|
|
186
|
+
const N = evalQueryIds.length;
|
|
187
|
+
const ndcg10 = ndcgSum / N;
|
|
188
|
+
const mrr10 = mrrSum / N;
|
|
189
|
+
const recall10 = recall10Sum / N;
|
|
190
|
+
const recall100 = recall100Sum / N;
|
|
191
|
+
|
|
192
|
+
console.log(`\n=== Results (N=${N} queries) ===`);
|
|
193
|
+
console.log(` nDCG@10: ${ndcg10.toFixed(4)}`);
|
|
194
|
+
console.log(` MRR@10: ${mrr10.toFixed(4)}`);
|
|
195
|
+
console.log(` Recall@10: ${recall10.toFixed(4)}`);
|
|
196
|
+
console.log(` Recall@100: ${recall100.toFixed(4)}`);
|
|
197
|
+
console.log(` Avg query latency: ${(queryMs / N).toFixed(0)}ms`);
|
|
198
|
+
|
|
199
|
+
console.log(`\n=== vs published NFCorpus nDCG@10 baselines ===`);
|
|
200
|
+
const ourEntry = { name: 'ruflo hybrid (3.10.25)', score: ndcg10, ours: true };
|
|
201
|
+
const ranking = [
|
|
202
|
+
...Object.entries(BASELINES_NDCG10).map(([name, score]) => ({ name, score, ours: false })),
|
|
203
|
+
ourEntry,
|
|
204
|
+
].sort((a, b) => b.score - a.score);
|
|
205
|
+
for (const r of ranking) {
|
|
206
|
+
const marker = r.ours ? ' ← us' : '';
|
|
207
|
+
console.log(` ${r.score.toFixed(3)} ${r.name}${marker}`);
|
|
208
|
+
}
|
|
209
|
+
const ourRank = ranking.findIndex((r) => r.ours) + 1;
|
|
210
|
+
console.log(`\n Our rank: ${ourRank} / ${ranking.length}`);
|
|
211
|
+
|
|
212
|
+
const summary = {
|
|
213
|
+
runAt: new Date().toISOString(),
|
|
214
|
+
benchmark: 'beir-nfcorpus',
|
|
215
|
+
queries: N,
|
|
216
|
+
corpusSize: corpus.length,
|
|
217
|
+
metrics: { ndcg10, mrr10, recall10, recall100, avgQueryLatencyMs: queryMs / N },
|
|
218
|
+
baselines: BASELINES_NDCG10,
|
|
219
|
+
ourRank,
|
|
220
|
+
leaderboardLength: ranking.length,
|
|
221
|
+
};
|
|
222
|
+
mkdirSync(RUNS_DIR, { recursive: true });
|
|
223
|
+
const stamp = summary.runAt.replace(/[:.]/g, '-');
|
|
224
|
+
writeFileSync(join(RUNS_DIR, `beir-nfcorpus-${stamp}.json`), JSON.stringify(summary, null, 2));
|
|
225
|
+
writeFileSync(join(RUNS_DIR, 'beir-nfcorpus-latest.json'), JSON.stringify(summary, null, 2));
|
|
226
|
+
console.log(`\nWrote ${join(RUNS_DIR, `beir-nfcorpus-${stamp}.json`)}`);
|
|
227
|
+
|
|
228
|
+
process.exit(0);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
main().catch((err) => { console.error(err); process.exit(1); });
|