@cachly-dev/mcp-server 0.10.89 → 0.10.91
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/README.md +1 -0
- package/dist/src/bench/cachly-bench.d.ts +46 -0
- package/dist/src/bench/cachly-bench.js +231 -0
- package/dist/src/bench/cachly-bench.js.map +1 -0
- package/dist/src/bench/external-corpus.d.ts +44 -0
- package/dist/src/bench/external-corpus.js +135 -0
- package/dist/src/bench/external-corpus.js.map +1 -0
- package/dist/src/bench/fixtures.d.ts +37 -0
- package/dist/src/bench/fixtures.js +225 -0
- package/dist/src/bench/fixtures.js.map +1 -0
- package/dist/src/handlers/brain.js +33 -2
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/index.js +80 -1
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -226,6 +226,7 @@ causal_trace(problem="auth breaks after restart")
|
|
|
226
226
|
```bash
|
|
227
227
|
npx @cachly-dev/mcp-server@latest autopilot # One command — signs in, configures every editor, bootstraps from git
|
|
228
228
|
npx @cachly-dev/mcp-server@latest demo # Preview your Brain (no account needed)
|
|
229
|
+
npx @cachly-dev/mcp-server@latest bench # Recall quality vs flat-file memory (no auth required)
|
|
229
230
|
npx @cachly-dev/mcp-server@latest setup # Interactive variant — pick editors yourself
|
|
230
231
|
npx @cachly-dev/mcp-server@latest health # Check token, API, editors, git hook
|
|
231
232
|
npx @cachly-dev/mcp-server@latest digest # Weekly Brain summary — shareable
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cachly-Bench — the moat proof.
|
|
3
|
+
*
|
|
4
|
+
* Measures recall quality on a fixed, labeled corpus and compares two rankers
|
|
5
|
+
* over the *same* BM25 candidate set, so the comparison isolates ranking quality:
|
|
6
|
+
*
|
|
7
|
+
* baseline — raw BM25+ keyword ranking (what a text-only / flat-file memory does)
|
|
8
|
+
* cachly — BM25+ followed by quality-aware reranking (rerank.ts)
|
|
9
|
+
*
|
|
10
|
+
* It reports standard IR metrics (Precision@k, Recall@k, MRR, nDCG@k) and the
|
|
11
|
+
* relative lift. The corpus (fixtures.ts) is fair: gold answers are the lessons
|
|
12
|
+
* that actually solve each problem, while text-similar failed attempts act as
|
|
13
|
+
* distractors — exactly where ranking by quality should pull ahead.
|
|
14
|
+
*
|
|
15
|
+
* Run: npm run bench (pretty report)
|
|
16
|
+
* npm run bench -- --json (machine-readable)
|
|
17
|
+
*/
|
|
18
|
+
import { type BenchLesson, type BenchQuery } from './fixtures.js';
|
|
19
|
+
export interface BenchMetrics {
|
|
20
|
+
precisionAt1: number;
|
|
21
|
+
precisionAt3: number;
|
|
22
|
+
recallAt3: number;
|
|
23
|
+
mrr: number;
|
|
24
|
+
ndcgAt5: number;
|
|
25
|
+
}
|
|
26
|
+
export interface BenchResult {
|
|
27
|
+
/** Flat-file memory simulation: naive term-overlap, no IDF, no quality signal. */
|
|
28
|
+
flatfile: BenchMetrics;
|
|
29
|
+
baseline: BenchMetrics;
|
|
30
|
+
cachly: BenchMetrics;
|
|
31
|
+
/** Relative lift of cachly vs the BM25 baseline. */
|
|
32
|
+
lift: BenchMetrics;
|
|
33
|
+
/** Relative lift of cachly vs a flat-file memory (the head-to-head that matters). */
|
|
34
|
+
liftVsFlatfile: BenchMetrics;
|
|
35
|
+
queryCount: number;
|
|
36
|
+
corpusSize: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Run the three-ranker benchmark over any labeled corpus + query set. Pure-ish:
|
|
40
|
+
* no network, no real Redis. Shared by the built-in fixture bench and the external
|
|
41
|
+
* labeled-corpus bench (external-corpus.ts) so both prove lift the same way.
|
|
42
|
+
*/
|
|
43
|
+
export declare function runBenchmarkOn(lessons: BenchLesson[], queries: BenchQuery[]): Promise<BenchResult>;
|
|
44
|
+
/** Run the benchmark over the built-in fixture corpus. */
|
|
45
|
+
export declare function runBenchmark(): Promise<BenchResult>;
|
|
46
|
+
export declare function formatReport(r: BenchResult): string;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cachly-Bench — the moat proof.
|
|
3
|
+
*
|
|
4
|
+
* Measures recall quality on a fixed, labeled corpus and compares two rankers
|
|
5
|
+
* over the *same* BM25 candidate set, so the comparison isolates ranking quality:
|
|
6
|
+
*
|
|
7
|
+
* baseline — raw BM25+ keyword ranking (what a text-only / flat-file memory does)
|
|
8
|
+
* cachly — BM25+ followed by quality-aware reranking (rerank.ts)
|
|
9
|
+
*
|
|
10
|
+
* It reports standard IR metrics (Precision@k, Recall@k, MRR, nDCG@k) and the
|
|
11
|
+
* relative lift. The corpus (fixtures.ts) is fair: gold answers are the lessons
|
|
12
|
+
* that actually solve each problem, while text-similar failed attempts act as
|
|
13
|
+
* distractors — exactly where ranking by quality should pull ahead.
|
|
14
|
+
*
|
|
15
|
+
* Run: npm run bench (pretty report)
|
|
16
|
+
* npm run bench -- --json (machine-readable)
|
|
17
|
+
*/
|
|
18
|
+
import { EventEmitter } from 'node:events';
|
|
19
|
+
import { keywordSearch } from '../search.js';
|
|
20
|
+
import { rerankByQuality } from '../rerank.js';
|
|
21
|
+
import { BENCH_LESSONS, BENCH_QUERIES } from './fixtures.js';
|
|
22
|
+
const LESSON_PREFIX = 'cachly:lesson:best:';
|
|
23
|
+
// ── Minimal in-memory Redis (only what keywordSearch needs) ────────────────────
|
|
24
|
+
class MiniRedis {
|
|
25
|
+
store = new Map();
|
|
26
|
+
set(key, value) { this.store.set(key, value); }
|
|
27
|
+
async get(key) { return this.store.get(key) ?? null; }
|
|
28
|
+
scanStream(opts) {
|
|
29
|
+
const emitter = new EventEmitter();
|
|
30
|
+
const pattern = '^' + opts.match.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$';
|
|
31
|
+
const regex = new RegExp(pattern);
|
|
32
|
+
const matches = [...this.store.keys()].filter(k => regex.test(k));
|
|
33
|
+
setImmediate(() => { emitter.emit('data', matches); emitter.emit('end'); });
|
|
34
|
+
return emitter;
|
|
35
|
+
}
|
|
36
|
+
pipeline() {
|
|
37
|
+
const cmds = [];
|
|
38
|
+
const store = this.store;
|
|
39
|
+
return {
|
|
40
|
+
get(key) { cmds.push(key); return this; },
|
|
41
|
+
async exec() {
|
|
42
|
+
return cmds.map(k => [null, store.get(k) ?? null]);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function indexCorpus(lessons) {
|
|
48
|
+
const redis = new MiniRedis();
|
|
49
|
+
for (const l of lessons) {
|
|
50
|
+
redis.set(`${LESSON_PREFIX}${l.topic}`, JSON.stringify(l));
|
|
51
|
+
}
|
|
52
|
+
return redis;
|
|
53
|
+
}
|
|
54
|
+
// ── IR metrics ─────────────────────────────────────────────────────────────────
|
|
55
|
+
function precisionAtK(ranked, relevant, k) {
|
|
56
|
+
const top = ranked.slice(0, k);
|
|
57
|
+
if (top.length === 0)
|
|
58
|
+
return 0;
|
|
59
|
+
const hits = top.filter(t => relevant.has(t)).length;
|
|
60
|
+
return hits / Math.min(k, top.length);
|
|
61
|
+
}
|
|
62
|
+
function recallAtK(ranked, relevant, k) {
|
|
63
|
+
if (relevant.size === 0)
|
|
64
|
+
return 0;
|
|
65
|
+
const top = ranked.slice(0, k);
|
|
66
|
+
const hits = top.filter(t => relevant.has(t)).length;
|
|
67
|
+
return hits / relevant.size;
|
|
68
|
+
}
|
|
69
|
+
function reciprocalRank(ranked, relevant) {
|
|
70
|
+
for (let i = 0; i < ranked.length; i++) {
|
|
71
|
+
if (relevant.has(ranked[i]))
|
|
72
|
+
return 1 / (i + 1);
|
|
73
|
+
}
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
function ndcgAtK(ranked, relevant, k) {
|
|
77
|
+
let dcg = 0;
|
|
78
|
+
for (let i = 0; i < Math.min(k, ranked.length); i++) {
|
|
79
|
+
if (relevant.has(ranked[i]))
|
|
80
|
+
dcg += 1 / Math.log2(i + 2);
|
|
81
|
+
}
|
|
82
|
+
let idcg = 0;
|
|
83
|
+
for (let i = 0; i < Math.min(k, relevant.size); i++) {
|
|
84
|
+
idcg += 1 / Math.log2(i + 2);
|
|
85
|
+
}
|
|
86
|
+
return idcg === 0 ? 0 : dcg / idcg;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Simulates how a flat-file memory (e.g. an LLM reading a `/memories` directory)
|
|
90
|
+
* actually surfaces knowledge: it has NO ranking engine. Practical retrieval is
|
|
91
|
+
* naive lexical overlap — count how many distinct query terms appear in the file's
|
|
92
|
+
* text — with NO inverse-document-frequency, NO length normalization, and crucially
|
|
93
|
+
* NO quality signal (a proven fix and a failed attempt are just text). Ties break by
|
|
94
|
+
* recency, mimicking "most recently edited note first". This is a fair, and honestly
|
|
95
|
+
* weaker, stand-in for "Claude reads its own memory files".
|
|
96
|
+
*/
|
|
97
|
+
function flatFileRank(lessons, query) {
|
|
98
|
+
const qTerms = new Set(query.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(t => t.length >= 3));
|
|
99
|
+
const scored = lessons.map(l => {
|
|
100
|
+
const blob = [l.topic, l.what_worked, l.what_failed ?? '', l.context ?? '', (l.tags ?? []).join(' ')]
|
|
101
|
+
.join(' ').toLowerCase();
|
|
102
|
+
let overlap = 0;
|
|
103
|
+
for (const t of qTerms)
|
|
104
|
+
if (blob.includes(t))
|
|
105
|
+
overlap++;
|
|
106
|
+
const recency = l.ts ? new Date(l.ts).getTime() : 0;
|
|
107
|
+
return { topic: l.topic, overlap, recency };
|
|
108
|
+
});
|
|
109
|
+
return scored
|
|
110
|
+
.filter(s => s.overlap > 0)
|
|
111
|
+
.sort((a, b) => (b.overlap - a.overlap) || (b.recency - a.recency))
|
|
112
|
+
.map(s => s.topic);
|
|
113
|
+
}
|
|
114
|
+
const matchTopic = (m) => m.key.replace(LESSON_PREFIX, '');
|
|
115
|
+
function mean(xs) {
|
|
116
|
+
return xs.length === 0 ? 0 : xs.reduce((a, b) => a + b, 0) / xs.length;
|
|
117
|
+
}
|
|
118
|
+
function aggregate(rows) {
|
|
119
|
+
return {
|
|
120
|
+
precisionAt1: mean(rows.map(r => r.precisionAt1)),
|
|
121
|
+
precisionAt3: mean(rows.map(r => r.precisionAt3)),
|
|
122
|
+
recallAt3: mean(rows.map(r => r.recallAt3)),
|
|
123
|
+
mrr: mean(rows.map(r => r.mrr)),
|
|
124
|
+
ndcgAt5: mean(rows.map(r => r.ndcgAt5)),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function scoreRanking(ranked, relevant) {
|
|
128
|
+
return {
|
|
129
|
+
precisionAt1: precisionAtK(ranked, relevant, 1),
|
|
130
|
+
precisionAt3: precisionAtK(ranked, relevant, 3),
|
|
131
|
+
recallAt3: recallAtK(ranked, relevant, 3),
|
|
132
|
+
mrr: reciprocalRank(ranked, relevant),
|
|
133
|
+
ndcgAt5: ndcgAtK(ranked, relevant, 5),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Run the three-ranker benchmark over any labeled corpus + query set. Pure-ish:
|
|
138
|
+
* no network, no real Redis. Shared by the built-in fixture bench and the external
|
|
139
|
+
* labeled-corpus bench (external-corpus.ts) so both prove lift the same way.
|
|
140
|
+
*/
|
|
141
|
+
export async function runBenchmarkOn(lessons, queries) {
|
|
142
|
+
const redis = indexCorpus(lessons);
|
|
143
|
+
const flatfileRows = [];
|
|
144
|
+
const baselineRows = [];
|
|
145
|
+
const cachlyRows = [];
|
|
146
|
+
for (const q of queries) {
|
|
147
|
+
const relevant = new Set(q.relevant);
|
|
148
|
+
// BM25 baseline + cachly share the same candidate set — isolates ranking quality.
|
|
149
|
+
const matches = await keywordSearch(redis, [`${LESSON_PREFIX}*`], q.query, 10);
|
|
150
|
+
const baselineRanked = matches.map(matchTopic);
|
|
151
|
+
const cachlyRanked = rerankByQuality(matches).map(matchTopic);
|
|
152
|
+
// Flat-file memory sees the whole corpus but has no ranking engine.
|
|
153
|
+
const flatfileRanked = flatFileRank(lessons, q.query);
|
|
154
|
+
flatfileRows.push(scoreRanking(flatfileRanked, relevant));
|
|
155
|
+
baselineRows.push(scoreRanking(baselineRanked, relevant));
|
|
156
|
+
cachlyRows.push(scoreRanking(cachlyRanked, relevant));
|
|
157
|
+
}
|
|
158
|
+
const flatfile = aggregate(flatfileRows);
|
|
159
|
+
const baseline = aggregate(baselineRows);
|
|
160
|
+
const cachly = aggregate(cachlyRows);
|
|
161
|
+
const liftOf = (b, c) => (b === 0 ? (c > 0 ? 1 : 0) : (c - b) / b);
|
|
162
|
+
const liftBetween = (from) => ({
|
|
163
|
+
precisionAt1: liftOf(from.precisionAt1, cachly.precisionAt1),
|
|
164
|
+
precisionAt3: liftOf(from.precisionAt3, cachly.precisionAt3),
|
|
165
|
+
recallAt3: liftOf(from.recallAt3, cachly.recallAt3),
|
|
166
|
+
mrr: liftOf(from.mrr, cachly.mrr),
|
|
167
|
+
ndcgAt5: liftOf(from.ndcgAt5, cachly.ndcgAt5),
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
flatfile, baseline, cachly,
|
|
171
|
+
lift: liftBetween(baseline),
|
|
172
|
+
liftVsFlatfile: liftBetween(flatfile),
|
|
173
|
+
queryCount: queries.length,
|
|
174
|
+
corpusSize: lessons.length,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** Run the benchmark over the built-in fixture corpus. */
|
|
178
|
+
export async function runBenchmark() {
|
|
179
|
+
return runBenchmarkOn(BENCH_LESSONS, BENCH_QUERIES);
|
|
180
|
+
}
|
|
181
|
+
// ── Reporting ──────────────────────────────────────────────────────────────────
|
|
182
|
+
const pct = (x) => `${(x * 100).toFixed(1)}%`;
|
|
183
|
+
const signedPct = (x) => `${x >= 0 ? '+' : ''}${(x * 100).toFixed(1)}%`;
|
|
184
|
+
export function formatReport(r) {
|
|
185
|
+
const row = (label, f, b, c, lf) => ` ${label.padEnd(12)} ${pct(f).padStart(8)} ${pct(b).padStart(8)} ${pct(c).padStart(8)} ${signedPct(lf).padStart(8)}`;
|
|
186
|
+
return [
|
|
187
|
+
'',
|
|
188
|
+
'🧠 Cachly-Bench — recall quality head-to-head',
|
|
189
|
+
'──────────────────────────────────────────────────────────────────────────',
|
|
190
|
+
` corpus: ${r.corpusSize} lessons · queries: ${r.queryCount}`,
|
|
191
|
+
' flat-file = naive term-overlap, no quality signal (≈ LLM reading memory files)',
|
|
192
|
+
' baseline = raw BM25+ keyword ranking',
|
|
193
|
+
' cachly = BM25+ followed by quality-aware reranking',
|
|
194
|
+
'',
|
|
195
|
+
` ${'metric'.padEnd(12)} ${'flatfile'.padStart(8)} ${'baseline'.padStart(8)} ${'cachly'.padStart(8)} ${'vs flat'.padStart(8)}`,
|
|
196
|
+
row('Precision@1', r.flatfile.precisionAt1, r.baseline.precisionAt1, r.cachly.precisionAt1, r.liftVsFlatfile.precisionAt1),
|
|
197
|
+
row('Precision@3', r.flatfile.precisionAt3, r.baseline.precisionAt3, r.cachly.precisionAt3, r.liftVsFlatfile.precisionAt3),
|
|
198
|
+
row('Recall@3', r.flatfile.recallAt3, r.baseline.recallAt3, r.cachly.recallAt3, r.liftVsFlatfile.recallAt3),
|
|
199
|
+
row('MRR', r.flatfile.mrr, r.baseline.mrr, r.cachly.mrr, r.liftVsFlatfile.mrr),
|
|
200
|
+
row('nDCG@5', r.flatfile.ndcgAt5, r.baseline.ndcgAt5, r.cachly.ndcgAt5, r.liftVsFlatfile.ndcgAt5),
|
|
201
|
+
'──────────────────────────────────────────────────────────────────────────',
|
|
202
|
+
` vs BM25 baseline : MRR ${signedPct(r.lift.mrr)} · Precision@1 ${signedPct(r.lift.precisionAt1)}`,
|
|
203
|
+
` vs flat-file mem : MRR ${signedPct(r.liftVsFlatfile.mrr)} · Precision@1 ${signedPct(r.liftVsFlatfile.precisionAt1)}`,
|
|
204
|
+
'',
|
|
205
|
+
].join('\n');
|
|
206
|
+
}
|
|
207
|
+
// ── CLI entry ───────────────────────────────────────────────────────────────────
|
|
208
|
+
const isMain = (() => {
|
|
209
|
+
try {
|
|
210
|
+
return import.meta.url === `file://${process.argv[1]}`
|
|
211
|
+
|| process.argv[1]?.endsWith('cachly-bench.ts')
|
|
212
|
+
|| process.argv[1]?.endsWith('cachly-bench.js');
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
})();
|
|
218
|
+
if (isMain) {
|
|
219
|
+
runBenchmark().then(result => {
|
|
220
|
+
if (process.argv.includes('--json')) {
|
|
221
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
process.stdout.write(formatReport(result));
|
|
225
|
+
}
|
|
226
|
+
}).catch(err => {
|
|
227
|
+
process.stderr.write(`bench failed: ${err instanceof Error ? err.stack : String(err)}\n`);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=cachly-bench.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cachly-bench.js","sourceRoot":"","sources":["../../../src/bench/cachly-bench.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External labeled-corpus benchmark.
|
|
3
|
+
*
|
|
4
|
+
* The built-in fixture bench (fixtures.ts) proves lift on a corpus *we* wrote.
|
|
5
|
+
* The honest next step is to prove the same lift on a corpus authored by a third
|
|
6
|
+
* party. This module loads a labeled corpus from a portable JSON file and runs the
|
|
7
|
+
* exact same three-ranker comparison (flat-file vs BM25 baseline vs cachly).
|
|
8
|
+
*
|
|
9
|
+
* Portable format (BEIR-ish, but with the optional quality metadata our reranker
|
|
10
|
+
* uses — a generic IR corpus without it will simply show cachly ≈ BM25, which is
|
|
11
|
+
* itself an honest, documented result):
|
|
12
|
+
*
|
|
13
|
+
* {
|
|
14
|
+
* "name": "my-team-incidents-2025",
|
|
15
|
+
* "lessons": [
|
|
16
|
+
* { "topic": "deploy:oom", "outcome": "success", "what_worked": "...",
|
|
17
|
+
* "confidence": 0.9, "recall_count": 4, "severity": "critical",
|
|
18
|
+
* "review_level": "senior", "reviewed_by": "alice", "endorsements": 2,
|
|
19
|
+
* "ts": "2025-01-02T00:00:00.000Z" },
|
|
20
|
+
* ...
|
|
21
|
+
* ],
|
|
22
|
+
* "queries": [
|
|
23
|
+
* { "query": "pod keeps restarting out of memory", "relevant": ["deploy:oom"] },
|
|
24
|
+
* ...
|
|
25
|
+
* ]
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* Run: npm run bench:external (uses the bundled sample)
|
|
29
|
+
* npm run bench:external -- ./path/corpus.json (your own labeled corpus)
|
|
30
|
+
* npm run bench:external -- ./corpus.json --json
|
|
31
|
+
*/
|
|
32
|
+
import { type BenchResult } from './cachly-bench.js';
|
|
33
|
+
import type { BenchLesson, BenchQuery } from './fixtures.js';
|
|
34
|
+
export interface ExternalCorpus {
|
|
35
|
+
name?: string;
|
|
36
|
+
lessons: BenchLesson[];
|
|
37
|
+
queries: BenchQuery[];
|
|
38
|
+
}
|
|
39
|
+
/** Parse + validate a portable corpus object. Throws a clear error on bad shape. */
|
|
40
|
+
export declare function parseExternalCorpus(raw: unknown): ExternalCorpus;
|
|
41
|
+
/** Load + validate a corpus from a JSON file path. */
|
|
42
|
+
export declare function loadExternalCorpus(path: string): Promise<ExternalCorpus>;
|
|
43
|
+
/** Run the three-ranker benchmark on an external corpus. */
|
|
44
|
+
export declare function runExternalBenchmark(corpus: ExternalCorpus): Promise<BenchResult>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External labeled-corpus benchmark.
|
|
3
|
+
*
|
|
4
|
+
* The built-in fixture bench (fixtures.ts) proves lift on a corpus *we* wrote.
|
|
5
|
+
* The honest next step is to prove the same lift on a corpus authored by a third
|
|
6
|
+
* party. This module loads a labeled corpus from a portable JSON file and runs the
|
|
7
|
+
* exact same three-ranker comparison (flat-file vs BM25 baseline vs cachly).
|
|
8
|
+
*
|
|
9
|
+
* Portable format (BEIR-ish, but with the optional quality metadata our reranker
|
|
10
|
+
* uses — a generic IR corpus without it will simply show cachly ≈ BM25, which is
|
|
11
|
+
* itself an honest, documented result):
|
|
12
|
+
*
|
|
13
|
+
* {
|
|
14
|
+
* "name": "my-team-incidents-2025",
|
|
15
|
+
* "lessons": [
|
|
16
|
+
* { "topic": "deploy:oom", "outcome": "success", "what_worked": "...",
|
|
17
|
+
* "confidence": 0.9, "recall_count": 4, "severity": "critical",
|
|
18
|
+
* "review_level": "senior", "reviewed_by": "alice", "endorsements": 2,
|
|
19
|
+
* "ts": "2025-01-02T00:00:00.000Z" },
|
|
20
|
+
* ...
|
|
21
|
+
* ],
|
|
22
|
+
* "queries": [
|
|
23
|
+
* { "query": "pod keeps restarting out of memory", "relevant": ["deploy:oom"] },
|
|
24
|
+
* ...
|
|
25
|
+
* ]
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* Run: npm run bench:external (uses the bundled sample)
|
|
29
|
+
* npm run bench:external -- ./path/corpus.json (your own labeled corpus)
|
|
30
|
+
* npm run bench:external -- ./corpus.json --json
|
|
31
|
+
*/
|
|
32
|
+
import { readFile } from 'node:fs/promises';
|
|
33
|
+
import { fileURLToPath } from 'node:url';
|
|
34
|
+
import { dirname, resolve } from 'node:path';
|
|
35
|
+
import { runBenchmarkOn, formatReport } from './cachly-bench.js';
|
|
36
|
+
/** Parse + validate a portable corpus object. Throws a clear error on bad shape. */
|
|
37
|
+
export function parseExternalCorpus(raw) {
|
|
38
|
+
if (!raw || typeof raw !== 'object') {
|
|
39
|
+
throw new Error('Corpus must be a JSON object with `lessons` and `queries`.');
|
|
40
|
+
}
|
|
41
|
+
const obj = raw;
|
|
42
|
+
const lessons = obj.lessons;
|
|
43
|
+
const queries = obj.queries;
|
|
44
|
+
if (!Array.isArray(lessons) || lessons.length === 0) {
|
|
45
|
+
throw new Error('Corpus.lessons must be a non-empty array.');
|
|
46
|
+
}
|
|
47
|
+
if (!Array.isArray(queries) || queries.length === 0) {
|
|
48
|
+
throw new Error('Corpus.queries must be a non-empty array.');
|
|
49
|
+
}
|
|
50
|
+
const topics = new Set();
|
|
51
|
+
for (const [i, l] of lessons.entries()) {
|
|
52
|
+
const lesson = l;
|
|
53
|
+
if (typeof lesson.topic !== 'string' || !lesson.topic.trim()) {
|
|
54
|
+
throw new Error(`lessons[${i}].topic must be a non-empty string.`);
|
|
55
|
+
}
|
|
56
|
+
if (typeof lesson.what_worked !== 'string') {
|
|
57
|
+
throw new Error(`lessons[${i}].what_worked must be a string.`);
|
|
58
|
+
}
|
|
59
|
+
if (topics.has(lesson.topic)) {
|
|
60
|
+
throw new Error(`Duplicate lesson topic "${lesson.topic}" — topics must be unique.`);
|
|
61
|
+
}
|
|
62
|
+
topics.add(lesson.topic);
|
|
63
|
+
}
|
|
64
|
+
for (const [i, q] of queries.entries()) {
|
|
65
|
+
const query = q;
|
|
66
|
+
if (typeof query.query !== 'string' || !query.query.trim()) {
|
|
67
|
+
throw new Error(`queries[${i}].query must be a non-empty string.`);
|
|
68
|
+
}
|
|
69
|
+
if (!Array.isArray(query.relevant) || query.relevant.length === 0) {
|
|
70
|
+
throw new Error(`queries[${i}].relevant must be a non-empty array of topics.`);
|
|
71
|
+
}
|
|
72
|
+
for (const rel of query.relevant) {
|
|
73
|
+
if (typeof rel !== 'string' || !topics.has(rel)) {
|
|
74
|
+
throw new Error(`queries[${i}] references unknown relevant topic "${String(rel)}".`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
name: typeof obj.name === 'string' ? obj.name : undefined,
|
|
80
|
+
lessons: lessons,
|
|
81
|
+
queries: queries,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Load + validate a corpus from a JSON file path. */
|
|
85
|
+
export async function loadExternalCorpus(path) {
|
|
86
|
+
const text = await readFile(path, 'utf-8');
|
|
87
|
+
let raw;
|
|
88
|
+
try {
|
|
89
|
+
raw = JSON.parse(text);
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
throw new Error(`Corpus file is not valid JSON: ${e.message}`);
|
|
93
|
+
}
|
|
94
|
+
return parseExternalCorpus(raw);
|
|
95
|
+
}
|
|
96
|
+
/** Run the three-ranker benchmark on an external corpus. */
|
|
97
|
+
export async function runExternalBenchmark(corpus) {
|
|
98
|
+
return runBenchmarkOn(corpus.lessons, corpus.queries);
|
|
99
|
+
}
|
|
100
|
+
// ── CLI entry ───────────────────────────────────────────────────────────────────
|
|
101
|
+
const isMain = (() => {
|
|
102
|
+
try {
|
|
103
|
+
return import.meta.url === `file://${process.argv[1]}`
|
|
104
|
+
|| process.argv[1]?.endsWith('external-corpus.ts')
|
|
105
|
+
|| process.argv[1]?.endsWith('external-corpus.js');
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
if (isMain) {
|
|
112
|
+
const args = process.argv.slice(2).filter(a => a !== '--json');
|
|
113
|
+
const asJson = process.argv.includes('--json');
|
|
114
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
115
|
+
// Default to the bundled sample so `npm run bench:external` works out of the box.
|
|
116
|
+
const corpusPath = args[0]
|
|
117
|
+
? resolve(process.cwd(), args[0])
|
|
118
|
+
: resolve(here, 'external', 'sample-corpus.json');
|
|
119
|
+
loadExternalCorpus(corpusPath)
|
|
120
|
+
.then(async (corpus) => {
|
|
121
|
+
const result = await runExternalBenchmark(corpus);
|
|
122
|
+
if (asJson) {
|
|
123
|
+
console.log(JSON.stringify({ corpus: corpus.name ?? corpusPath, ...result }, null, 2));
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
console.log(`\n📦 External corpus: ${corpus.name ?? corpusPath}`);
|
|
127
|
+
console.log(formatReport(result));
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
.catch((e) => {
|
|
131
|
+
console.error(`\n❌ External bench failed: ${e.message}\n`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=external-corpus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external-corpus.js","sourceRoot":"","sources":["../../../src/bench/external-corpus.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cachly-Bench fixture corpus.
|
|
3
|
+
*
|
|
4
|
+
* A small but realistic corpus of engineering "lessons" plus a set of labeled
|
|
5
|
+
* natural-language queries. Each query declares which lesson topics are *truly*
|
|
6
|
+
* relevant — the answer an agent actually wants.
|
|
7
|
+
*
|
|
8
|
+
* The corpus is deliberately adversarial in a fair way: for several queries there
|
|
9
|
+
* is a text-similar but LOW-QUALITY distractor (a failed attempt, an unverified
|
|
10
|
+
* note) competing with the proven success lesson. This is exactly the situation
|
|
11
|
+
* where raw text ranking (and a flat-file memory) goes wrong, and where Cachly's
|
|
12
|
+
* quality-aware reranking should win. The labels reward the lesson that solves the
|
|
13
|
+
* problem, not the one that merely shares vocabulary.
|
|
14
|
+
*/
|
|
15
|
+
export interface BenchLesson {
|
|
16
|
+
/** Stored under cachly:lesson:best:{topic} */
|
|
17
|
+
topic: string;
|
|
18
|
+
outcome: 'success' | 'partial' | 'failure';
|
|
19
|
+
what_worked: string;
|
|
20
|
+
what_failed?: string;
|
|
21
|
+
context?: string;
|
|
22
|
+
severity?: 'critical' | 'major' | 'minor';
|
|
23
|
+
confidence?: number;
|
|
24
|
+
recall_count?: number;
|
|
25
|
+
reviewed_by?: string;
|
|
26
|
+
review_level?: 'senior' | 'peer';
|
|
27
|
+
endorsements?: number;
|
|
28
|
+
tags?: string[];
|
|
29
|
+
ts?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface BenchQuery {
|
|
32
|
+
query: string;
|
|
33
|
+
/** Topics that genuinely answer the query (the gold set). */
|
|
34
|
+
relevant: string[];
|
|
35
|
+
}
|
|
36
|
+
export declare const BENCH_LESSONS: BenchLesson[];
|
|
37
|
+
export declare const BENCH_QUERIES: BenchQuery[];
|