@cachly-dev/mcp-server 0.10.65 → 0.10.67

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 (277) hide show
  1. package/dist/src/embeddings.js +10 -0
  2. package/dist/src/embeddings.js.map +1 -1
  3. package/dist/src/handlers/brain.js +2 -1
  4. package/dist/src/handlers/brain.js.map +1 -1
  5. package/dist/src/handlers/cache.js +4 -0
  6. package/dist/src/handlers/cache.js.map +1 -1
  7. package/dist/src/handlers/tco.js +1 -0
  8. package/dist/src/handlers/tco.js.map +1 -1
  9. package/dist/src/index.js +2 -2
  10. package/dist/src/index.js.map +1 -1
  11. package/package.json +7 -1
  12. package/server.json +2 -2
  13. package/.env.test.example +0 -23
  14. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -50
  15. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  16. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
  17. package/.github/workflows/eas-build-android.yml +0 -58
  18. package/.github/workflows/eas-update.yml +0 -55
  19. package/.github/workflows/publish.yml +0 -72
  20. package/BENCH.md +0 -113
  21. package/CHANGELOG.md +0 -653
  22. package/Dockerfile +0 -16
  23. package/LAUNCH_TEXTS.md +0 -182
  24. package/PROGRESS.md +0 -141
  25. package/STRATEGY.md +0 -228
  26. package/VISION_10X.md +0 -133
  27. package/apps/travel-chaos-organizer/.env.example +0 -49
  28. package/apps/travel-chaos-organizer/DEPLOY.md +0 -328
  29. package/apps/travel-chaos-organizer/Makefile +0 -57
  30. package/apps/travel-chaos-organizer/README.md +0 -159
  31. package/apps/travel-chaos-organizer/backend/.env.example +0 -10
  32. package/apps/travel-chaos-organizer/backend/Dockerfile +0 -25
  33. package/apps/travel-chaos-organizer/backend/alembic/env.py +0 -56
  34. package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +0 -24
  35. package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
  36. package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +0 -24
  37. package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +0 -24
  38. package/apps/travel-chaos-organizer/backend/alembic.ini +0 -39
  39. package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
  40. package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
  41. package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +0 -81
  42. package/apps/travel-chaos-organizer/backend/app/config.py +0 -66
  43. package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
  44. package/apps/travel-chaos-organizer/backend/app/db/database.py +0 -20
  45. package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +0 -71
  46. package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +0 -31
  47. package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +0 -4
  48. package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +0 -5
  49. package/apps/travel-chaos-organizer/backend/app/limiter.py +0 -4
  50. package/apps/travel-chaos-organizer/backend/app/logging_config.py +0 -25
  51. package/apps/travel-chaos-organizer/backend/app/main.py +0 -77
  52. package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
  53. package/apps/travel-chaos-organizer/backend/app/models/schemas.py +0 -142
  54. package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
  55. package/apps/travel-chaos-organizer/backend/app/routers/admin.py +0 -237
  56. package/apps/travel-chaos-organizer/backend/app/routers/cache.py +0 -13
  57. package/apps/travel-chaos-organizer/backend/app/routers/events.py +0 -29
  58. package/apps/travel-chaos-organizer/backend/app/routers/export.py +0 -122
  59. package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +0 -122
  60. package/apps/travel-chaos-organizer/backend/app/routers/items.py +0 -130
  61. package/apps/travel-chaos-organizer/backend/app/routers/mail.py +0 -108
  62. package/apps/travel-chaos-organizer/backend/app/routers/parse.py +0 -303
  63. package/apps/travel-chaos-organizer/backend/app/routers/payments.py +0 -179
  64. package/apps/travel-chaos-organizer/backend/app/routers/trips.py +0 -134
  65. package/apps/travel-chaos-organizer/backend/app/routers/users.py +0 -169
  66. package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +0 -37
  67. package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
  68. package/apps/travel-chaos-organizer/backend/app/services/cache.py +0 -131
  69. package/apps/travel-chaos-organizer/backend/app/services/drip.py +0 -128
  70. package/apps/travel-chaos-organizer/backend/app/services/email.py +0 -61
  71. package/apps/travel-chaos-organizer/backend/app/services/errors.py +0 -44
  72. package/apps/travel-chaos-organizer/backend/app/services/notifier.py +0 -10
  73. package/apps/travel-chaos-organizer/backend/app/services/ollama.py +0 -101
  74. package/apps/travel-chaos-organizer/backend/app/services/parser.py +0 -22
  75. package/apps/travel-chaos-organizer/backend/app/services/quota.py +0 -85
  76. package/apps/travel-chaos-organizer/backend/app/services/sentry.py +0 -11
  77. package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +0 -35
  78. package/apps/travel-chaos-organizer/backend/pytest.ini +0 -4
  79. package/apps/travel-chaos-organizer/backend/requirements-dev.txt +0 -6
  80. package/apps/travel-chaos-organizer/backend/requirements.txt +0 -25
  81. package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
  82. package/apps/travel-chaos-organizer/backend/tests/conftest.py +0 -223
  83. package/apps/travel-chaos-organizer/backend/tests/test_admin.py +0 -99
  84. package/apps/travel-chaos-organizer/backend/tests/test_cache.py +0 -142
  85. package/apps/travel-chaos-organizer/backend/tests/test_drip.py +0 -32
  86. package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +0 -121
  87. package/apps/travel-chaos-organizer/backend/tests/test_export.py +0 -48
  88. package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +0 -107
  89. package/apps/travel-chaos-organizer/backend/tests/test_items.py +0 -138
  90. package/apps/travel-chaos-organizer/backend/tests/test_mail.py +0 -113
  91. package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +0 -145
  92. package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +0 -98
  93. package/apps/travel-chaos-organizer/backend/tests/test_parser.py +0 -72
  94. package/apps/travel-chaos-organizer/backend/tests/test_payments.py +0 -163
  95. package/apps/travel-chaos-organizer/backend/tests/test_quota.py +0 -46
  96. package/apps/travel-chaos-organizer/backend/tests/test_search.py +0 -67
  97. package/apps/travel-chaos-organizer/backend/tests/test_trips.py +0 -123
  98. package/apps/travel-chaos-organizer/backend/tests/test_users.py +0 -52
  99. package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
  100. package/apps/travel-chaos-organizer/caddy/Caddyfile +0 -27
  101. package/apps/travel-chaos-organizer/docker-compose.node1.yml +0 -31
  102. package/apps/travel-chaos-organizer/docker-compose.override.yml +0 -14
  103. package/apps/travel-chaos-organizer/docker-compose.yml +0 -128
  104. package/apps/travel-chaos-organizer/frontend/.env.example +0 -13
  105. package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +0 -80
  106. package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +0 -166
  107. package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +0 -130
  108. package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +0 -381
  109. package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +0 -346
  110. package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +0 -363
  111. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +0 -34
  112. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +0 -35
  113. package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +0 -91
  114. package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +0 -71
  115. package/apps/travel-chaos-organizer/frontend/app/auth.tsx +0 -29
  116. package/apps/travel-chaos-organizer/frontend/app/index.tsx +0 -16
  117. package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +0 -119
  118. package/apps/travel-chaos-organizer/frontend/app.config.ts +0 -54
  119. package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +0 -66
  120. package/apps/travel-chaos-organizer/frontend/assets/icon.svg +0 -79
  121. package/apps/travel-chaos-organizer/frontend/assets/splash.svg +0 -15
  122. package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +0 -102
  123. package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +0 -107
  124. package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +0 -109
  125. package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +0 -45
  126. package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +0 -19
  127. package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +0 -155
  128. package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +0 -55
  129. package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +0 -80
  130. package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +0 -39
  131. package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +0 -39
  132. package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +0 -70
  133. package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +0 -121
  134. package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +0 -70
  135. package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +0 -73
  136. package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +0 -130
  137. package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +0 -60
  138. package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +0 -42
  139. package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +0 -146
  140. package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +0 -70
  141. package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +0 -44
  142. package/apps/travel-chaos-organizer/frontend/eas.json +0 -22
  143. package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +0 -24
  144. package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +0 -60
  145. package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +0 -112
  146. package/apps/travel-chaos-organizer/frontend/lib/api.ts +0 -223
  147. package/apps/travel-chaos-organizer/frontend/lib/auth.ts +0 -93
  148. package/apps/travel-chaos-organizer/frontend/lib/db.ts +0 -86
  149. package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +0 -54
  150. package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +0 -13
  151. package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +0 -112
  152. package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +0 -145
  153. package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +0 -43
  154. package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +0 -28
  155. package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +0 -44
  156. package/apps/travel-chaos-organizer/frontend/package-lock.json +0 -13221
  157. package/apps/travel-chaos-organizer/frontend/package.json +0 -44
  158. package/apps/travel-chaos-organizer/frontend/tsconfig.json +0 -23
  159. package/apps/travel-chaos-organizer/keycloak/realm-export.json +0 -59
  160. package/apps/travel-chaos-organizer/landing/index.html +0 -773
  161. package/apps/travel-chaos-organizer/scripts/backup.sh +0 -14
  162. package/apps/travel-chaos-organizer/scripts/configure-env.sh +0 -82
  163. package/apps/travel-chaos-organizer/scripts/deploy.sh +0 -54
  164. package/apps/travel-chaos-organizer/scripts/health-check.sh +0 -36
  165. package/apps/travel-chaos-organizer/scripts/migrate.sh +0 -7
  166. package/apps/travel-chaos-organizer/scripts/server-setup.sh +0 -85
  167. package/apps/travel-chaos-organizer/scripts/smoke-test.sh +0 -29
  168. package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +0 -71
  169. package/apps/travel-chaos-organizer/telegram-bot/.env.example +0 -3
  170. package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +0 -6
  171. package/apps/travel-chaos-organizer/telegram-bot/bot.py +0 -504
  172. package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +0 -3
  173. package/dist/src/__tests__/auth.test.d.ts +0 -1
  174. package/dist/src/__tests__/auth.test.js +0 -107
  175. package/dist/src/__tests__/auth.test.js.map +0 -1
  176. package/dist/src/__tests__/brain-flow.test.d.ts +0 -12
  177. package/dist/src/__tests__/brain-flow.test.js +0 -1241
  178. package/dist/src/__tests__/brain-flow.test.js.map +0 -1
  179. package/dist/src/__tests__/confidence.test.d.ts +0 -13
  180. package/dist/src/__tests__/confidence.test.js +0 -233
  181. package/dist/src/__tests__/confidence.test.js.map +0 -1
  182. package/dist/src/__tests__/e2e/api-auth.test.d.ts +0 -7
  183. package/dist/src/__tests__/e2e/api-auth.test.js +0 -162
  184. package/dist/src/__tests__/e2e/api-auth.test.js.map +0 -1
  185. package/dist/src/__tests__/e2e/api-health.test.d.ts +0 -7
  186. package/dist/src/__tests__/e2e/api-health.test.js +0 -95
  187. package/dist/src/__tests__/e2e/api-health.test.js.map +0 -1
  188. package/dist/src/__tests__/e2e/cli.test.d.ts +0 -10
  189. package/dist/src/__tests__/e2e/cli.test.js +0 -204
  190. package/dist/src/__tests__/e2e/cli.test.js.map +0 -1
  191. package/dist/src/__tests__/e2e/config.d.ts +0 -35
  192. package/dist/src/__tests__/e2e/config.js +0 -53
  193. package/dist/src/__tests__/e2e/config.js.map +0 -1
  194. package/dist/src/__tests__/e2e/funnel.test.d.ts +0 -13
  195. package/dist/src/__tests__/e2e/funnel.test.js +0 -90
  196. package/dist/src/__tests__/e2e/funnel.test.js.map +0 -1
  197. package/dist/src/__tests__/e2e/mcp-tools.test.d.ts +0 -7
  198. package/dist/src/__tests__/e2e/mcp-tools.test.js +0 -129
  199. package/dist/src/__tests__/e2e/mcp-tools.test.js.map +0 -1
  200. package/dist/src/__tests__/e2e/provisioning.test.d.ts +0 -15
  201. package/dist/src/__tests__/e2e/provisioning.test.js +0 -123
  202. package/dist/src/__tests__/e2e/provisioning.test.js.map +0 -1
  203. package/dist/src/__tests__/handlers-unit.test.d.ts +0 -12
  204. package/dist/src/__tests__/handlers-unit.test.js +0 -505
  205. package/dist/src/__tests__/handlers-unit.test.js.map +0 -1
  206. package/dist/src/__tests__/namespace.test.d.ts +0 -1
  207. package/dist/src/__tests__/namespace.test.js +0 -114
  208. package/dist/src/__tests__/namespace.test.js.map +0 -1
  209. package/dist/src/__tests__/rerank.test.d.ts +0 -1
  210. package/dist/src/__tests__/rerank.test.js +0 -125
  211. package/dist/src/__tests__/rerank.test.js.map +0 -1
  212. package/dist/src/__tests__/utils.test.d.ts +0 -1
  213. package/dist/src/__tests__/utils.test.js +0 -83
  214. package/dist/src/__tests__/utils.test.js.map +0 -1
  215. package/dist/src/bench/cachly-bench.d.ts +0 -39
  216. package/dist/src/bench/cachly-bench.js +0 -223
  217. package/dist/src/bench/cachly-bench.js.map +0 -1
  218. package/dist/src/bench/fixtures.d.ts +0 -37
  219. package/dist/src/bench/fixtures.js +0 -225
  220. package/dist/src/bench/fixtures.js.map +0 -1
  221. package/dist/src/search.test.d.ts +0 -1
  222. package/dist/src/search.test.js +0 -713
  223. package/dist/src/search.test.js.map +0 -1
  224. package/eslint.config.js +0 -16
  225. package/glama.json +0 -36
  226. package/llms.txt +0 -155
  227. package/packages/telegram-notify/SPLIT_PLAN.md +0 -106
  228. package/packages/telegram-notify/client.ts +0 -88
  229. package/packages/telegram-notify/emojis.json +0 -22
  230. package/packages/telegram-notify/pyproject.toml +0 -17
  231. package/packages/telegram-notify/telegram_notify/__init__.py +0 -3
  232. package/packages/telegram-notify/telegram_notify/client.py +0 -87
  233. package/packages/telegram-notify/telegram_notify/emojis.json +0 -22
  234. package/scripts/e2e-run.sh +0 -160
  235. package/scripts/generate-icons.js +0 -28
  236. package/smithery.yaml +0 -45
  237. package/src/__tests__/auth.test.ts +0 -147
  238. package/src/__tests__/brain-flow.test.ts +0 -1472
  239. package/src/__tests__/confidence.test.ts +0 -285
  240. package/src/__tests__/e2e/api-auth.test.ts +0 -182
  241. package/src/__tests__/e2e/api-health.test.ts +0 -103
  242. package/src/__tests__/e2e/cli.test.ts +0 -228
  243. package/src/__tests__/e2e/config.ts +0 -53
  244. package/src/__tests__/e2e/funnel.test.ts +0 -99
  245. package/src/__tests__/e2e/mcp-tools.test.ts +0 -191
  246. package/src/__tests__/e2e/provisioning.test.ts +0 -128
  247. package/src/__tests__/handlers-unit.test.ts +0 -677
  248. package/src/__tests__/namespace.test.ts +0 -126
  249. package/src/__tests__/rerank.test.ts +0 -141
  250. package/src/__tests__/utils.test.ts +0 -81
  251. package/src/auth.ts +0 -46
  252. package/src/bench/cachly-bench.ts +0 -263
  253. package/src/bench/fixtures.ts +0 -256
  254. package/src/ckg.ts +0 -103
  255. package/src/confidence.ts +0 -38
  256. package/src/embeddings.ts +0 -217
  257. package/src/handlers/advanced.ts +0 -647
  258. package/src/handlers/brain.ts +0 -2622
  259. package/src/handlers/cache.ts +0 -633
  260. package/src/handlers/context.ts +0 -222
  261. package/src/handlers/fedbrain.ts +0 -1232
  262. package/src/handlers/instances.ts +0 -180
  263. package/src/handlers/roadmap.ts +0 -201
  264. package/src/handlers/syndicate.ts +0 -460
  265. package/src/handlers/tco.ts +0 -345
  266. package/src/handlers/team.ts +0 -1249
  267. package/src/index.ts +0 -2815
  268. package/src/namespace.ts +0 -128
  269. package/src/notifier.ts +0 -5
  270. package/src/rerank.ts +0 -148
  271. package/src/search.test.ts +0 -848
  272. package/src/search.ts +0 -1781
  273. package/src/tools.ts +0 -2056
  274. package/src/utils.ts +0 -90
  275. package/test-features.cjs +0 -23
  276. package/tsconfig.json +0 -16
  277. 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
- }