@claude-flow/cli 3.10.27 → 3.10.29

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.27",
3
+ "version": "3.10.29",
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",
@@ -121,7 +121,8 @@
121
121
  "@ruvector/rvagent-wasm": "^0.1.0",
122
122
  "@ruvector/sona": "^0.1.6",
123
123
  "agentdb": "^3.0.0-alpha.16",
124
- "agentic-flow": "^3.0.0-alpha.1"
124
+ "agentic-flow": "^3.0.0-alpha.1",
125
+ "ruvector": "^0.2.27"
125
126
  },
126
127
  "publishConfig": {
127
128
  "access": "public",
@@ -62,6 +62,18 @@ const BASELINES_BY_DATASET = {
62
62
  'BGE-large-v1.5 (pub)': 0.722,
63
63
  'SBERT msmarco': 0.555,
64
64
  },
65
+ arguana: {
66
+ 'BM25 (Lucene)': 0.397,
67
+ 'DocT5query': 0.349,
68
+ 'TAS-B': 0.429,
69
+ 'GenQ': 0.493,
70
+ 'ColBERT': 0.233,
71
+ 'Contriever': 0.379,
72
+ 'GTR-XL': 0.439,
73
+ 'SPLADE++': 0.521,
74
+ 'BGE-large-v1.5 (pub)': 0.636,
75
+ 'SBERT msmarco': 0.371,
76
+ },
65
77
  };
66
78
 
67
79
  // Auto-detect dataset from DATA_DIR path; default to nfcorpus baselines.
@@ -219,7 +231,9 @@ async function main() {
219
231
  ? [...qrels.keys()].slice(0, MAX_QUERIES)
220
232
  : [...qrels.keys()];
221
233
 
222
- console.log(`\nRunning ${evalQueryIds.length} queries...`);
234
+ // ADR-090: BGE_QUERY_PREFIX=1 enables BAAI's recommended query prefix.
235
+ const USE_QUERY_PREFIX = process.env.BGE_QUERY_PREFIX === '1';
236
+ console.log(`\nRunning ${evalQueryIds.length} queries${USE_QUERY_PREFIX ? ' (with BGE query prefix, ADR-090)' : ''}...`);
223
237
  let nSum = 0, mSum = 0, r10Sum = 0, r100Sum = 0, n = 0;
224
238
  // ADR-086 — save per-query metrics for paired bootstrap significance testing.
225
239
  const perQuery = [];
@@ -227,8 +241,9 @@ async function main() {
227
241
  for (const qid of evalQueryIds) {
228
242
  const qtext = queriesById.get(qid);
229
243
  if (!qtext) continue;
230
- // BGE v1.5 non-icl works without the query prefix.
231
- const qEmb = await emb.embed(qtext);
244
+ // ADR-090: opt-in BGE query prefix per BAAI's docs (+0.009 nDCG@10 on
245
+ // NFCorpus dense-alone). Falls back to plain embed() if not enabled.
246
+ const qEmb = USE_QUERY_PREFIX && emb.embedQuery ? await emb.embedQuery(qtext) : await emb.embed(qtext);
232
247
  const scores = new Array(docEmbeds.length);
233
248
  for (let i = 0; i < docEmbeds.length; i++) scores[i] = { id: docIds[i], score: cosine(qEmb, docEmbeds[i]) };
234
249
  scores.sort((a, b) => b.score - a.score);
@@ -41,6 +41,7 @@ const MAX_QUERIES = Number(process.env.MAX_QUERIES) || 0;
41
41
  const BASELINES_BY_DATASET = {
42
42
  nfcorpus: { 'BM25 (Lucene)': 0.325, 'DocT5query': 0.328, 'TAS-B': 0.319, 'GenQ': 0.319, 'ColBERT': 0.305, 'Contriever': 0.328, 'GTR-XL': 0.343, 'SPLADE++': 0.347, 'BGE-large-v1.5 (pub)': 0.380, 'SBERT msmarco': 0.272 },
43
43
  scifact: { 'BM25 (Lucene)': 0.679, 'DocT5query': 0.675, 'TAS-B': 0.643, 'GenQ': 0.644, 'ColBERT': 0.671, 'Contriever': 0.677, 'GTR-XL': 0.662, 'SPLADE++': 0.704, 'BGE-large-v1.5 (pub)': 0.722, 'SBERT msmarco': 0.555 },
44
+ arguana: { 'BM25 (Lucene)': 0.397, 'DocT5query': 0.349, 'TAS-B': 0.429, 'GenQ': 0.493, 'ColBERT': 0.233, 'Contriever': 0.379, 'GTR-XL': 0.439, 'SPLADE++': 0.521, 'BGE-large-v1.5 (pub)': 0.636, 'SBERT msmarco': 0.371 },
44
45
  };
45
46
  function detectDataset(path) {
46
47
  const p = path.toLowerCase();
@@ -92,15 +93,29 @@ async function main() {
92
93
  const docEmbeds = cached.embeds;
93
94
  console.log(`Loaded ${docIds.length} cached BGE embeddings`);
94
95
 
95
- // BM25 setup — multi-field over (title, text) per doc.
96
- const { tokenize, buildCorpusStats, multiFieldBM25 } = await import(join(CLI_ROOT, 'dist/src/memory/hybrid-retrieval.js'));
97
- const titleDocs = corpus.map((d) => tokenize(d.title || ''));
98
- const textDocs = corpus.map((d) => tokenize(d.text || ''));
99
- const titleStats = buildCorpusStats(titleDocs);
100
- const textStats = buildCorpusStats(textDocs);
101
- const SUBJECT_WEIGHT = Number(process.env.SUBJECT_WEIGHT ?? 1.0); // titles in BEIR are short, neutral weight is fine
102
- const BODY_WEIGHT = Number(process.env.BODY_WEIGHT ?? 1.0);
103
- console.log(`BM25: multi-field (title sw=${SUBJECT_WEIGHT}, text bw=${BODY_WEIGHT})`);
96
+ // BM25 setup — USE_LUCENE_BM25=1 uses Porter+Lucene-stopword BM25 (matches
97
+ // published BEIR baselines); default is hybrid-retrieval's multi-field BM25.
98
+ const USE_LUCENE_BM25 = process.env.USE_LUCENE_BM25 === '1';
99
+ let bm25Score, tokenizeFn, titleDocs, textDocs, titleStats, textStats, luceneDocs, luceneStats;
100
+ if (USE_LUCENE_BM25) {
101
+ const { luceneTokenize, buildLuceneCorpusStats, luceneBM25 } = await import(join(CLI_ROOT, 'dist/src/memory/lucene-bm25.js'));
102
+ tokenizeFn = luceneTokenize;
103
+ luceneDocs = corpus.map((d) => luceneTokenize(`${d.title || ''} ${d.text || ''}`));
104
+ luceneStats = buildLuceneCorpusStats(luceneDocs);
105
+ bm25Score = (qTokens, idx) => luceneBM25(qTokens, luceneDocs[idx], luceneStats);
106
+ console.log(`BM25: Lucene-style (Porter stem + Lucene stopwords + length norm) — ADR-088`);
107
+ } else {
108
+ const { tokenize, buildCorpusStats, multiFieldBM25 } = await import(join(CLI_ROOT, 'dist/src/memory/hybrid-retrieval.js'));
109
+ tokenizeFn = tokenize;
110
+ titleDocs = corpus.map((d) => tokenize(d.title || ''));
111
+ textDocs = corpus.map((d) => tokenize(d.text || ''));
112
+ titleStats = buildCorpusStats(titleDocs);
113
+ textStats = buildCorpusStats(textDocs);
114
+ const SUBJECT_WEIGHT = Number(process.env.SUBJECT_WEIGHT ?? 1.0);
115
+ const BODY_WEIGHT = Number(process.env.BODY_WEIGHT ?? 1.0);
116
+ bm25Score = (qTokens, idx) => multiFieldBM25(qTokens, titleDocs[idx], textDocs[idx], titleStats, textStats, SUBJECT_WEIGHT, BODY_WEIGHT);
117
+ console.log(`BM25: multi-field (title sw=${SUBJECT_WEIGHT}, text bw=${BODY_WEIGHT})`);
118
+ }
104
119
 
105
120
  // Optional cross-encoder reranker.
106
121
  let crossEncoder = null;
@@ -130,18 +145,19 @@ async function main() {
130
145
  const qtext = queriesById.get(qid);
131
146
  if (!qtext) continue;
132
147
 
133
- // §1 — dense BGE retrieval
134
- const qEmb = await emb.embed(qtext);
148
+ // §1 — dense BGE retrieval (ADR-090: opt-in query prefix when BGE_QUERY_PREFIX=1)
149
+ const qEmb = (process.env.BGE_QUERY_PREFIX === '1' && emb.embedQuery)
150
+ ? await emb.embedQuery(qtext)
151
+ : await emb.embed(qtext);
135
152
  const denseScored = new Array(docEmbeds.length);
136
153
  for (let i = 0; i < docEmbeds.length; i++) denseScored[i] = { id: docIds[i], score: cosine(qEmb, docEmbeds[i]) };
137
154
  denseScored.sort((a, b) => b.score - a.score);
138
155
 
139
- // §2 — BM25 retrieval (over corpus order, then map to docIds)
140
- const qTokens = tokenize(qtext);
156
+ // §2 — BM25 retrieval (Lucene or multi-field depending on USE_LUCENE_BM25)
157
+ const qTokens = tokenizeFn(qtext);
141
158
  const bm25Scored = new Array(corpus.length);
142
159
  for (let i = 0; i < corpus.length; i++) {
143
- const s = multiFieldBM25(qTokens, titleDocs[i], textDocs[i], titleStats, textStats, SUBJECT_WEIGHT, BODY_WEIGHT);
144
- bm25Scored[i] = { id: corpusIdxToId[i], score: s };
160
+ bm25Scored[i] = { id: corpusIdxToId[i], score: bm25Score(qTokens, i) };
145
161
  }
146
162
  bm25Scored.sort((a, b) => b.score - a.score);
147
163
 
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ // run-beir-lucene-bm25.mjs — test our Lucene-style BM25 against the
3
+ // published BEIR BM25 baselines. Also runs RRF with Lucene-BM25 + dense
4
+ // to see if the stronger BM25 closes the asymmetric-strength gap that
5
+ // broke RRF in ADR-087.
6
+ //
7
+ // Usage:
8
+ // cd /tmp/beir-nfcorpus
9
+ // node /path/to/scripts/run-beir-lucene-bm25.mjs
10
+ // BEIR_DATA_DIR=/tmp/beir-scifact/scifact node /path/to/scripts/run-beir-lucene-bm25.mjs
11
+
12
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { dirname, join, resolve } from 'node:path';
15
+ import { performance } from 'node:perf_hooks';
16
+
17
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
18
+ const CLI_ROOT = resolve(SCRIPT_DIR, '..');
19
+ const RUFLO_ROOT = resolve(SCRIPT_DIR, '../../../..');
20
+ const RUNS_DIR = join(RUFLO_ROOT, 'docs', 'benchmarks', 'runs');
21
+
22
+ const DATA_DIR = process.env.BEIR_DATA_DIR || '/tmp/beir-nfcorpus/nfcorpus';
23
+ const BGE_MODEL = 'Xenova/bge-base-en-v1.5';
24
+ const CACHE_DIR = join(dirname(DATA_DIR), 'bge-cache');
25
+
26
+ const BASELINES = {
27
+ nfcorpus: { BM25: 0.325, 'BGE-large-pub': 0.380, 'SPLADE++': 0.347 },
28
+ scifact: { BM25: 0.679, 'BGE-large-pub': 0.722, 'SPLADE++': 0.704 },
29
+ };
30
+ function detect(p) { const x = p.toLowerCase(); return Object.keys(BASELINES).find(d => x.includes(d)) || 'nfcorpus'; }
31
+
32
+ function dcg(rels, k) { let s = 0; for (let i = 0; i < Math.min(rels.length, k); i++) s += (Math.pow(2, rels[i]) - 1) / Math.log2(i + 2); return s; }
33
+ function ndcg(retrieved, qrels, k) { const rels = retrieved.slice(0, k).map((id) => qrels.get(id) ?? 0); const ideal = [...qrels.values()].sort((a, b) => b - a).slice(0, k); const idcg = dcg(ideal, k); return idcg > 0 ? dcg(rels, k) / idcg : 0; }
34
+ function recall(retrieved, qrels, k) { const tot = [...qrels.values()].filter((v) => v > 0).length; if (tot === 0) return 0; let h = 0; for (let i = 0; i < Math.min(retrieved.length, k); i++) if ((qrels.get(retrieved[i]) ?? 0) > 0) h++; return h / tot; }
35
+ function cosine(a, b) { let s = 0; for (let i = 0; i < a.length; i++) s += a[i] * b[i]; return s; }
36
+
37
+ function loadJsonl(p) { return readFileSync(p, 'utf-8').split('\n').filter(Boolean).map((l) => JSON.parse(l)); }
38
+ function loadQrels(p) { const q = new Map(); const lines = readFileSync(p, 'utf-8').split('\n'); for (let i = 1; i < lines.length; i++) { if (!lines[i].trim()) continue; const [qid, did, s] = lines[i].split('\t'); if (!q.has(qid)) q.set(qid, new Map()); q.get(qid).set(did, Number(s)); } return q; }
39
+
40
+ function cachePath(s) { const safe = BGE_MODEL.replace(/\//g, '_'); return join(CACHE_DIR, `${safe}.${s}`); }
41
+ function loadEmbeddings(dim) {
42
+ if (!existsSync(cachePath('ids')) || !existsSync(cachePath('f32'))) return null;
43
+ const ids = readFileSync(cachePath('ids'), 'utf-8').split('\n').filter(Boolean);
44
+ const raw = readFileSync(cachePath('f32'));
45
+ const buf = new Float32Array(raw.buffer, raw.byteOffset, raw.length / 4);
46
+ return { ids, embeds: ids.map((_, i) => buf.slice(i * dim, (i + 1) * dim)) };
47
+ }
48
+
49
+ function rrfFuse(rankings, k = 60, weights) {
50
+ const scores = new Map();
51
+ rankings.forEach((ranking, sIdx) => {
52
+ const w = weights?.[sIdx] ?? 1;
53
+ ranking.forEach((doc, rankIdx) => {
54
+ const rank = rankIdx + 1;
55
+ scores.set(doc.id, (scores.get(doc.id) ?? 0) + w / (k + rank));
56
+ });
57
+ });
58
+ return [...scores.entries()].map(([id, score]) => ({ id, score })).sort((a, b) => b.score - a.score);
59
+ }
60
+
61
+ async function main() {
62
+ const dataset = detect(DATA_DIR);
63
+ console.log(`# Lucene-style BM25 + RRF on BEIR ${dataset} (ADR-088)`);
64
+ console.log(`Data: ${DATA_DIR}\n`);
65
+
66
+ const corpus = loadJsonl(join(DATA_DIR, 'corpus.jsonl'));
67
+ const queries = loadJsonl(join(DATA_DIR, 'queries.jsonl'));
68
+ const qrels = loadQrels(join(DATA_DIR, 'qrels/test.tsv'));
69
+ console.log(`Corpus: ${corpus.length} · test qrels: ${qrels.size}`);
70
+
71
+ // Lucene BM25
72
+ const { luceneTokenize, buildLuceneCorpusStats, luceneBM25 } = await import(join(CLI_ROOT, 'dist/src/memory/lucene-bm25.js'));
73
+ console.log('\nTokenising corpus (Lucene-style: Porter stem + Lucene stopwords + length norm)...');
74
+ const tIngest = performance.now();
75
+ const docs = corpus.map((d) => luceneTokenize(`${d.title || ''} ${d.text || ''}`));
76
+ const stats = buildLuceneCorpusStats(docs);
77
+ console.log(`Tokenized + stats built in ${((performance.now() - tIngest) / 1000).toFixed(1)}s. avgDocLen=${stats.avgDocLen.toFixed(0)}, vocab=${stats.idf.size}`);
78
+
79
+ // BGE dense (from cache)
80
+ const bge = await import(join(CLI_ROOT, 'dist/src/memory/bge-embedder.js'));
81
+ const emb = await bge.getBgeEmbedder(BGE_MODEL);
82
+ if (!emb) { console.error('BGE failed'); process.exit(1); }
83
+ const cached = loadEmbeddings(emb.dim());
84
+ if (!cached) { console.error(`No cached embeddings. Run run-beir-bge.mjs first.`); process.exit(2); }
85
+ const { ids: docIds, embeds: docEmbeds } = cached;
86
+ const corpusIdxToId = corpus.map((d) => d._id);
87
+
88
+ // Run all configs
89
+ const queriesById = new Map(queries.map((q) => [q._id, q.text]));
90
+ const evalQids = [...qrels.keys()];
91
+ console.log(`\nRunning ${evalQids.length} queries through 4 configs (dense, Lucene-BM25, RRF, RRF-tuned)...\n`);
92
+
93
+ let n = 0;
94
+ const results = {
95
+ dense: { n: 0, ndcg: 0, r10: 0, r100: 0 },
96
+ bm25: { n: 0, ndcg: 0, r10: 0, r100: 0 },
97
+ rrf60: { n: 0, ndcg: 0, r10: 0, r100: 0 },
98
+ rrf30: { n: 0, ndcg: 0, r10: 0, r100: 0 },
99
+ };
100
+ const tQ = performance.now();
101
+ for (const qid of evalQids) {
102
+ const qtext = queriesById.get(qid);
103
+ if (!qtext) continue;
104
+ n++;
105
+
106
+ // Dense
107
+ const qEmb = await emb.embed(qtext);
108
+ const dense = docIds.map((id, i) => ({ id, score: cosine(qEmb, docEmbeds[i]) })).sort((a, b) => b.score - a.score).slice(0, 1000);
109
+
110
+ // BM25
111
+ const qTokens = luceneTokenize(qtext);
112
+ const bm25 = corpus.map((d, i) => ({ id: corpusIdxToId[i], score: luceneBM25(qTokens, docs[i], stats) })).filter((d) => d.score > 0).sort((a, b) => b.score - a.score).slice(0, 1000);
113
+
114
+ // RRF k=60 and k=30
115
+ const rrf60 = rrfFuse([dense, bm25], 60);
116
+ const rrf30 = rrfFuse([dense, bm25], 30);
117
+
118
+ const qmap = qrels.get(qid);
119
+ for (const [key, top] of [['dense', dense], ['bm25', bm25], ['rrf60', rrf60], ['rrf30', rrf30]]) {
120
+ const top100 = top.slice(0, 100).map((s) => s.id);
121
+ results[key].n++;
122
+ results[key].ndcg += ndcg(top100, qmap, 10);
123
+ results[key].r10 += recall(top100, qmap, 10);
124
+ results[key].r100 += recall(top100, qmap, 100);
125
+ }
126
+ if (n % 50 === 0) console.log(` ${n}/${evalQids.length} dense=${(results.dense.ndcg / results.dense.n).toFixed(3)} bm25=${(results.bm25.ndcg / results.bm25.n).toFixed(3)} rrf60=${(results.rrf60.ndcg / results.rrf60.n).toFixed(3)}`);
127
+ }
128
+ const elapsed = (performance.now() - tQ) / 1000;
129
+
130
+ console.log(`\nDone in ${elapsed.toFixed(0)}s.\n`);
131
+ console.log('Config | nDCG@10 R@10 R@100 vs published-BM25');
132
+ console.log('---------------|---------------------------------');
133
+ for (const [key, r] of Object.entries(results)) {
134
+ const score = r.ndcg / r.n;
135
+ const delta = score - BASELINES[dataset].BM25;
136
+ console.log(`${key.padEnd(14)} | ${score.toFixed(4)} ${(r.r10 / r.n).toFixed(3)} ${(r.r100 / r.n).toFixed(3)} ${(delta >= 0 ? '+' : '') + delta.toFixed(4)}`);
137
+ }
138
+
139
+ const summary = {
140
+ runAt: new Date().toISOString(),
141
+ benchmark: `beir-${dataset}-lucene-bm25`,
142
+ dataset, model: BGE_MODEL, queries: n,
143
+ results: Object.fromEntries(Object.entries(results).map(([k, v]) => [k, { ndcg10: v.ndcg / v.n, recall10: v.r10 / v.n, recall100: v.r100 / v.n }])),
144
+ baselines: BASELINES[dataset],
145
+ };
146
+ mkdirSync(RUNS_DIR, { recursive: true });
147
+ const stamp = summary.runAt.replace(/[:.]/g, '-');
148
+ writeFileSync(join(RUNS_DIR, `beir-${dataset}-lucene-bm25-${stamp}.json`), JSON.stringify(summary, null, 2));
149
+ writeFileSync(join(RUNS_DIR, `beir-${dataset}-lucene-bm25-latest.json`), JSON.stringify(summary, null, 2));
150
+ console.log(`\nWrote ${join(RUNS_DIR, `beir-${dataset}-lucene-bm25-${stamp}.json`)}`);
151
+ process.exit(0);
152
+ }
153
+
154
+ main().catch((err) => { console.error(err); process.exit(1); });