@claude-flow/cli 3.10.27 → 3.10.28

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.28",
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",
@@ -92,15 +92,29 @@ async function main() {
92
92
  const docEmbeds = cached.embeds;
93
93
  console.log(`Loaded ${docIds.length} cached BGE embeddings`);
94
94
 
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})`);
95
+ // BM25 setup — USE_LUCENE_BM25=1 uses Porter+Lucene-stopword BM25 (matches
96
+ // published BEIR baselines); default is hybrid-retrieval's multi-field BM25.
97
+ const USE_LUCENE_BM25 = process.env.USE_LUCENE_BM25 === '1';
98
+ let bm25Score, tokenizeFn, titleDocs, textDocs, titleStats, textStats, luceneDocs, luceneStats;
99
+ if (USE_LUCENE_BM25) {
100
+ const { luceneTokenize, buildLuceneCorpusStats, luceneBM25 } = await import(join(CLI_ROOT, 'dist/src/memory/lucene-bm25.js'));
101
+ tokenizeFn = luceneTokenize;
102
+ luceneDocs = corpus.map((d) => luceneTokenize(`${d.title || ''} ${d.text || ''}`));
103
+ luceneStats = buildLuceneCorpusStats(luceneDocs);
104
+ bm25Score = (qTokens, idx) => luceneBM25(qTokens, luceneDocs[idx], luceneStats);
105
+ console.log(`BM25: Lucene-style (Porter stem + Lucene stopwords + length norm) — ADR-088`);
106
+ } else {
107
+ const { tokenize, buildCorpusStats, multiFieldBM25 } = await import(join(CLI_ROOT, 'dist/src/memory/hybrid-retrieval.js'));
108
+ tokenizeFn = tokenize;
109
+ titleDocs = corpus.map((d) => tokenize(d.title || ''));
110
+ textDocs = corpus.map((d) => tokenize(d.text || ''));
111
+ titleStats = buildCorpusStats(titleDocs);
112
+ textStats = buildCorpusStats(textDocs);
113
+ const SUBJECT_WEIGHT = Number(process.env.SUBJECT_WEIGHT ?? 1.0);
114
+ const BODY_WEIGHT = Number(process.env.BODY_WEIGHT ?? 1.0);
115
+ bm25Score = (qTokens, idx) => multiFieldBM25(qTokens, titleDocs[idx], textDocs[idx], titleStats, textStats, SUBJECT_WEIGHT, BODY_WEIGHT);
116
+ console.log(`BM25: multi-field (title sw=${SUBJECT_WEIGHT}, text bw=${BODY_WEIGHT})`);
117
+ }
104
118
 
105
119
  // Optional cross-encoder reranker.
106
120
  let crossEncoder = null;
@@ -136,12 +150,11 @@ async function main() {
136
150
  for (let i = 0; i < docEmbeds.length; i++) denseScored[i] = { id: docIds[i], score: cosine(qEmb, docEmbeds[i]) };
137
151
  denseScored.sort((a, b) => b.score - a.score);
138
152
 
139
- // §2 — BM25 retrieval (over corpus order, then map to docIds)
140
- const qTokens = tokenize(qtext);
153
+ // §2 — BM25 retrieval (Lucene or multi-field depending on USE_LUCENE_BM25)
154
+ const qTokens = tokenizeFn(qtext);
141
155
  const bm25Scored = new Array(corpus.length);
142
156
  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 };
157
+ bm25Scored[i] = { id: corpusIdxToId[i], score: bm25Score(qTokens, i) };
145
158
  }
146
159
  bm25Scored.sort((a, b) => b.score - a.score);
147
160
 
@@ -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); });