@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/dist/src/memory/lucene-bm25.d.ts +19 -0
- package/dist/src/memory/lucene-bm25.d.ts.map +1 -0
- package/dist/src/memory/lucene-bm25.js +308 -0
- package/dist/src/memory/lucene-bm25.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/scripts/run-beir-hybrid.mjs +26 -13
- package/scripts/run-beir-lucene-bm25.mjs +154 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.10.
|
|
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 —
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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 (
|
|
140
|
-
const qTokens =
|
|
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
|
-
|
|
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); });
|