@cachly-dev/mcp-server 0.10.88 → 0.10.90

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 CHANGED
@@ -22,7 +22,7 @@
22
22
  <img src="https://img.shields.io/badge/GDPR-EU%20servers-green" alt="GDPR: EU servers" />
23
23
  </a>
24
24
  &nbsp;
25
- <img src="https://img.shields.io/badge/107%20MCP%20tools-violet" alt="107 MCP tools" />
25
+ <img src="https://img.shields.io/badge/114%20MCP%20tools-violet" alt="114 MCP tools" />
26
26
  &nbsp;
27
27
  <img src="https://img.shields.io/badge/License-Apache--2.0-yellow" alt="License: Apache-2.0" />
28
28
  </p>
@@ -101,30 +101,20 @@ Like what you see? Make it permanent in the next step.
101
101
 
102
102
  ---
103
103
 
104
- ## The plan — three steps, then it's automatic
105
-
106
- ### 1. Run one command
104
+ ## Setup — one command
107
105
 
108
106
  ```bash
109
- npx @cachly-dev/mcp-server@latest setup
107
+ npx @cachly-dev/mcp-server@latest autopilot
110
108
  ```
111
109
 
112
- It signs you in (one browser click, no password, no credit card), detects every AI
113
- editor you use, writes the MCP config, seeds your brain from git history, and installs
114
- a git hook so it keeps learning.
115
-
116
- ### 2. Restart your editor
117
-
118
- That's it. From now on your AI arrives pre-briefed — every session.
119
-
120
- ### 3. Just work
121
-
122
- cachly learns in the background. You never have to "remember to save." Every fix,
123
- every commit, every session feeds the brain automatically.
110
+ Autopilot does everything in a single command: it auto-detects every AI editor you use,
111
+ writes the MCP config, signs you in via browser device-flow (one click, no password, no
112
+ credit card), and bootstraps your brain from git history. Restart your editor and your AI
113
+ arrives pre-briefed — every session, automatically.
124
114
 
125
115
  > **Already inside Claude / Cursor / Copilot?** Paste this to your AI and it configures everything itself:
126
116
  > ```
127
- > Set up cachly for this project. Run: npx @cachly-dev/mcp-server@latest setup
117
+ > Set up cachly for this project. Run: npx @cachly-dev/mcp-server@latest autopilot
128
118
  > It gives my AI persistent memory across sessions. Follow the browser login
129
119
  > (one click, no credit card), then restart the editor.
130
120
  > ```
@@ -234,8 +224,10 @@ causal_trace(problem="auth breaks after restart")
234
224
  ## CLI Commands
235
225
 
236
226
  ```bash
227
+ npx @cachly-dev/mcp-server@latest autopilot # One command — signs in, configures every editor, bootstraps from git
237
228
  npx @cachly-dev/mcp-server@latest demo # Preview your Brain (no account needed)
238
- npx @cachly-dev/mcp-server@latest setup # Wire up all your AI editors (1–5 minutes)
229
+ npx @cachly-dev/mcp-server@latest bench # Recall quality vs flat-file memory (no auth required)
230
+ npx @cachly-dev/mcp-server@latest setup # Interactive variant — pick editors yourself
239
231
  npx @cachly-dev/mcp-server@latest health # Check token, API, editors, git hook
240
232
  npx @cachly-dev/mcp-server@latest digest # Weekly Brain summary — shareable
241
233
  npx @cachly-dev/mcp-server@latest share # Generate a shareable stats card + tweet
@@ -252,7 +244,7 @@ npx @cachly-dev/mcp-server@latest learn-git # Auto-learn lessons from recent git
252
244
 
253
245
  ---
254
246
 
255
- ## MCP Tools (113 total)
247
+ ## MCP Tools (114 total)
256
248
 
257
249
  ### 🧠 Session & Memory (most used)
258
250
 
@@ -344,7 +336,7 @@ sessions + WIP registry — typically one tool call.
344
336
 
345
337
  ## Editor support matrix
346
338
 
347
- `npx @cachly-dev/mcp-server@latest setup` auto-detects and configures all of the
339
+ `npx @cachly-dev/mcp-server@latest autopilot` auto-detects and configures all of the
348
340
  following. Manual snippets are in the **Manual Setup** section below.
349
341
 
350
342
  | Editor / Client | Auto-setup | Config file written | Global config | Notes |
@@ -370,7 +362,7 @@ following. Manual snippets are in the **Manual Setup** section below.
370
362
 
371
363
  > **Tip — fastest per-project setup from inside Claude Code:**
372
364
  > ```
373
- > Set up cachly for this project: npx @cachly-dev/mcp-server@latest setup
365
+ > Set up cachly for this project: npx @cachly-dev/mcp-server@latest autopilot
374
366
  > ```
375
367
  > Claude runs it and restarts automatically.
376
368
 
@@ -455,7 +447,7 @@ private backend instead of `api.cachly.dev`:
455
447
 
456
448
  ```bash
457
449
  # One-shot: wire up the wizard against your self-hosted backend
458
- npx @cachly-dev/mcp-server@latest setup --api-url https://cachly.mycorp.internal
450
+ npx @cachly-dev/mcp-server@latest autopilot --api-url https://cachly.mycorp.internal
459
451
 
460
452
  # Or non-interactively
461
453
  npx @cachly-dev/mcp-server@latest init \
@@ -513,5 +505,5 @@ keep talking to your backend on every editor launch. All data stays in your infr
513
505
  > brain that remembers, learns, and gets sharper with every commit.
514
506
  >
515
507
  > ```bash
516
- > npx @cachly-dev/mcp-server@latest setup
508
+ > npx @cachly-dev/mcp-server@latest autopilot
517
509
  > ```
@@ -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[];