@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.10.23",
3
+ "version": "3.10.25",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+ // benchmark-cross-repo.mjs — cross-repo generalisation proof (ADR-084).
3
+ //
4
+ // Pretrains on a DIFFERENT repo's history (default: ruvnet/agentdb) and runs
5
+ // labelled queries about THAT repo's work. If the retrieval system genuinely
6
+ // generalises (vs overfits to the ruflo corpus it was tuned on), nDCG@3
7
+ // should stay near 0.96.
8
+ //
9
+ // Usage:
10
+ // 1. cd /tmp/agentdb-bench
11
+ // 2. REPO_ROOT=/tmp/agentdb-bench GH_REPO=ruvnet/agentdb \
12
+ // node /path/to/pretrain-from-github.mjs # writes /tmp/agentdb-bench/.claude-flow/neural/
13
+ // 3. cd /tmp/agentdb-bench && node /path/to/benchmark-cross-repo.mjs
14
+ //
15
+ // Or use the wrapper at the bottom that does both steps with --repo=<name>.
16
+
17
+ import { writeFileSync, mkdirSync } from 'node:fs';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { dirname, join, resolve } from 'node:path';
20
+ import { performance } from 'node:perf_hooks';
21
+
22
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
23
+ const CLI_ROOT = resolve(SCRIPT_DIR, '..');
24
+ const RUFLO_ROOT = resolve(SCRIPT_DIR, '../../../..');
25
+ const RUNS_DIR = join(RUFLO_ROOT, 'docs', 'benchmarks', 'runs');
26
+
27
+ // Labelled queries about agentdb's actual history (commits + issues
28
+ // observed via `git log` + `gh issue list` on ruvnet/agentdb).
29
+ // Each query targets a specific piece of work; labels are case-insensitive
30
+ // substring matches against the pattern name.
31
+ const QUERIES_AGENTDB = [
32
+ { q: 'SQL injection fix REINDEX',
33
+ labels: ['sql injection', 'reindex'] },
34
+ { q: 'insecure PRNG random IDs',
35
+ labels: ['insecure prng', 'random ids', 'random id', 'crypto', 'uuid'] },
36
+ { q: 'JSON parse crash hardening',
37
+ labels: ['json.parse', 'json parse', 'crash hardening'] },
38
+ { q: 'protobuf critical CVE patch',
39
+ labels: ['protobufjs', 'protobuf', 'cve'] },
40
+ { q: 'spawnSync vs execSync security',
41
+ labels: ['spawnsync', 'execsync'] },
42
+ { q: 'better-sqlite3 inline schemas',
43
+ labels: ['better-sqlite3', 'inline schema', 'schemas'] },
44
+ { q: 'Claude Code marketplace plugins',
45
+ labels: ['marketplace', 'plugin', 'claude code'] },
46
+ { q: 'README rewrite ruflo style',
47
+ labels: ['readme', 'rewrite'] },
48
+ { q: 'WASM browser fs.readFileSync blocking',
49
+ labels: ['wasm', 'browser', 'fs.readfilesync', 'readfilesync', 'agentdb/wasm'] },
50
+ { q: 'AgentDB missing schema files',
51
+ labels: ['missing schema', 'mising schema', 'schema files', 'not working'] },
52
+ ];
53
+
54
+ // Labelled queries about agentic-flow's actual history.
55
+ const QUERIES_AGENTIC_FLOW = [
56
+ { q: 'CWE-78 shell injection fix',
57
+ labels: ['cwe-78', 'shell injection', 'execsync', 'safe-exec'] },
58
+ { q: 'SSRF hardcoded key NaN panic security',
59
+ labels: ['ssrf', 'hardcoded key', 'nan-panic', 'nan panic'] },
60
+ { q: 'WebSocket QUIC transport fallback',
61
+ labels: ['websocket', 'quic', 'transport'] },
62
+ { q: 'agentdb submodule bump',
63
+ labels: ['agentdb submodule', 'bump agentdb', 'submodule'] },
64
+ { q: 'sql.js prepared statement leak',
65
+ labels: ['sql.js', 'prepared statement', 'leak'] },
66
+ { q: 'WASM ADR-071 integration',
67
+ labels: ['wasm', 'adr-071', 'wasm-integration'] },
68
+ { q: 'patch CVEs transitive overrides',
69
+ labels: ['cve', 'patch', 'transitive', 'overrides'] },
70
+ { q: 'GraphDatabaseAdapter delete API',
71
+ labels: ['graphdatabaseadapter', 'delete api', 'reflexionmemory', 'deleteepisode'] },
72
+ { q: 'release alpha publish version',
73
+ labels: ['release', 'publish', 'alpha', 'version'] },
74
+ { q: 'extract packages to dedicated repo',
75
+ labels: ['extract', 'refactor', 'dedicated repo', 'submodule', 'agentdb'] },
76
+ ];
77
+
78
+ // Auto-pick query set based on GH_REPO env var.
79
+ const QUERY_SETS = {
80
+ 'ruvnet/agentdb': QUERIES_AGENTDB,
81
+ 'ruvnet/agentic-flow': QUERIES_AGENTIC_FLOW,
82
+ };
83
+ const QUERIES = QUERY_SETS[process.env.GH_REPO] || QUERIES_AGENTDB;
84
+ const TOP_K = 5;
85
+
86
+ function isRelevant(name, labels) {
87
+ if (!name || !labels?.length) return false;
88
+ const lower = String(name).toLowerCase();
89
+ return labels.some((s) => lower.includes(s.toLowerCase()));
90
+ }
91
+
92
+ function ndcgAtK(rankedRel, k) {
93
+ const arr = rankedRel.slice(0, k);
94
+ const dcg = arr.reduce((acc, rel, i) => acc + (rel ? 1 / Math.log2(i + 2) : 0), 0);
95
+ const numRelevant = arr.filter(Boolean).length;
96
+ if (numRelevant === 0) return 0;
97
+ let idcg = 0;
98
+ for (let i = 0; i < numRelevant; i++) idcg += 1 / Math.log2(i + 2);
99
+ return idcg > 0 ? dcg / idcg : 0;
100
+ }
101
+
102
+ async function evalConfig(tool, params) {
103
+ let top1 = 0, top3 = 0, ranks = [], ndcg3sum = 0, ndcg5sum = 0, p3sum = 0;
104
+ const t0 = performance.now();
105
+ const perQuery = [];
106
+ for (const { q, labels } of QUERIES) {
107
+ const r = await tool.handler({ action: 'search', query: q, mode: 'hybrid', limit: TOP_K, ...params });
108
+ const matches = (r.results || []).slice(0, TOP_K);
109
+ const rel = matches.map((m) => isRelevant(m?.name, labels));
110
+ if (rel[0]) top1++;
111
+ if (rel.slice(0, 3).some(Boolean)) top3++;
112
+ const firstRank = rel.findIndex(Boolean);
113
+ if (firstRank >= 0) ranks.push(firstRank + 1);
114
+ ndcg3sum += ndcgAtK(rel, 3);
115
+ ndcg5sum += ndcgAtK(rel, 5);
116
+ p3sum += rel.slice(0, 3).filter(Boolean).length / 3;
117
+ perQuery.push({ q, top1: rel[0] || false, topK: matches.map((m, i) => ({ name: m.name?.slice(0, 90), relevant: rel[i], score: (m.score ?? m.similarity)?.toFixed?.(3) })) });
118
+ }
119
+ const elapsed = performance.now() - t0;
120
+ return {
121
+ top1HitRate: top1 / QUERIES.length,
122
+ top3HitRate: top3 / QUERIES.length,
123
+ mrr3: ranks.reduce((s, r) => s + 1 / r, 0) / QUERIES.length,
124
+ precision3: p3sum / QUERIES.length,
125
+ ndcg3: ndcg3sum / QUERIES.length,
126
+ ndcg5: ndcg5sum / QUERIES.length,
127
+ avgLatencyMs: elapsed / QUERIES.length,
128
+ perQuery,
129
+ };
130
+ }
131
+
132
+ async function main() {
133
+ const neural = await import(join(CLI_ROOT, 'dist/src/mcp-tools/neural-tools.js'));
134
+ const intel = await import(join(CLI_ROOT, 'dist/src/memory/intelligence.js'));
135
+ const stats = await intel.getUnifiedLearningStats();
136
+ const storeSize = stats.neuralPatterns.patternCount;
137
+
138
+ if (storeSize === 0) {
139
+ console.error('No patterns in neural store. Run pretrain-from-github.mjs first (with REPO_ROOT + GH_REPO env).');
140
+ process.exit(2);
141
+ }
142
+
143
+ const tool = neural.neuralTools.find((t) => t.name === 'neural_patterns');
144
+ if (!tool) throw new Error('neural_patterns tool not found');
145
+
146
+ console.log(`# Cross-repo generalisation benchmark (ADR-084)`);
147
+ console.log(`Cwd: ${process.cwd()}`);
148
+ console.log(`Store size: ${storeSize} patterns`);
149
+ console.log(`Queries: ${QUERIES.length} (labelled set for ${process.env.GH_REPO || 'default agentdb'})`);
150
+ console.log('');
151
+
152
+ console.log('--- Hybrid path (defaults) ---');
153
+ const hybrid = await evalConfig(tool, { rerank: false });
154
+ console.log(` top-1: ${(hybrid.top1HitRate * 100).toFixed(0)}% top-3: ${(hybrid.top3HitRate * 100).toFixed(0)}% MRR@3: ${hybrid.mrr3.toFixed(3)} P3: ${hybrid.precision3.toFixed(3)} nDCG@3: ${hybrid.ndcg3.toFixed(3)} nDCG@5: ${hybrid.ndcg5.toFixed(3)} ${hybrid.avgLatencyMs.toFixed(0)}ms`);
155
+
156
+ console.log('--- Rerank path (defaults) ---');
157
+ const rerank = await evalConfig(tool, { rerank: true });
158
+ console.log(` top-1: ${(rerank.top1HitRate * 100).toFixed(0)}% top-3: ${(rerank.top3HitRate * 100).toFixed(0)}% MRR@3: ${rerank.mrr3.toFixed(3)} P3: ${rerank.precision3.toFixed(3)} nDCG@3: ${rerank.ndcg3.toFixed(3)} nDCG@5: ${rerank.ndcg5.toFixed(3)} ${rerank.avgLatencyMs.toFixed(0)}ms`);
159
+
160
+ console.log('\n--- Per-query rerank top-3 ---');
161
+ for (const q of rerank.perQuery) {
162
+ console.log(`Q: "${q.q}" (top-1: ${q.top1 ? '✓' : '✗'})`);
163
+ for (const m of q.topK.slice(0, 3)) {
164
+ console.log(` ${m.relevant ? '★' : ' '} [${m.score ?? '—'}] ${m.name}`);
165
+ }
166
+ }
167
+
168
+ const summary = {
169
+ runAt: new Date().toISOString(),
170
+ benchmark: 'cross-repo-generalisation',
171
+ repo: process.env.GH_REPO || 'unknown',
172
+ cwd: process.cwd(),
173
+ storeSize,
174
+ queries: QUERIES.length,
175
+ hybrid: {
176
+ top1HitRate: hybrid.top1HitRate,
177
+ top3HitRate: hybrid.top3HitRate,
178
+ mrr3: hybrid.mrr3,
179
+ precision3: hybrid.precision3,
180
+ ndcg3: hybrid.ndcg3,
181
+ ndcg5: hybrid.ndcg5,
182
+ avgLatencyMs: hybrid.avgLatencyMs,
183
+ },
184
+ rerank: {
185
+ top1HitRate: rerank.top1HitRate,
186
+ top3HitRate: rerank.top3HitRate,
187
+ mrr3: rerank.mrr3,
188
+ precision3: rerank.precision3,
189
+ ndcg3: rerank.ndcg3,
190
+ ndcg5: rerank.ndcg5,
191
+ avgLatencyMs: rerank.avgLatencyMs,
192
+ },
193
+ perQueryRerank: rerank.perQuery,
194
+ };
195
+
196
+ mkdirSync(RUNS_DIR, { recursive: true });
197
+ const stamp = summary.runAt.replace(/[:.]/g, '-');
198
+ const repoSlug = (process.env.GH_REPO || 'unknown').replace('/', '-');
199
+ writeFileSync(join(RUNS_DIR, `cross-repo-${repoSlug}-${stamp}.json`), JSON.stringify(summary, null, 2));
200
+ writeFileSync(join(RUNS_DIR, `cross-repo-${repoSlug}-latest.json`), JSON.stringify(summary, null, 2));
201
+ console.log(`\nWrote ${join(RUNS_DIR, `cross-repo-${repoSlug}-${stamp}.json`)}`);
202
+
203
+ process.exit(0);
204
+ }
205
+
206
+ main().catch((err) => { console.error(err); process.exit(1); });
@@ -32,8 +32,14 @@ import { performance } from 'node:perf_hooks';
32
32
 
33
33
  const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
34
34
  const CLI_ROOT = resolve(SCRIPT_DIR, '..');
35
- const REPO_ROOT = resolve(SCRIPT_DIR, '../../../..');
36
- const RUNS_DIR = join(REPO_ROOT, 'docs', 'benchmarks', 'runs');
35
+ const RUFLO_ROOT = resolve(SCRIPT_DIR, '../../../..');
36
+ const RUNS_DIR = join(RUFLO_ROOT, 'docs', 'benchmarks', 'runs');
37
+
38
+ // ADR-084 cross-repo support — REPO_ROOT and GH_REPO env-overridable so the
39
+ // same script can pretrain on agentdb / agentic-flow / any other repo for
40
+ // generalisation testing. Defaults preserve ruflo behaviour.
41
+ const REPO_ROOT = process.env.REPO_ROOT ? resolve(process.env.REPO_ROOT) : RUFLO_ROOT;
42
+ const GH_REPO = process.env.GH_REPO || 'ruvnet/ruflo';
37
43
 
38
44
  const COMMITS = Number(process.env.COMMITS) || 50;
39
45
  const ISSUES = Number(process.env.ISSUES) || 30;
@@ -156,7 +162,7 @@ function harvestIssues(n) {
156
162
  if (SOURCE === 'git') return [];
157
163
  try {
158
164
  const raw = execSync(
159
- `gh issue list --repo ruvnet/ruflo --state all --limit ${n} --json number,title,body,state,closedAt 2>/dev/null`,
165
+ `gh issue list --repo ${GH_REPO} --state all --limit ${n} --json number,title,body,state,closedAt 2>/dev/null`,
160
166
  { cwd: REPO_ROOT, encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024 },
161
167
  );
162
168
  const items = JSON.parse(raw);
@@ -0,0 +1,292 @@
1
+ #!/usr/bin/env node
2
+ // run-beir-bge.mjs — NFCorpus retrieval using real BGE-base-en-v1.5
3
+ // embeddings (bypasses neural-tools' broken agentic-flow path that hash-fell-
4
+ // back on darwin-arm64 without sharp/libvips).
5
+ //
6
+ // Stores doc embeddings in a single .f32 binary file (3633 * 768 * 4 = ~11MB)
7
+ // for fast bench iteration without re-embedding.
8
+ //
9
+ // Usage:
10
+ // cd /tmp/beir-nfcorpus
11
+ // node /path/to/scripts/run-beir-bge.mjs # full ingest + bench
12
+ // SKIP_INGEST=1 node /path/to/scripts/run-beir-bge.mjs # reuse cached embeds
13
+ // BGE_MODEL=Xenova/bge-small-en-v1.5 node ... # faster, lower-quality
14
+ // BGE_MODEL=Xenova/bge-large-en-v1.5 node ... # slower, higher-quality
15
+ //
16
+ // ADR-086.
17
+
18
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { dirname, join, resolve } from 'node:path';
21
+ import { performance } from 'node:perf_hooks';
22
+
23
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
24
+ const CLI_ROOT = resolve(SCRIPT_DIR, '..');
25
+ const RUFLO_ROOT = resolve(SCRIPT_DIR, '../../../..');
26
+ const RUNS_DIR = join(RUFLO_ROOT, 'docs', 'benchmarks', 'runs');
27
+
28
+ const DATA_DIR = process.env.BEIR_DATA_DIR || '/tmp/beir-nfcorpus/nfcorpus';
29
+ const CACHE_DIR = join('/tmp/beir-nfcorpus', 'bge-cache');
30
+ const BGE_MODEL = process.env.BGE_MODEL || 'Xenova/bge-base-en-v1.5';
31
+ const SKIP_INGEST = process.env.SKIP_INGEST === '1';
32
+ const MAX_QUERIES = Number(process.env.MAX_QUERIES) || 0;
33
+
34
+ // Published BEIR baselines per dataset (nDCG@10).
35
+ // Sources: Thakur et al. 2021 (BEIR paper), BGE paper (BAAI 2024),
36
+ // SPLADE++ paper, papers-with-code BEIR leaderboards.
37
+ const BASELINES_BY_DATASET = {
38
+ nfcorpus: {
39
+ 'BM25 (Lucene)': 0.325,
40
+ 'DocT5query': 0.328,
41
+ 'TAS-B': 0.319,
42
+ 'GenQ': 0.319,
43
+ 'ColBERT': 0.305,
44
+ 'Contriever': 0.328,
45
+ 'GTR-XL': 0.343,
46
+ 'SPLADE++': 0.347,
47
+ 'BGE-large-v1.5 (pub)': 0.380,
48
+ 'SBERT msmarco': 0.272,
49
+ },
50
+ scifact: {
51
+ 'BM25 (Lucene)': 0.679,
52
+ 'DocT5query': 0.675,
53
+ 'TAS-B': 0.643,
54
+ 'GenQ': 0.644,
55
+ 'ColBERT': 0.671,
56
+ 'Contriever': 0.677,
57
+ 'GTR-XL': 0.662,
58
+ 'SPLADE++': 0.704,
59
+ 'BGE-large-v1.5 (pub)': 0.722,
60
+ 'SBERT msmarco': 0.555,
61
+ },
62
+ };
63
+
64
+ // Auto-detect dataset from DATA_DIR path; default to nfcorpus baselines.
65
+ function detectDataset(path) {
66
+ const p = path.toLowerCase();
67
+ for (const ds of Object.keys(BASELINES_BY_DATASET)) {
68
+ if (p.includes(ds)) return ds;
69
+ }
70
+ return 'nfcorpus';
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // nDCG (graded)
75
+ // ---------------------------------------------------------------------------
76
+
77
+ function dcg(rels, k) {
78
+ let s = 0;
79
+ for (let i = 0; i < Math.min(rels.length, k); i++) {
80
+ s += (Math.pow(2, rels[i]) - 1) / Math.log2(i + 2);
81
+ }
82
+ return s;
83
+ }
84
+ function ndcg(retrieved, qrels, k) {
85
+ const rels = retrieved.slice(0, k).map((id) => qrels.get(id) ?? 0);
86
+ const ideal = [...qrels.values()].sort((a, b) => b - a).slice(0, k);
87
+ const idcg = dcg(ideal, k);
88
+ return idcg > 0 ? dcg(rels, k) / idcg : 0;
89
+ }
90
+ function mrr(retrieved, qrels, k) {
91
+ for (let i = 0; i < Math.min(retrieved.length, k); i++) {
92
+ if ((qrels.get(retrieved[i]) ?? 0) > 0) return 1 / (i + 1);
93
+ }
94
+ return 0;
95
+ }
96
+ function recall(retrieved, qrels, k) {
97
+ const tot = [...qrels.values()].filter((v) => v > 0).length;
98
+ if (tot === 0) return 0;
99
+ let h = 0;
100
+ for (let i = 0; i < Math.min(retrieved.length, k); i++) {
101
+ if ((qrels.get(retrieved[i]) ?? 0) > 0) h++;
102
+ }
103
+ return h / tot;
104
+ }
105
+
106
+ function cosine(a, b) {
107
+ let s = 0;
108
+ for (let i = 0; i < a.length; i++) s += a[i] * b[i];
109
+ return s; // already L2-normalised in BGE
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Loaders
114
+ // ---------------------------------------------------------------------------
115
+
116
+ function loadJsonl(path) {
117
+ return readFileSync(path, 'utf-8').split('\n').filter(Boolean).map((l) => JSON.parse(l));
118
+ }
119
+ function loadQrels(path) {
120
+ const qrels = new Map();
121
+ const lines = readFileSync(path, 'utf-8').split('\n');
122
+ for (let i = 1; i < lines.length; i++) {
123
+ if (!lines[i].trim()) continue;
124
+ const [qid, did, score] = lines[i].split('\t');
125
+ if (!qrels.has(qid)) qrels.set(qid, new Map());
126
+ qrels.get(qid).set(did, Number(score));
127
+ }
128
+ return qrels;
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Embedding cache I/O — single .f32 file + .ids file
133
+ // ---------------------------------------------------------------------------
134
+
135
+ function cachePath(suffix) {
136
+ const safe = BGE_MODEL.replace(/\//g, '_');
137
+ return join(CACHE_DIR, `${safe}.${suffix}`);
138
+ }
139
+
140
+ function saveEmbeddings(ids, embeddings, dim) {
141
+ mkdirSync(CACHE_DIR, { recursive: true });
142
+ // ids: one per line
143
+ writeFileSync(cachePath('ids'), ids.join('\n') + '\n');
144
+ // embeddings: concatenated Float32Array
145
+ const buf = new Float32Array(ids.length * dim);
146
+ for (let i = 0; i < ids.length; i++) buf.set(embeddings[i], i * dim);
147
+ writeFileSync(cachePath('f32'), Buffer.from(buf.buffer));
148
+ }
149
+
150
+ function loadEmbeddings(dim) {
151
+ if (!existsSync(cachePath('ids')) || !existsSync(cachePath('f32'))) return null;
152
+ const ids = readFileSync(cachePath('ids'), 'utf-8').split('\n').filter(Boolean);
153
+ const raw = readFileSync(cachePath('f32'));
154
+ const buf = new Float32Array(raw.buffer, raw.byteOffset, raw.length / 4);
155
+ const embeds = [];
156
+ for (let i = 0; i < ids.length; i++) embeds.push(buf.slice(i * dim, (i + 1) * dim));
157
+ return { ids, embeds };
158
+ }
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // Main
162
+ // ---------------------------------------------------------------------------
163
+
164
+ async function main() {
165
+ const dataset = detectDataset(DATA_DIR);
166
+ const BASELINES_NDCG10 = BASELINES_BY_DATASET[dataset];
167
+ console.log(`# BEIR ${dataset} — BGE embedder (ADR-085/086)`);
168
+ console.log(`Model: ${BGE_MODEL}`);
169
+ console.log(`Data: ${DATA_DIR}`);
170
+
171
+ const corpus = loadJsonl(join(DATA_DIR, 'corpus.jsonl'));
172
+ const queries = loadJsonl(join(DATA_DIR, 'queries.jsonl'));
173
+ const qrels = loadQrels(join(DATA_DIR, 'qrels/test.tsv'));
174
+ console.log(`Corpus: ${corpus.length} docs · Queries: ${queries.length} · Test qrels: ${qrels.size}`);
175
+
176
+ const bge = await import(join(CLI_ROOT, 'dist/src/memory/bge-embedder.js'));
177
+ const emb = await bge.getBgeEmbedder(BGE_MODEL);
178
+ if (!emb) {
179
+ console.error('BGE failed to load:', bge.getBgeStatus());
180
+ process.exit(1);
181
+ }
182
+ const dim = emb.dim();
183
+ console.log(`BGE loaded (dim=${dim})`);
184
+
185
+ // Ingest or load cached doc embeddings.
186
+ let docIds, docEmbeds;
187
+ const cached = SKIP_INGEST ? loadEmbeddings(dim) : null;
188
+ if (cached) {
189
+ docIds = cached.ids;
190
+ docEmbeds = cached.embeds;
191
+ console.log(`Loaded ${docIds.length} cached doc embeddings from ${cachePath('f32')}`);
192
+ } else {
193
+ console.log(`\nEmbedding ${corpus.length} docs (this is the slow step, ~20 min on M-series CPU)...`);
194
+ docIds = corpus.map((d) => d._id);
195
+ docEmbeds = new Array(corpus.length);
196
+ const tIngest = performance.now();
197
+ for (let i = 0; i < corpus.length; i++) {
198
+ const d = corpus[i];
199
+ const text = `${d.title || ''}\n${d.text || ''}`.slice(0, 4096);
200
+ docEmbeds[i] = await emb.embed(text);
201
+ if ((i + 1) % 100 === 0) {
202
+ const elapsed = (performance.now() - tIngest) / 1000;
203
+ const rate = (i + 1) / elapsed;
204
+ const eta = (corpus.length - i - 1) / rate;
205
+ console.log(` ${i + 1}/${corpus.length} ${rate.toFixed(1)} docs/s ETA ${eta.toFixed(0)}s`);
206
+ }
207
+ }
208
+ console.log(`Embedded ${corpus.length} docs in ${((performance.now() - tIngest) / 1000).toFixed(0)}s`);
209
+ saveEmbeddings(docIds, docEmbeds, dim);
210
+ console.log(`Cached to ${cachePath('f32')}`);
211
+ }
212
+
213
+ // Run retrieval per query.
214
+ const queriesById = new Map(queries.map((q) => [q._id, q.text]));
215
+ const evalQueryIds = MAX_QUERIES
216
+ ? [...qrels.keys()].slice(0, MAX_QUERIES)
217
+ : [...qrels.keys()];
218
+
219
+ console.log(`\nRunning ${evalQueryIds.length} queries...`);
220
+ let nSum = 0, mSum = 0, r10Sum = 0, r100Sum = 0, n = 0;
221
+ const tQ = performance.now();
222
+ for (const qid of evalQueryIds) {
223
+ const qtext = queriesById.get(qid);
224
+ if (!qtext) continue;
225
+ // BGE recommends prepending "Represent this sentence for searching relevant passages:" to queries
226
+ // but only for v1.5 *en-icl* variants. v1.5 (non-icl) works without it.
227
+ const qEmb = await emb.embed(qtext);
228
+ // Score every doc, sort.
229
+ const scores = new Array(docEmbeds.length);
230
+ for (let i = 0; i < docEmbeds.length; i++) scores[i] = { id: docIds[i], score: cosine(qEmb, docEmbeds[i]) };
231
+ scores.sort((a, b) => b.score - a.score);
232
+ const top100 = scores.slice(0, 100).map((s) => s.id);
233
+
234
+ const qmap = qrels.get(qid);
235
+ nSum += ndcg(top100, qmap, 10);
236
+ mSum += mrr(top100, qmap, 10);
237
+ r10Sum += recall(top100, qmap, 10);
238
+ r100Sum += recall(top100, qmap, 100);
239
+ n++;
240
+ if (n % 50 === 0) {
241
+ const elapsed = (performance.now() - tQ) / 1000;
242
+ console.log(` ${n}/${evalQueryIds.length} in ${elapsed.toFixed(0)}s running nDCG@10=${(nSum / n).toFixed(4)}`);
243
+ }
244
+ }
245
+ const queryMs = performance.now() - tQ;
246
+
247
+ const ndcg10 = nSum / n;
248
+ const mrr10 = mSum / n;
249
+ const recall10 = r10Sum / n;
250
+ const recall100 = r100Sum / n;
251
+
252
+ console.log(`\n=== Results (N=${n}, BGE-base-en-v1.5) ===`);
253
+ console.log(` nDCG@10: ${ndcg10.toFixed(4)}`);
254
+ console.log(` MRR@10: ${mrr10.toFixed(4)}`);
255
+ console.log(` Recall@10: ${recall10.toFixed(4)}`);
256
+ console.log(` Recall@100: ${recall100.toFixed(4)}`);
257
+ console.log(` Avg query latency: ${(queryMs / n).toFixed(0)}ms`);
258
+
259
+ console.log(`\n=== vs published NFCorpus baselines (nDCG@10) ===`);
260
+ const ourLabel = `ruflo + ${BGE_MODEL.replace('Xenova/', '')}`;
261
+ const ranking = [
262
+ ...Object.entries(BASELINES_NDCG10).map(([name, score]) => ({ name, score, ours: false })),
263
+ { name: ourLabel, score: ndcg10, ours: true },
264
+ ].sort((a, b) => b.score - a.score);
265
+ for (const r of ranking) {
266
+ console.log(` ${r.score.toFixed(3)} ${r.name}${r.ours ? ' ← us' : ''}`);
267
+ }
268
+ const ourRank = ranking.findIndex((r) => r.ours) + 1;
269
+ console.log(`\n Our rank: ${ourRank} / ${ranking.length}`);
270
+
271
+ const summary = {
272
+ runAt: new Date().toISOString(),
273
+ benchmark: `beir-${dataset}-bge`,
274
+ dataset,
275
+ model: BGE_MODEL,
276
+ queries: n,
277
+ corpusSize: corpus.length,
278
+ metrics: { ndcg10, mrr10, recall10, recall100, avgQueryLatencyMs: queryMs / n },
279
+ baselines: BASELINES_NDCG10,
280
+ ourRank,
281
+ leaderboardLength: ranking.length,
282
+ };
283
+ mkdirSync(RUNS_DIR, { recursive: true });
284
+ const stamp = summary.runAt.replace(/[:.]/g, '-');
285
+ const safe = BGE_MODEL.replace(/\//g, '_');
286
+ writeFileSync(join(RUNS_DIR, `beir-${dataset}-bge-${safe}-${stamp}.json`), JSON.stringify(summary, null, 2));
287
+ writeFileSync(join(RUNS_DIR, `beir-${dataset}-bge-latest.json`), JSON.stringify(summary, null, 2));
288
+ console.log(`\nWrote ${join(RUNS_DIR, `beir-${dataset}-bge-${safe}-${stamp}.json`)}`);
289
+ process.exit(0);
290
+ }
291
+
292
+ main().catch((err) => { console.error(err); process.exit(1); });