@cachly-dev/mcp-server 0.10.65 → 0.10.66

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.
Files changed (269) hide show
  1. package/dist/src/handlers/brain.js +1 -1
  2. package/dist/src/index.js +1 -1
  3. package/package.json +7 -1
  4. package/server.json +2 -2
  5. package/.env.test.example +0 -23
  6. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -50
  7. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  8. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
  9. package/.github/workflows/eas-build-android.yml +0 -58
  10. package/.github/workflows/eas-update.yml +0 -55
  11. package/.github/workflows/publish.yml +0 -72
  12. package/BENCH.md +0 -113
  13. package/CHANGELOG.md +0 -653
  14. package/Dockerfile +0 -16
  15. package/LAUNCH_TEXTS.md +0 -182
  16. package/PROGRESS.md +0 -141
  17. package/STRATEGY.md +0 -228
  18. package/VISION_10X.md +0 -133
  19. package/apps/travel-chaos-organizer/.env.example +0 -49
  20. package/apps/travel-chaos-organizer/DEPLOY.md +0 -328
  21. package/apps/travel-chaos-organizer/Makefile +0 -57
  22. package/apps/travel-chaos-organizer/README.md +0 -159
  23. package/apps/travel-chaos-organizer/backend/.env.example +0 -10
  24. package/apps/travel-chaos-organizer/backend/Dockerfile +0 -25
  25. package/apps/travel-chaos-organizer/backend/alembic/env.py +0 -56
  26. package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +0 -24
  27. package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
  28. package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +0 -24
  29. package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +0 -24
  30. package/apps/travel-chaos-organizer/backend/alembic.ini +0 -39
  31. package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
  32. package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
  33. package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +0 -81
  34. package/apps/travel-chaos-organizer/backend/app/config.py +0 -66
  35. package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
  36. package/apps/travel-chaos-organizer/backend/app/db/database.py +0 -20
  37. package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +0 -71
  38. package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +0 -31
  39. package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +0 -4
  40. package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +0 -5
  41. package/apps/travel-chaos-organizer/backend/app/limiter.py +0 -4
  42. package/apps/travel-chaos-organizer/backend/app/logging_config.py +0 -25
  43. package/apps/travel-chaos-organizer/backend/app/main.py +0 -77
  44. package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
  45. package/apps/travel-chaos-organizer/backend/app/models/schemas.py +0 -142
  46. package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
  47. package/apps/travel-chaos-organizer/backend/app/routers/admin.py +0 -237
  48. package/apps/travel-chaos-organizer/backend/app/routers/cache.py +0 -13
  49. package/apps/travel-chaos-organizer/backend/app/routers/events.py +0 -29
  50. package/apps/travel-chaos-organizer/backend/app/routers/export.py +0 -122
  51. package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +0 -122
  52. package/apps/travel-chaos-organizer/backend/app/routers/items.py +0 -130
  53. package/apps/travel-chaos-organizer/backend/app/routers/mail.py +0 -108
  54. package/apps/travel-chaos-organizer/backend/app/routers/parse.py +0 -303
  55. package/apps/travel-chaos-organizer/backend/app/routers/payments.py +0 -179
  56. package/apps/travel-chaos-organizer/backend/app/routers/trips.py +0 -134
  57. package/apps/travel-chaos-organizer/backend/app/routers/users.py +0 -169
  58. package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +0 -37
  59. package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
  60. package/apps/travel-chaos-organizer/backend/app/services/cache.py +0 -131
  61. package/apps/travel-chaos-organizer/backend/app/services/drip.py +0 -128
  62. package/apps/travel-chaos-organizer/backend/app/services/email.py +0 -61
  63. package/apps/travel-chaos-organizer/backend/app/services/errors.py +0 -44
  64. package/apps/travel-chaos-organizer/backend/app/services/notifier.py +0 -10
  65. package/apps/travel-chaos-organizer/backend/app/services/ollama.py +0 -101
  66. package/apps/travel-chaos-organizer/backend/app/services/parser.py +0 -22
  67. package/apps/travel-chaos-organizer/backend/app/services/quota.py +0 -85
  68. package/apps/travel-chaos-organizer/backend/app/services/sentry.py +0 -11
  69. package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +0 -35
  70. package/apps/travel-chaos-organizer/backend/pytest.ini +0 -4
  71. package/apps/travel-chaos-organizer/backend/requirements-dev.txt +0 -6
  72. package/apps/travel-chaos-organizer/backend/requirements.txt +0 -25
  73. package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
  74. package/apps/travel-chaos-organizer/backend/tests/conftest.py +0 -223
  75. package/apps/travel-chaos-organizer/backend/tests/test_admin.py +0 -99
  76. package/apps/travel-chaos-organizer/backend/tests/test_cache.py +0 -142
  77. package/apps/travel-chaos-organizer/backend/tests/test_drip.py +0 -32
  78. package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +0 -121
  79. package/apps/travel-chaos-organizer/backend/tests/test_export.py +0 -48
  80. package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +0 -107
  81. package/apps/travel-chaos-organizer/backend/tests/test_items.py +0 -138
  82. package/apps/travel-chaos-organizer/backend/tests/test_mail.py +0 -113
  83. package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +0 -145
  84. package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +0 -98
  85. package/apps/travel-chaos-organizer/backend/tests/test_parser.py +0 -72
  86. package/apps/travel-chaos-organizer/backend/tests/test_payments.py +0 -163
  87. package/apps/travel-chaos-organizer/backend/tests/test_quota.py +0 -46
  88. package/apps/travel-chaos-organizer/backend/tests/test_search.py +0 -67
  89. package/apps/travel-chaos-organizer/backend/tests/test_trips.py +0 -123
  90. package/apps/travel-chaos-organizer/backend/tests/test_users.py +0 -52
  91. package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
  92. package/apps/travel-chaos-organizer/caddy/Caddyfile +0 -27
  93. package/apps/travel-chaos-organizer/docker-compose.node1.yml +0 -31
  94. package/apps/travel-chaos-organizer/docker-compose.override.yml +0 -14
  95. package/apps/travel-chaos-organizer/docker-compose.yml +0 -128
  96. package/apps/travel-chaos-organizer/frontend/.env.example +0 -13
  97. package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +0 -80
  98. package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +0 -166
  99. package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +0 -130
  100. package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +0 -381
  101. package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +0 -346
  102. package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +0 -363
  103. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +0 -34
  104. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +0 -35
  105. package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +0 -91
  106. package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +0 -71
  107. package/apps/travel-chaos-organizer/frontend/app/auth.tsx +0 -29
  108. package/apps/travel-chaos-organizer/frontend/app/index.tsx +0 -16
  109. package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +0 -119
  110. package/apps/travel-chaos-organizer/frontend/app.config.ts +0 -54
  111. package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +0 -66
  112. package/apps/travel-chaos-organizer/frontend/assets/icon.svg +0 -79
  113. package/apps/travel-chaos-organizer/frontend/assets/splash.svg +0 -15
  114. package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +0 -102
  115. package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +0 -107
  116. package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +0 -109
  117. package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +0 -45
  118. package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +0 -19
  119. package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +0 -155
  120. package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +0 -55
  121. package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +0 -80
  122. package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +0 -39
  123. package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +0 -39
  124. package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +0 -70
  125. package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +0 -121
  126. package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +0 -70
  127. package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +0 -73
  128. package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +0 -130
  129. package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +0 -60
  130. package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +0 -42
  131. package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +0 -146
  132. package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +0 -70
  133. package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +0 -44
  134. package/apps/travel-chaos-organizer/frontend/eas.json +0 -22
  135. package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +0 -24
  136. package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +0 -60
  137. package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +0 -112
  138. package/apps/travel-chaos-organizer/frontend/lib/api.ts +0 -223
  139. package/apps/travel-chaos-organizer/frontend/lib/auth.ts +0 -93
  140. package/apps/travel-chaos-organizer/frontend/lib/db.ts +0 -86
  141. package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +0 -54
  142. package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +0 -13
  143. package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +0 -112
  144. package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +0 -145
  145. package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +0 -43
  146. package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +0 -28
  147. package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +0 -44
  148. package/apps/travel-chaos-organizer/frontend/package-lock.json +0 -13221
  149. package/apps/travel-chaos-organizer/frontend/package.json +0 -44
  150. package/apps/travel-chaos-organizer/frontend/tsconfig.json +0 -23
  151. package/apps/travel-chaos-organizer/keycloak/realm-export.json +0 -59
  152. package/apps/travel-chaos-organizer/landing/index.html +0 -773
  153. package/apps/travel-chaos-organizer/scripts/backup.sh +0 -14
  154. package/apps/travel-chaos-organizer/scripts/configure-env.sh +0 -82
  155. package/apps/travel-chaos-organizer/scripts/deploy.sh +0 -54
  156. package/apps/travel-chaos-organizer/scripts/health-check.sh +0 -36
  157. package/apps/travel-chaos-organizer/scripts/migrate.sh +0 -7
  158. package/apps/travel-chaos-organizer/scripts/server-setup.sh +0 -85
  159. package/apps/travel-chaos-organizer/scripts/smoke-test.sh +0 -29
  160. package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +0 -71
  161. package/apps/travel-chaos-organizer/telegram-bot/.env.example +0 -3
  162. package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +0 -6
  163. package/apps/travel-chaos-organizer/telegram-bot/bot.py +0 -504
  164. package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +0 -3
  165. package/dist/src/__tests__/auth.test.d.ts +0 -1
  166. package/dist/src/__tests__/auth.test.js +0 -107
  167. package/dist/src/__tests__/auth.test.js.map +0 -1
  168. package/dist/src/__tests__/brain-flow.test.d.ts +0 -12
  169. package/dist/src/__tests__/brain-flow.test.js +0 -1241
  170. package/dist/src/__tests__/brain-flow.test.js.map +0 -1
  171. package/dist/src/__tests__/confidence.test.d.ts +0 -13
  172. package/dist/src/__tests__/confidence.test.js +0 -233
  173. package/dist/src/__tests__/confidence.test.js.map +0 -1
  174. package/dist/src/__tests__/e2e/api-auth.test.d.ts +0 -7
  175. package/dist/src/__tests__/e2e/api-auth.test.js +0 -162
  176. package/dist/src/__tests__/e2e/api-auth.test.js.map +0 -1
  177. package/dist/src/__tests__/e2e/api-health.test.d.ts +0 -7
  178. package/dist/src/__tests__/e2e/api-health.test.js +0 -95
  179. package/dist/src/__tests__/e2e/api-health.test.js.map +0 -1
  180. package/dist/src/__tests__/e2e/cli.test.d.ts +0 -10
  181. package/dist/src/__tests__/e2e/cli.test.js +0 -204
  182. package/dist/src/__tests__/e2e/cli.test.js.map +0 -1
  183. package/dist/src/__tests__/e2e/config.d.ts +0 -35
  184. package/dist/src/__tests__/e2e/config.js +0 -53
  185. package/dist/src/__tests__/e2e/config.js.map +0 -1
  186. package/dist/src/__tests__/e2e/funnel.test.d.ts +0 -13
  187. package/dist/src/__tests__/e2e/funnel.test.js +0 -90
  188. package/dist/src/__tests__/e2e/funnel.test.js.map +0 -1
  189. package/dist/src/__tests__/e2e/mcp-tools.test.d.ts +0 -7
  190. package/dist/src/__tests__/e2e/mcp-tools.test.js +0 -129
  191. package/dist/src/__tests__/e2e/mcp-tools.test.js.map +0 -1
  192. package/dist/src/__tests__/e2e/provisioning.test.d.ts +0 -15
  193. package/dist/src/__tests__/e2e/provisioning.test.js +0 -123
  194. package/dist/src/__tests__/e2e/provisioning.test.js.map +0 -1
  195. package/dist/src/__tests__/handlers-unit.test.d.ts +0 -12
  196. package/dist/src/__tests__/handlers-unit.test.js +0 -505
  197. package/dist/src/__tests__/handlers-unit.test.js.map +0 -1
  198. package/dist/src/__tests__/namespace.test.d.ts +0 -1
  199. package/dist/src/__tests__/namespace.test.js +0 -114
  200. package/dist/src/__tests__/namespace.test.js.map +0 -1
  201. package/dist/src/__tests__/rerank.test.d.ts +0 -1
  202. package/dist/src/__tests__/rerank.test.js +0 -125
  203. package/dist/src/__tests__/rerank.test.js.map +0 -1
  204. package/dist/src/__tests__/utils.test.d.ts +0 -1
  205. package/dist/src/__tests__/utils.test.js +0 -83
  206. package/dist/src/__tests__/utils.test.js.map +0 -1
  207. package/dist/src/bench/cachly-bench.d.ts +0 -39
  208. package/dist/src/bench/cachly-bench.js +0 -223
  209. package/dist/src/bench/cachly-bench.js.map +0 -1
  210. package/dist/src/bench/fixtures.d.ts +0 -37
  211. package/dist/src/bench/fixtures.js +0 -225
  212. package/dist/src/bench/fixtures.js.map +0 -1
  213. package/dist/src/search.test.d.ts +0 -1
  214. package/dist/src/search.test.js +0 -713
  215. package/dist/src/search.test.js.map +0 -1
  216. package/eslint.config.js +0 -16
  217. package/glama.json +0 -36
  218. package/llms.txt +0 -155
  219. package/packages/telegram-notify/SPLIT_PLAN.md +0 -106
  220. package/packages/telegram-notify/client.ts +0 -88
  221. package/packages/telegram-notify/emojis.json +0 -22
  222. package/packages/telegram-notify/pyproject.toml +0 -17
  223. package/packages/telegram-notify/telegram_notify/__init__.py +0 -3
  224. package/packages/telegram-notify/telegram_notify/client.py +0 -87
  225. package/packages/telegram-notify/telegram_notify/emojis.json +0 -22
  226. package/scripts/e2e-run.sh +0 -160
  227. package/scripts/generate-icons.js +0 -28
  228. package/smithery.yaml +0 -45
  229. package/src/__tests__/auth.test.ts +0 -147
  230. package/src/__tests__/brain-flow.test.ts +0 -1472
  231. package/src/__tests__/confidence.test.ts +0 -285
  232. package/src/__tests__/e2e/api-auth.test.ts +0 -182
  233. package/src/__tests__/e2e/api-health.test.ts +0 -103
  234. package/src/__tests__/e2e/cli.test.ts +0 -228
  235. package/src/__tests__/e2e/config.ts +0 -53
  236. package/src/__tests__/e2e/funnel.test.ts +0 -99
  237. package/src/__tests__/e2e/mcp-tools.test.ts +0 -191
  238. package/src/__tests__/e2e/provisioning.test.ts +0 -128
  239. package/src/__tests__/handlers-unit.test.ts +0 -677
  240. package/src/__tests__/namespace.test.ts +0 -126
  241. package/src/__tests__/rerank.test.ts +0 -141
  242. package/src/__tests__/utils.test.ts +0 -81
  243. package/src/auth.ts +0 -46
  244. package/src/bench/cachly-bench.ts +0 -263
  245. package/src/bench/fixtures.ts +0 -256
  246. package/src/ckg.ts +0 -103
  247. package/src/confidence.ts +0 -38
  248. package/src/embeddings.ts +0 -217
  249. package/src/handlers/advanced.ts +0 -647
  250. package/src/handlers/brain.ts +0 -2622
  251. package/src/handlers/cache.ts +0 -633
  252. package/src/handlers/context.ts +0 -222
  253. package/src/handlers/fedbrain.ts +0 -1232
  254. package/src/handlers/instances.ts +0 -180
  255. package/src/handlers/roadmap.ts +0 -201
  256. package/src/handlers/syndicate.ts +0 -460
  257. package/src/handlers/tco.ts +0 -345
  258. package/src/handlers/team.ts +0 -1249
  259. package/src/index.ts +0 -2815
  260. package/src/namespace.ts +0 -128
  261. package/src/notifier.ts +0 -5
  262. package/src/rerank.ts +0 -148
  263. package/src/search.test.ts +0 -848
  264. package/src/search.ts +0 -1781
  265. package/src/tools.ts +0 -2056
  266. package/src/utils.ts +0 -90
  267. package/test-features.cjs +0 -23
  268. package/tsconfig.json +0 -16
  269. package/vitest.config.ts +0 -14
@@ -1,126 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { detectNamespace } from '../namespace.js';
3
-
4
- // ── Namespace Auto-Detection ───────────────────────────────────────────────
5
-
6
- describe('detectNamespace', () => {
7
- // ── Code ────────────────────────────────────────────────────────────────────
8
- describe('code detection', () => {
9
- it.each([
10
- ['python def', 'def process(data): return data * 2'],
11
- ['js const arrow', 'const handler = () => response.send(200)'],
12
- ['ts class', 'class UserService { constructor(private repo: Repo) {} }'],
13
- ['import statement', 'import { useState } from "react"'],
14
- ['shebang line', '#!/usr/bin/env python3\nprint("hello")'],
15
- ['go func', 'func main() { fmt.Println("hello") }'],
16
- ['cpp include', '#include <iostream>\nint main() { return 0; }'],
17
- ['interface block', 'interface ICache { get(key: string): void; }'],
18
- ['struct block', 'struct Config { host string }'],
19
- ['async def', 'async def fetch_data(url: str) -> dict:'],
20
- ['lambda', 'fn = lambda x: x * 2'],
21
- ['package decl', 'package com.example.service;'],
22
- ])('detects code: %s', (_, prompt) => {
23
- expect(detectNamespace(prompt)).toBe('cachly:sem:code');
24
- });
25
- });
26
-
27
- // ── Translation ─────────────────────────────────────────────────────────────
28
- describe('translation detection', () => {
29
- it.each([
30
- ['translate', 'translate this paragraph to Spanish'],
31
- ['übersetze', 'übersetze diesen Text bitte'],
32
- ['auf deutsch', 'Schreib das auf deutsch'],
33
- ['in english', 'Please write this in english'],
34
- ['ins deutsche', 'Bitte übersetze ins Deutsche'],
35
- ['traduce', 'traduce este texto al alemán'],
36
- ['traduis', 'traduis ce texte en anglais'],
37
- ['vertaal', 'vertaal dit naar het Nederlands'],
38
- ])('detects translation: %s', (_, prompt) => {
39
- expect(detectNamespace(prompt)).toBe('cachly:sem:translation');
40
- });
41
- });
42
-
43
- // ── Summary ──────────────────────────────────────────────────────────────────
44
- describe('summary detection', () => {
45
- it.each([
46
- ['summarize', 'summarize this article for me'],
47
- ['summarise', 'summarise the key points'],
48
- ['tl;dr', 'tl;dr of the following blog post:'],
49
- ['tldr', 'tldr of this document'],
50
- ['key points', 'what are the key points?'],
51
- ['zusammenfass', 'fasse zusammen worum es geht'],
52
- ['give me a brief', 'give me a brief overview'],
53
- ['in a nutshell', 'explain machine learning in a nutshell'],
54
- ['stichpunkte', 'Gib mir die wichtigsten Stichpunkte'],
55
- ])('detects summary: %s', (_, prompt) => {
56
- expect(detectNamespace(prompt)).toBe('cachly:sem:summary');
57
- });
58
- });
59
-
60
- // ── Q&A ──────────────────────────────────────────────────────────────────────
61
- describe('qa detection', () => {
62
- it.each([
63
- ['what is', 'what is the capital of France?'],
64
- ['how does', 'how does photosynthesis work?'],
65
- ['why is', 'why is the sky blue?'],
66
- ['who invented', 'who invented the internet?'],
67
- ['where is', 'where is Mount Everest located?'],
68
- ['when was', 'when was the Eiffel Tower built?'],
69
- ['can you', 'can you explain recursion?'],
70
- ['is redis', 'is Redis single-threaded?'],
71
- ['does postgres', 'does postgres support JSON natively?'],
72
- ['wer ist', 'wer ist der aktuelle Bundeskanzler?'],
73
- ['wie funktioniert', 'wie funktioniert ein JWT Token?'],
74
- ['trailing ?', 'Redis vs Memcached?'],
75
- ])('detects q&a: %s', (_, prompt) => {
76
- expect(detectNamespace(prompt)).toBe('cachly:sem:qa');
77
- });
78
- });
79
-
80
- // ── Creative ─────────────────────────────────────────────────────────────────
81
- describe('creative fallback', () => {
82
- it.each([
83
- ['poem request', 'Write a short poem about autumn leaves'],
84
- ['story request', 'Tell me a fantasy story about dragons'],
85
- ['product copy', 'Generate a product description for running shoes'],
86
- ['greeting', 'Good morning, how are you today'],
87
- ['general task', 'Help me brainstorm names for my startup'],
88
- ])('falls back to creative: %s', (_, prompt) => {
89
- expect(detectNamespace(prompt)).toBe('cachly:sem:creative');
90
- });
91
- });
92
-
93
- // ── Edge cases ────────────────────────────────────────────────────────────────
94
- describe('edge cases', () => {
95
- it('is case-insensitive', () => {
96
- expect(detectNamespace('CONST x = 1')).toBe('cachly:sem:code');
97
- expect(detectNamespace('TRANSLATE THIS')).toBe('cachly:sem:translation');
98
- expect(detectNamespace('SUMMARIZE PLEASE')).toBe('cachly:sem:summary');
99
- expect(detectNamespace('WHAT IS GRAVITY?')).toBe('cachly:sem:qa');
100
- });
101
-
102
- it('trims surrounding whitespace', () => {
103
- expect(detectNamespace(' what is Redis? ')).toBe('cachly:sem:qa');
104
- expect(detectNamespace(' const x = 1 ')).toBe('cachly:sem:code');
105
- });
106
-
107
- it('code keyword takes priority over translation', () => {
108
- // Both "translate" and "def" present – code wins (checked first)
109
- expect(detectNamespace('translate this function: def foo(): pass')).toBe('cachly:sem:code');
110
- });
111
-
112
- it('code keyword takes priority over summary', () => {
113
- expect(detectNamespace('summarize this class MyService { }'))
114
- .toBe('cachly:sem:code');
115
- });
116
-
117
- it('handles empty string as creative', () => {
118
- expect(detectNamespace('')).toBe('cachly:sem:creative');
119
- });
120
-
121
- it('a single ? at end is qa', () => {
122
- expect(detectNamespace('Interesting concept?')).toBe('cachly:sem:qa');
123
- });
124
- });
125
- });
126
-
@@ -1,141 +0,0 @@
1
- /**
2
- * Tests for quality-aware reranking and the Cachly-Bench regression guard.
3
- */
4
- import { describe, it, expect } from 'vitest';
5
- import { qualityMultiplier, extractLessonQuality, rerankByQuality, type LessonQuality } from '../rerank.js';
6
- import type { KeywordMatch } from '../search.js';
7
- import { runBenchmark } from '../bench/cachly-bench.js';
8
-
9
- const lessonMatch = (topic: string, score: number, lesson: Partial<LessonQuality> & Record<string, unknown>): KeywordMatch => ({
10
- key: `cachly:lesson:best:${topic}`,
11
- content: JSON.stringify(lesson),
12
- score,
13
- matchedWords: ['x'],
14
- });
15
-
16
- describe('qualityMultiplier', () => {
17
- it('returns neutral 1.0 for documents with no quality signal', () => {
18
- expect(qualityMultiplier(null)).toBe(1.0);
19
- expect(qualityMultiplier({})).toBe(1.0);
20
- });
21
-
22
- it('ranks a proven success above a fresh failure', () => {
23
- const success = qualityMultiplier({ outcome: 'success', confidence: 0.95, recall_count: 12 });
24
- const failure = qualityMultiplier({ outcome: 'failure', confidence: 0.3, recall_count: 0 });
25
- expect(success).toBeGreaterThan(failure);
26
- });
27
-
28
- it('rewards proven-ness (recall_count) but saturates', () => {
29
- const never = qualityMultiplier({ outcome: 'success', confidence: 0.8, recall_count: 0 });
30
- const some = qualityMultiplier({ outcome: 'success', confidence: 0.8, recall_count: 5 });
31
- const many = qualityMultiplier({ outcome: 'success', confidence: 0.8, recall_count: 500 });
32
- expect(some).toBeGreaterThan(never);
33
- expect(many).toBeGreaterThan(some);
34
- expect(many - some).toBeLessThan(some - never + 0.2); // diminishing returns
35
- });
36
-
37
- it('clamps within [0.5, 2.5]', () => {
38
- const hi = qualityMultiplier({ outcome: 'success', confidence: 1, recall_count: 100000, severity: 'critical', review_level: 'senior', reviewed_by: 'x', endorsements: 99 });
39
- const lo = qualityMultiplier({ outcome: 'failure', confidence: 0, recall_count: 0, severity: 'minor' });
40
- expect(hi).toBeLessThanOrEqual(2.5);
41
- expect(lo).toBeGreaterThanOrEqual(0.5);
42
- });
43
-
44
- it('boosts a human-reviewed lesson above an unreviewed equal', () => {
45
- const base = { outcome: 'success', confidence: 0.8, recall_count: 3 } as const;
46
- const unreviewed = qualityMultiplier({ ...base });
47
- const peer = qualityMultiplier({ ...base, reviewed_by: 'alice', review_level: 'peer', endorsements: 1 });
48
- const senior = qualityMultiplier({ ...base, reviewed_by: 'bob', review_level: 'senior', endorsements: 1 });
49
- expect(peer).toBeGreaterThan(unreviewed);
50
- expect(senior).toBeGreaterThan(peer);
51
- });
52
-
53
- it('rewards additional distinct endorsements but saturates', () => {
54
- const base = { outcome: 'success', confidence: 0.8, recall_count: 3, reviewed_by: 'a', review_level: 'peer' } as const;
55
- const one = qualityMultiplier({ ...base, endorsements: 1 });
56
- const three = qualityMultiplier({ ...base, endorsements: 3 });
57
- const fifty = qualityMultiplier({ ...base, endorsements: 50 });
58
- expect(three).toBeGreaterThan(one);
59
- expect(fifty).toBeGreaterThan(three);
60
- expect(fifty - three).toBeLessThan(three - one + 0.1);
61
- });
62
- });
63
-
64
- describe('extractLessonQuality', () => {
65
- it('parses lesson JSON for lesson keys', () => {
66
- const q = extractLessonQuality({ key: 'cachly:lesson:best:foo', content: '{"outcome":"success"}' });
67
- expect(q?.outcome).toBe('success');
68
- });
69
- it('returns null for non-lesson keys (e.g. context)', () => {
70
- expect(extractLessonQuality({ key: 'cachly:ctx:custom:foo', content: 'plain text' })).toBeNull();
71
- });
72
- it('returns null for corrupt lesson JSON', () => {
73
- expect(extractLessonQuality({ key: 'cachly:lesson:best:foo', content: '{not json' })).toBeNull();
74
- });
75
- });
76
-
77
- describe('rerankByQuality', () => {
78
- it('promotes a proven success over a higher-BM25 failure distractor', () => {
79
- const matches: KeywordMatch[] = [
80
- lessonMatch('symptom-failure', 10, { outcome: 'failure', confidence: 0.2, recall_count: 0 }),
81
- lessonMatch('proven-success', 8, { outcome: 'success', confidence: 0.95, recall_count: 12 }),
82
- ];
83
- const ranked = rerankByQuality(matches);
84
- expect(ranked[0].key).toBe('cachly:lesson:best:proven-success');
85
- });
86
-
87
- it('keeps a strong text match competitive (does not bury it on quality alone)', () => {
88
- const matches: KeywordMatch[] = [
89
- lessonMatch('weak-but-perfect', 1, { outcome: 'success', confidence: 1, recall_count: 100 }),
90
- lessonMatch('strong-text', 100, { outcome: 'partial', confidence: 0.6, recall_count: 0 }),
91
- ];
92
- const ranked = rerankByQuality(matches);
93
- expect(ranked[0].key).toBe('cachly:lesson:best:strong-text');
94
- });
95
-
96
- it('is stable and returns every input', () => {
97
- const matches: KeywordMatch[] = [
98
- lessonMatch('a', 5, { outcome: 'success' }),
99
- lessonMatch('b', 5, { outcome: 'success' }),
100
- ];
101
- const ranked = rerankByQuality(matches);
102
- expect(ranked).toHaveLength(2);
103
- });
104
-
105
- it('handles empty input', () => {
106
- expect(rerankByQuality([])).toEqual([]);
107
- });
108
- });
109
-
110
- describe('Cachly-Bench regression guard', () => {
111
- it('quality rerank never regresses headline metrics vs baseline', async () => {
112
- const r = await runBenchmark();
113
- // Cachly must be >= baseline on the headline metrics (the moat must not hurt).
114
- expect(r.cachly.mrr).toBeGreaterThanOrEqual(r.baseline.mrr - 1e-9);
115
- expect(r.cachly.precisionAt1).toBeGreaterThanOrEqual(r.baseline.precisionAt1 - 1e-9);
116
- expect(r.cachly.ndcgAt5).toBeGreaterThanOrEqual(r.baseline.ndcgAt5 - 1e-9);
117
- });
118
-
119
- it('demonstrates a positive lift on Precision@1 and MRR (the proof)', async () => {
120
- const r = await runBenchmark();
121
- expect(r.lift.precisionAt1).toBeGreaterThan(0);
122
- expect(r.lift.mrr).toBeGreaterThan(0);
123
- });
124
-
125
- it('baseline is already strong (corpus is not rigged to make rerank look good)', async () => {
126
- const r = await runBenchmark();
127
- // A fair benchmark: BM25 alone already gets most queries right.
128
- expect(r.baseline.mrr).toBeGreaterThan(0.7);
129
- });
130
-
131
- it('beats a flat-file memory on the decisive metrics (right answer first)', async () => {
132
- const r = await runBenchmark();
133
- // Even when the flat-file memory is steelmanned (recency tiebreak), cachly puts
134
- // the single best answer first more often — Precision@1 and MRR are what matter
135
- // for an agent that acts on the top result.
136
- expect(r.cachly.precisionAt1).toBeGreaterThanOrEqual(r.flatfile.precisionAt1 - 1e-9);
137
- expect(r.cachly.mrr).toBeGreaterThanOrEqual(r.flatfile.mrr - 1e-9);
138
- // The flat-file simulation is honest: it isn't trivially terrible.
139
- expect(r.flatfile.mrr).toBeGreaterThan(0.5);
140
- });
141
- });
@@ -1,81 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { EventEmitter } from 'node:events';
3
- import { safeJsonParse, withTimeout, normalizeGitPath, scanKeys } from '../utils.js';
4
-
5
- describe('safeJsonParse', () => {
6
- it('parses valid JSON', () => {
7
- expect(safeJsonParse('{"a":1}', {})).toEqual({ a: 1 });
8
- });
9
- it('returns fallback on null/empty/malformed', () => {
10
- expect(safeJsonParse(null, 'fb')).toBe('fb');
11
- expect(safeJsonParse('', 'fb')).toBe('fb');
12
- expect(safeJsonParse('{not json', 'fb')).toBe('fb');
13
- });
14
- });
15
-
16
- describe('withTimeout', () => {
17
- it('resolves with the promise value when it settles in time', async () => {
18
- const out = await withTimeout(Promise.resolve('ok'), 1000, 'fallback');
19
- expect(out).toBe('ok');
20
- });
21
- it('resolves with fallback when the promise is too slow', async () => {
22
- const slow = new Promise<string>((r) => setTimeout(() => r('late'), 100));
23
- const out = await withTimeout(slow, 10, 'fallback');
24
- expect(out).toBe('fallback');
25
- });
26
- it('resolves with fallback when the promise rejects', async () => {
27
- const out = await withTimeout(Promise.reject(new Error('boom')), 1000, 'fallback');
28
- expect(out).toBe('fallback');
29
- });
30
- });
31
-
32
- describe('normalizeGitPath', () => {
33
- it('passes a normal path through', () => {
34
- expect(normalizeGitPath('src/auth/jwt.ts')).toBe('src/auth/jwt.ts');
35
- });
36
- it('resolves brace rename notation to the post-rename path', () => {
37
- expect(normalizeGitPath('src/{old => new}/file.ts')).toBe('src/new/file.ts');
38
- });
39
- it('resolves bare rename notation', () => {
40
- expect(normalizeGitPath('old/path.ts => new/path.ts')).toBe('new/path.ts');
41
- });
42
- it('collapses an empty rename segment cleanly', () => {
43
- // dir/{ => sub}/f.ts → dir/sub/f.ts
44
- expect(normalizeGitPath('dir/{ => sub}/f.ts')).toBe('dir/sub/f.ts');
45
- });
46
- it('trims whitespace', () => {
47
- expect(normalizeGitPath(' src/x.ts ')).toBe('src/x.ts');
48
- });
49
- });
50
-
51
- // Minimal scan-capable mock
52
- class ScanMock {
53
- constructor(private keys: string[], private opts: { hang?: boolean } = {}) {}
54
- scanStream(_o: { match: string; count?: number }) {
55
- const em = new EventEmitter();
56
- if (this.opts.hang) return em; // never emits — exercises the timeout path
57
- setImmediate(() => {
58
- em.emit('data', this.keys);
59
- em.emit('end');
60
- });
61
- return em;
62
- }
63
- }
64
-
65
- describe('scanKeys', () => {
66
- it('collects all matching keys', async () => {
67
- const mock = new ScanMock(['a', 'b', 'c']);
68
- const out = await scanKeys(mock, 'pattern:*');
69
- expect(out).toEqual(['a', 'b', 'c']);
70
- });
71
- it('caps at max keys', async () => {
72
- const mock = new ScanMock(['a', 'b', 'c', 'd', 'e']);
73
- const out = await scanKeys(mock, 'pattern:*', { max: 3 });
74
- expect(out).toHaveLength(3);
75
- });
76
- it('resolves with gathered keys on timeout instead of hanging', async () => {
77
- const mock = new ScanMock([], { hang: true });
78
- const out = await scanKeys(mock, 'pattern:*', { timeoutMs: 20 });
79
- expect(out).toEqual([]);
80
- });
81
- });
package/src/auth.ts DELETED
@@ -1,46 +0,0 @@
1
- import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
2
-
3
- export function jwtExpiryMs(token: string): number | null {
4
- try {
5
- const parts = token.split('.');
6
- if (parts.length !== 3) return null;
7
- const payload = JSON.parse(
8
- Buffer.from(parts[1], 'base64url').toString('utf-8'),
9
- ) as { exp?: number };
10
- return payload.exp ? payload.exp * 1000 : null;
11
- } catch {
12
- return null;
13
- }
14
- }
15
-
16
- export function checkJwt(jwt: string): void {
17
- if (!jwt) {
18
- throw new McpError(
19
- ErrorCode.InvalidRequest,
20
- 'CACHLY_JWT env var not set.\n\nGet your token at https://cachly.dev/setup-ai and add it to your MCP config:\n CACHLY_JWT=<your-token>',
21
- );
22
- }
23
- const expMs = jwtExpiryMs(jwt);
24
- if (expMs !== null && expMs < Date.now()) {
25
- throw new McpError(
26
- ErrorCode.InvalidRequest,
27
- `CACHLY_JWT expired at ${new Date(expMs).toISOString()}.\n\nGet a fresh token at https://cachly.dev/setup-ai and update CACHLY_JWT in your MCP config.`,
28
- );
29
- }
30
- }
31
-
32
- export function handleApiError(status: number, detail: string): never {
33
- if (status === 401) {
34
- throw new McpError(
35
- ErrorCode.InvalidRequest,
36
- `Authentication failed (401): ${detail}\n\nYour CACHLY_JWT may be expired or invalid. Get a fresh token at https://cachly.dev/setup-ai`,
37
- );
38
- }
39
- if (status === 403) {
40
- throw new McpError(
41
- ErrorCode.InvalidRequest,
42
- `Access denied (403): ${detail}\n\nCheck that your CACHLY_JWT belongs to an account with access to this resource.`,
43
- );
44
- }
45
- throw new McpError(ErrorCode.InternalError, `cachly API error ${status}: ${detail}`);
46
- }
@@ -1,263 +0,0 @@
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
-
19
- import { EventEmitter } from 'node:events';
20
- import { keywordSearch, type KeywordMatch } from '../search.js';
21
- import { rerankByQuality } from '../rerank.js';
22
- import { BENCH_LESSONS, BENCH_QUERIES, type BenchLesson } from './fixtures.js';
23
-
24
- const LESSON_PREFIX = 'cachly:lesson:best:';
25
-
26
- // ── Minimal in-memory Redis (only what keywordSearch needs) ────────────────────
27
- class MiniRedis {
28
- private store = new Map<string, string>();
29
-
30
- set(key: string, value: string): void { this.store.set(key, value); }
31
- async get(key: string): Promise<string | null> { return this.store.get(key) ?? null; }
32
-
33
- scanStream(opts: { match: string; count?: number }): EventEmitter {
34
- const emitter = new EventEmitter();
35
- const pattern = '^' + opts.match.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$';
36
- const regex = new RegExp(pattern);
37
- const matches = [...this.store.keys()].filter(k => regex.test(k));
38
- setImmediate(() => { emitter.emit('data', matches); emitter.emit('end'); });
39
- return emitter;
40
- }
41
-
42
- pipeline() {
43
- const cmds: string[] = [];
44
- const store = this.store;
45
- return {
46
- get(key: string) { cmds.push(key); return this; },
47
- async exec(): Promise<Array<[null, string | null]>> {
48
- return cmds.map(k => [null, store.get(k) ?? null] as [null, string | null]);
49
- },
50
- };
51
- }
52
- }
53
-
54
- function indexCorpus(lessons: BenchLesson[]): MiniRedis {
55
- const redis = new MiniRedis();
56
- for (const l of lessons) {
57
- redis.set(`${LESSON_PREFIX}${l.topic}`, JSON.stringify(l));
58
- }
59
- return redis;
60
- }
61
-
62
- // ── IR metrics ─────────────────────────────────────────────────────────────────
63
- function precisionAtK(ranked: string[], relevant: Set<string>, k: number): number {
64
- const top = ranked.slice(0, k);
65
- if (top.length === 0) return 0;
66
- const hits = top.filter(t => relevant.has(t)).length;
67
- return hits / Math.min(k, top.length);
68
- }
69
-
70
- function recallAtK(ranked: string[], relevant: Set<string>, k: number): number {
71
- if (relevant.size === 0) return 0;
72
- const top = ranked.slice(0, k);
73
- const hits = top.filter(t => relevant.has(t)).length;
74
- return hits / relevant.size;
75
- }
76
-
77
- function reciprocalRank(ranked: string[], relevant: Set<string>): number {
78
- for (let i = 0; i < ranked.length; i++) {
79
- if (relevant.has(ranked[i])) return 1 / (i + 1);
80
- }
81
- return 0;
82
- }
83
-
84
- function ndcgAtK(ranked: string[], relevant: Set<string>, k: number): number {
85
- let dcg = 0;
86
- for (let i = 0; i < Math.min(k, ranked.length); i++) {
87
- if (relevant.has(ranked[i])) dcg += 1 / Math.log2(i + 2);
88
- }
89
- let idcg = 0;
90
- for (let i = 0; i < Math.min(k, relevant.size); i++) {
91
- idcg += 1 / Math.log2(i + 2);
92
- }
93
- return idcg === 0 ? 0 : dcg / idcg;
94
- }
95
-
96
- export interface BenchMetrics {
97
- precisionAt1: number;
98
- precisionAt3: number;
99
- recallAt3: number;
100
- mrr: number;
101
- ndcgAt5: number;
102
- }
103
-
104
- export interface BenchResult {
105
- /** Flat-file memory simulation: naive term-overlap, no IDF, no quality signal. */
106
- flatfile: BenchMetrics;
107
- baseline: BenchMetrics;
108
- cachly: BenchMetrics;
109
- /** Relative lift of cachly vs the BM25 baseline. */
110
- lift: BenchMetrics;
111
- /** Relative lift of cachly vs a flat-file memory (the head-to-head that matters). */
112
- liftVsFlatfile: BenchMetrics;
113
- queryCount: number;
114
- corpusSize: number;
115
- }
116
-
117
- /**
118
- * Simulates how a flat-file memory (e.g. an LLM reading a `/memories` directory)
119
- * actually surfaces knowledge: it has NO ranking engine. Practical retrieval is
120
- * naive lexical overlap — count how many distinct query terms appear in the file's
121
- * text — with NO inverse-document-frequency, NO length normalization, and crucially
122
- * NO quality signal (a proven fix and a failed attempt are just text). Ties break by
123
- * recency, mimicking "most recently edited note first". This is a fair, and honestly
124
- * weaker, stand-in for "Claude reads its own memory files".
125
- */
126
- function flatFileRank(lessons: BenchLesson[], query: string): string[] {
127
- const qTerms = new Set(
128
- query.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(t => t.length >= 3),
129
- );
130
- const scored = lessons.map(l => {
131
- const blob = [l.topic, l.what_worked, l.what_failed ?? '', l.context ?? '', (l.tags ?? []).join(' ')]
132
- .join(' ').toLowerCase();
133
- let overlap = 0;
134
- for (const t of qTerms) if (blob.includes(t)) overlap++;
135
- const recency = l.ts ? new Date(l.ts).getTime() : 0;
136
- return { topic: l.topic, overlap, recency };
137
- });
138
- return scored
139
- .filter(s => s.overlap > 0)
140
- .sort((a, b) => (b.overlap - a.overlap) || (b.recency - a.recency))
141
- .map(s => s.topic);
142
- }
143
-
144
- const matchTopic = (m: KeywordMatch): string => m.key.replace(LESSON_PREFIX, '');
145
-
146
- function mean(xs: number[]): number {
147
- return xs.length === 0 ? 0 : xs.reduce((a, b) => a + b, 0) / xs.length;
148
- }
149
-
150
- function aggregate(rows: BenchMetrics[]): BenchMetrics {
151
- return {
152
- precisionAt1: mean(rows.map(r => r.precisionAt1)),
153
- precisionAt3: mean(rows.map(r => r.precisionAt3)),
154
- recallAt3: mean(rows.map(r => r.recallAt3)),
155
- mrr: mean(rows.map(r => r.mrr)),
156
- ndcgAt5: mean(rows.map(r => r.ndcgAt5)),
157
- };
158
- }
159
-
160
- function scoreRanking(ranked: string[], relevant: Set<string>): BenchMetrics {
161
- return {
162
- precisionAt1: precisionAtK(ranked, relevant, 1),
163
- precisionAt3: precisionAtK(ranked, relevant, 3),
164
- recallAt3: recallAtK(ranked, relevant, 3),
165
- mrr: reciprocalRank(ranked, relevant),
166
- ndcgAt5: ndcgAtK(ranked, relevant, 5),
167
- };
168
- }
169
-
170
- /** Run the benchmark over the fixture corpus. Pure-ish: no network, no real Redis. */
171
- export async function runBenchmark(): Promise<BenchResult> {
172
- const redis = indexCorpus(BENCH_LESSONS);
173
- const flatfileRows: BenchMetrics[] = [];
174
- const baselineRows: BenchMetrics[] = [];
175
- const cachlyRows: BenchMetrics[] = [];
176
-
177
- for (const q of BENCH_QUERIES) {
178
- const relevant = new Set(q.relevant);
179
-
180
- // BM25 baseline + cachly share the same candidate set — isolates ranking quality.
181
- const matches = await keywordSearch(redis as never, [`${LESSON_PREFIX}*`], q.query, 10);
182
- const baselineRanked = matches.map(matchTopic);
183
- const cachlyRanked = rerankByQuality(matches).map(matchTopic);
184
-
185
- // Flat-file memory sees the whole corpus but has no ranking engine.
186
- const flatfileRanked = flatFileRank(BENCH_LESSONS, q.query);
187
-
188
- flatfileRows.push(scoreRanking(flatfileRanked, relevant));
189
- baselineRows.push(scoreRanking(baselineRanked, relevant));
190
- cachlyRows.push(scoreRanking(cachlyRanked, relevant));
191
- }
192
-
193
- const flatfile = aggregate(flatfileRows);
194
- const baseline = aggregate(baselineRows);
195
- const cachly = aggregate(cachlyRows);
196
- const liftOf = (b: number, c: number) => (b === 0 ? (c > 0 ? 1 : 0) : (c - b) / b);
197
- const liftBetween = (from: BenchMetrics): BenchMetrics => ({
198
- precisionAt1: liftOf(from.precisionAt1, cachly.precisionAt1),
199
- precisionAt3: liftOf(from.precisionAt3, cachly.precisionAt3),
200
- recallAt3: liftOf(from.recallAt3, cachly.recallAt3),
201
- mrr: liftOf(from.mrr, cachly.mrr),
202
- ndcgAt5: liftOf(from.ndcgAt5, cachly.ndcgAt5),
203
- });
204
-
205
- return {
206
- flatfile, baseline, cachly,
207
- lift: liftBetween(baseline),
208
- liftVsFlatfile: liftBetween(flatfile),
209
- queryCount: BENCH_QUERIES.length,
210
- corpusSize: BENCH_LESSONS.length,
211
- };
212
- }
213
-
214
- // ── Reporting ──────────────────────────────────────────────────────────────────
215
- const pct = (x: number) => `${(x * 100).toFixed(1)}%`;
216
- const signedPct = (x: number) => `${x >= 0 ? '+' : ''}${(x * 100).toFixed(1)}%`;
217
-
218
- export function formatReport(r: BenchResult): string {
219
- const row = (label: string, f: number, b: number, c: number, lf: number) =>
220
- ` ${label.padEnd(12)} ${pct(f).padStart(8)} ${pct(b).padStart(8)} ${pct(c).padStart(8)} ${signedPct(lf).padStart(8)}`;
221
- return [
222
- '',
223
- '🧠 Cachly-Bench — recall quality head-to-head',
224
- '──────────────────────────────────────────────────────────────────────────',
225
- ` corpus: ${r.corpusSize} lessons · queries: ${r.queryCount}`,
226
- ' flat-file = naive term-overlap, no quality signal (≈ LLM reading memory files)',
227
- ' baseline = raw BM25+ keyword ranking',
228
- ' cachly = BM25+ followed by quality-aware reranking',
229
- '',
230
- ` ${'metric'.padEnd(12)} ${'flatfile'.padStart(8)} ${'baseline'.padStart(8)} ${'cachly'.padStart(8)} ${'vs flat'.padStart(8)}`,
231
- row('Precision@1', r.flatfile.precisionAt1, r.baseline.precisionAt1, r.cachly.precisionAt1, r.liftVsFlatfile.precisionAt1),
232
- row('Precision@3', r.flatfile.precisionAt3, r.baseline.precisionAt3, r.cachly.precisionAt3, r.liftVsFlatfile.precisionAt3),
233
- row('Recall@3', r.flatfile.recallAt3, r.baseline.recallAt3, r.cachly.recallAt3, r.liftVsFlatfile.recallAt3),
234
- row('MRR', r.flatfile.mrr, r.baseline.mrr, r.cachly.mrr, r.liftVsFlatfile.mrr),
235
- row('nDCG@5', r.flatfile.ndcgAt5, r.baseline.ndcgAt5, r.cachly.ndcgAt5, r.liftVsFlatfile.ndcgAt5),
236
- '──────────────────────────────────────────────────────────────────────────',
237
- ` vs BM25 baseline : MRR ${signedPct(r.lift.mrr)} · Precision@1 ${signedPct(r.lift.precisionAt1)}`,
238
- ` vs flat-file mem : MRR ${signedPct(r.liftVsFlatfile.mrr)} · Precision@1 ${signedPct(r.liftVsFlatfile.precisionAt1)}`,
239
- '',
240
- ].join('\n');
241
- }
242
-
243
- // ── CLI entry ───────────────────────────────────────────────────────────────────
244
- const isMain = (() => {
245
- try {
246
- return import.meta.url === `file://${process.argv[1]}`
247
- || process.argv[1]?.endsWith('cachly-bench.ts')
248
- || process.argv[1]?.endsWith('cachly-bench.js');
249
- } catch { return false; }
250
- })();
251
-
252
- if (isMain) {
253
- runBenchmark().then(result => {
254
- if (process.argv.includes('--json')) {
255
- process.stdout.write(JSON.stringify(result, null, 2) + '\n');
256
- } else {
257
- process.stdout.write(formatReport(result));
258
- }
259
- }).catch(err => {
260
- process.stderr.write(`bench failed: ${err instanceof Error ? err.stack : String(err)}\n`);
261
- process.exit(1);
262
- });
263
- }