@claude-flow/cli 3.10.23 → 3.10.25

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.
@@ -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); });