@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,1472 +0,0 @@
1
- /**
2
- * Use-case tests for the Cachly Brain flow.
3
- *
4
- * Tests the core Brain operations end-to-end using an in-memory Redis mock:
5
- * learn_from_attempts → CKG update → recall_best_solution (BM25)
6
- * session_start → lessons summary
7
- * CKG: Bayesian confidence, contradiction detection
8
- * Embeddings: provider detection, config sync
9
- *
10
- * Run: npx vitest run src/__tests__/brain-flow.test.ts
11
- */
12
-
13
- import { describe, it, expect, beforeEach } from 'vitest';
14
- import { EventEmitter } from 'node:events';
15
-
16
- // ── Modules under test ────────────────────────────────────────────────────────
17
- import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge,
18
- ckgUpsertPersonNode, ckgUpsertFileNode } from '../ckg.js';
19
- import type { PersonNode, FileNode } from '../ckg.js';
20
- import { EMBED_PROVIDER, hasEmbedProvider, embedProviderHint, embedConfig, setEmbedJwt } from '../embeddings.js';
21
- import { keywordSearch } from '../search.js';
22
- import { handleBrainTool } from '../handlers/brain.js';
23
- import type { Redis } from 'ioredis';
24
-
25
- // ── In-memory Redis mock ──────────────────────────────────────────────────────
26
-
27
- class MockRedis {
28
- private store = new Map<string, string>();
29
- private lists = new Map<string, string[]>();
30
- private sets = new Map<string, Set<string>>();
31
-
32
- async get(key: string): Promise<string | null> {
33
- return this.store.get(key) ?? null;
34
- }
35
-
36
- async set(key: string, value: string, ...opts: unknown[]): Promise<'OK' | null> {
37
- // Honor a trailing 'NX' flag so first-wins semantics (born_at, first_recall_at) work.
38
- if (opts.includes('NX') && this.store.has(key)) return null;
39
- this.store.set(key, value);
40
- return 'OK';
41
- }
42
-
43
- async incr(key: string): Promise<number> {
44
- const next = parseInt(this.store.get(key) ?? '0', 10) + 1;
45
- this.store.set(key, String(next));
46
- return next;
47
- }
48
-
49
- async rpush(key: string, ...values: string[]): Promise<number> {
50
- const list = this.lists.get(key) ?? [];
51
- list.push(...values);
52
- this.lists.set(key, list);
53
- return list.length;
54
- }
55
-
56
- async lrange(key: string, start: number, stop: number): Promise<string[]> {
57
- const list = this.lists.get(key) ?? [];
58
- const end = stop < 0 ? list.length + stop + 1 : stop + 1;
59
- return list.slice(start < 0 ? list.length + start : start, end);
60
- }
61
-
62
- async ltrim(key: string, start: number, stop: number): Promise<'OK'> {
63
- const list = this.lists.get(key) ?? [];
64
- const end = stop < 0 ? list.length + stop + 1 : stop + 1;
65
- this.lists.set(key, list.slice(start < 0 ? list.length + start : start, end));
66
- return 'OK';
67
- }
68
-
69
- async sadd(key: string, ...members: string[]): Promise<number> {
70
- const s = this.sets.get(key) ?? new Set<string>();
71
- let added = 0;
72
- for (const m of members) { if (!s.has(m)) { s.add(m); added++; } }
73
- this.sets.set(key, s);
74
- return added;
75
- }
76
-
77
- async smembers(key: string): Promise<string[]> {
78
- return [...(this.sets.get(key) ?? [])];
79
- }
80
-
81
- async mget(...keys: string[]): Promise<(string | null)[]> {
82
- return keys.map(k => this.store.get(k) ?? null);
83
- }
84
-
85
- async exists(key: string): Promise<number> {
86
- return this.store.has(key) ? 1 : 0;
87
- }
88
-
89
- async incrbyfloat(key: string, increment: number): Promise<string> {
90
- const cur = parseFloat(this.store.get(key) ?? '0');
91
- const next = cur + increment;
92
- this.store.set(key, String(next));
93
- return String(next);
94
- }
95
-
96
- async expire(_key: string, _ttl: number): Promise<number> { return 1; }
97
-
98
- async del(...keys: string[]): Promise<number> {
99
- let count = 0;
100
- for (const k of keys) { if (this.store.delete(k)) count++; }
101
- return count;
102
- }
103
-
104
- /** Simplified scanStream: returns all matching keys in a single 'data' event. */
105
- scanStream(opts: { match: string; count?: number }): EventEmitter {
106
- const emitter = new EventEmitter();
107
- const pattern = opts.match.replace(/\*/g, '.*').replace(/\?/g, '.');
108
- const regex = new RegExp(`^${pattern}$`);
109
- const matches = [...this.store.keys()].filter(k => regex.test(k));
110
- setImmediate(() => {
111
- emitter.emit('data', matches);
112
- emitter.emit('end');
113
- });
114
- return emitter;
115
- }
116
-
117
- /** Minimal pipeline: batches GET calls, returns [err, value][] */
118
- pipeline() {
119
- const commands: Array<{ cmd: string; key: string }> = [];
120
- const store = this.store;
121
- return {
122
- get(key: string) { commands.push({ cmd: 'get', key }); return this; },
123
- async exec(): Promise<Array<[null, string | null]>> {
124
- const out: Array<[null, string | null]> = [];
125
- for (const c of commands) {
126
- out.push([null, store.get(c.key) ?? null]);
127
- }
128
- return out;
129
- },
130
- };
131
- }
132
-
133
- // Expose raw store for assertions
134
- _getStore() { return this.store; }
135
- _getLists() { return this.lists; }
136
- _getSets() { return this.sets; }
137
- }
138
-
139
- // Lesson schema (mirrors what learn_from_attempts stores)
140
- interface LessonObj {
141
- topic: string;
142
- outcome: 'success' | 'failure' | 'partial';
143
- what_worked: string;
144
- what_failed?: string;
145
- context?: string;
146
- severity?: 'critical' | 'major' | 'minor';
147
- commands?: string[];
148
- tags?: string[];
149
- file_paths?: string[];
150
- recall_count: number;
151
- ts: string;
152
- verified_at?: string;
153
- confidence: number;
154
- audit_trail: Array<{ ts: string; action: string; prev_outcome?: string }>;
155
- version: number;
156
- }
157
-
158
- /** Minimal implementation of the learn_from_attempts Redis storage logic */
159
- async function storeLessonInRedis(
160
- redis: MockRedis,
161
- lesson: Omit<LessonObj, 'recall_count' | 'ts' | 'verified_at' | 'confidence' | 'audit_trail' | 'version'>,
162
- ): Promise<LessonObj> {
163
- const ts = new Date().toISOString();
164
- const existingRaw = await redis.get(`cachly:lesson:best:${lesson.topic}`);
165
- const auditTrail: LessonObj['audit_trail'] = existingRaw
166
- ? [{ ts, action: 'updated', prev_outcome: (JSON.parse(existingRaw) as LessonObj).outcome }]
167
- : [{ ts, action: 'created' }];
168
- const obj: LessonObj = {
169
- ...lesson,
170
- recall_count: 0,
171
- ts,
172
- verified_at: lesson.outcome !== 'failure' ? ts : undefined,
173
- confidence: 1.0,
174
- audit_trail: auditTrail,
175
- version: 3,
176
- };
177
- const serialized = JSON.stringify(obj);
178
- await redis.rpush(`cachly:lessons:${lesson.topic}`, serialized);
179
- if (lesson.outcome === 'success' || lesson.outcome === 'partial' || !existingRaw) {
180
- await redis.set(`cachly:lesson:best:${lesson.topic}`, serialized);
181
- }
182
- return obj;
183
- }
184
-
185
- // ── Tests ─────────────────────────────────────────────────────────────────────
186
-
187
- describe('CKG helpers (ckg.ts)', () => {
188
- let redis: MockRedis;
189
- beforeEach(() => { redis = new MockRedis(); });
190
-
191
- // ── ckgSlug ─────────────────────────────────────────────────────────────────
192
- describe('ckgSlug', () => {
193
- it('lowercases and replaces non-alnum with hyphens', () => {
194
- // trailing '!' becomes '-' which is then stripped by the trailing-hyphen rule
195
- expect(ckgSlug('Deploy API!')).toBe('deploy-api');
196
- });
197
- it('collapses multiple hyphens', () => {
198
- expect(ckgSlug('foo -- bar')).toBe('foo-bar');
199
- });
200
- it('strips leading/trailing hyphens', () => {
201
- expect(ckgSlug('--hello--')).toBe('hello');
202
- });
203
- it('allows colon for namespaced slugs', () => {
204
- expect(ckgSlug('deploy:api')).toBe('deploy:api');
205
- });
206
- it('truncates at 80 chars', () => {
207
- expect(ckgSlug('a'.repeat(100))).toHaveLength(80);
208
- });
209
- });
210
-
211
- // ── extractProblemConcept ────────────────────────────────────────────────────
212
- describe('extractProblemConcept', () => {
213
- it('extracts 2 keywords from text', () => {
214
- const result = extractProblemConcept('docker container fails to start');
215
- expect(result).toBe('docker-container');
216
- });
217
- it('filters short words', () => {
218
- const result = extractProblemConcept('the key issue');
219
- // 'the' is stopword, 'key' is 3 chars (filtered), 'issue' passes
220
- expect(result).toBe('issue');
221
- });
222
- it('returns null for empty/all-stopword text', () => {
223
- expect(extractProblemConcept('')).toBeNull();
224
- });
225
- });
226
-
227
- // ── ckgUpsertNode ────────────────────────────────────────────────────────────
228
- describe('ckgUpsertNode', () => {
229
- it('creates a new node with count=1', async () => {
230
- await ckgUpsertNode(redis as unknown as Redis, 'deploy-api', 'backend', 'concept');
231
- const raw = await redis.get('cachly:ckg:node:deploy-api');
232
- expect(raw).not.toBeNull();
233
- const node = JSON.parse(raw!);
234
- expect(node.id).toBe('deploy-api');
235
- expect(node.domain).toBe('backend');
236
- expect(node.type).toBe('concept');
237
- expect(node.count).toBe(1);
238
- });
239
- it('increments count on repeated upsert', async () => {
240
- await ckgUpsertNode(redis as unknown as Redis, 'deploy-api', 'backend', 'concept');
241
- await ckgUpsertNode(redis as unknown as Redis, 'deploy-api', 'backend', 'concept');
242
- const node = JSON.parse((await redis.get('cachly:ckg:node:deploy-api'))!);
243
- expect(node.count).toBe(2);
244
- });
245
- });
246
-
247
- // ── ckgUpdateEdge ────────────────────────────────────────────────────────────
248
- describe('ckgUpdateEdge', () => {
249
- it('creates edge with Bayesian smoothed confidence', async () => {
250
- await ckgUpdateEdge(redis as unknown as Redis, 'problem-A', 'solved_by', 'solution-B', true);
251
- const edgeKey = 'cachly:ckg:edge:problem-A:solved_by:solution-B';
252
- const edge = JSON.parse((await redis.get(edgeKey))!);
253
- expect(edge.successes).toBe(1);
254
- expect(edge.trials).toBe(1);
255
- // Beta(successes+1, trials+2) = 2/3 ≈ 0.667
256
- expect(edge.confidence).toBeCloseTo(2 / 3, 5);
257
- });
258
- it('increases confidence after multiple successes', async () => {
259
- const r = redis as unknown as Redis;
260
- await ckgUpdateEdge(r, 'A', 'leads_to', 'B', true);
261
- await ckgUpdateEdge(r, 'A', 'leads_to', 'B', true);
262
- await ckgUpdateEdge(r, 'A', 'leads_to', 'B', true);
263
- const edge = JSON.parse((await redis.get('cachly:ckg:edge:A:leads_to:B'))!);
264
- // 3 successes, 3 trials → (3+1)/(3+2) = 0.8
265
- expect(edge.confidence).toBeCloseTo(0.8, 5);
266
- });
267
- it('partial success adds 0.5 to successes', async () => {
268
- const r = redis as unknown as Redis;
269
- await ckgUpdateEdge(r, 'A', 'leads_to', 'B', false, true); // partial
270
- const edge = JSON.parse((await redis.get('cachly:ckg:edge:A:leads_to:B'))!);
271
- expect(edge.successes).toBe(0.5);
272
- // (0.5+1)/(1+2) = 1.5/3 = 0.5
273
- expect(edge.confidence).toBeCloseTo(0.5, 5);
274
- });
275
- it('decreases confidence on failure', async () => {
276
- const r = redis as unknown as Redis;
277
- await ckgUpdateEdge(r, 'A', 'leads_to', 'B', true);
278
- const before = JSON.parse((await redis.get('cachly:ckg:edge:A:leads_to:B'))!).confidence;
279
- await ckgUpdateEdge(r, 'A', 'leads_to', 'B', false);
280
- const after = JSON.parse((await redis.get('cachly:ckg:edge:A:leads_to:B'))!).confidence;
281
- expect(after).toBeLessThan(before);
282
- });
283
- it('indexes edge in from/to index sets', async () => {
284
- const r = redis as unknown as Redis;
285
- await ckgUpdateEdge(r, 'prob', 'solved_by', 'sol', true);
286
- const fromIdx = await redis.smembers('cachly:ckg:idx:from:prob');
287
- const toIdx = await redis.smembers('cachly:ckg:idx:to:sol');
288
- expect(fromIdx).toContain('cachly:ckg:edge:prob:solved_by:sol');
289
- expect(toIdx).toContain('cachly:ckg:edge:prob:solved_by:sol');
290
- });
291
- });
292
- });
293
-
294
- // ── Embeddings (embeddings.ts) ────────────────────────────────────────────────
295
-
296
- describe('Embeddings config (embeddings.ts)', () => {
297
- it('EMBED_PROVIDER is a string', () => {
298
- expect(typeof EMBED_PROVIDER).toBe('string');
299
- });
300
-
301
- it('hasEmbedProvider returns false when no provider keys are set', () => {
302
- // In test environment (no API keys), EMBED_PROVIDER is 'none'
303
- // hasEmbedProvider() depends on env — just verify it returns a boolean
304
- expect(typeof hasEmbedProvider()).toBe('boolean');
305
- });
306
-
307
- it('embedProviderHint returns a non-empty string', () => {
308
- const hint = embedProviderHint();
309
- expect(typeof hint).toBe('string');
310
- expect(hint.length).toBeGreaterThan(0);
311
- });
312
-
313
- it('setEmbedJwt updates embedConfig.jwt', () => {
314
- const prev = embedConfig.jwt;
315
- setEmbedJwt('test-jwt-token-xyz');
316
- expect(embedConfig.jwt).toBe('test-jwt-token-xyz');
317
- // Restore
318
- setEmbedJwt(prev);
319
- });
320
-
321
- it('embedConfig.apiUrl defaults to cachly.dev', () => {
322
- expect(embedConfig.apiUrl).toContain('cachly.dev');
323
- });
324
- });
325
-
326
- // ── Brain Lesson Flow (learn → store → recall) ───────────────────────────────
327
-
328
- describe('Brain lesson flow', () => {
329
- let redis: MockRedis;
330
- beforeEach(() => { redis = new MockRedis(); });
331
-
332
- describe('storeLessonInRedis (learn_from_attempts logic)', () => {
333
- it('stores lesson at cachly:lesson:best:{topic}', async () => {
334
- await storeLessonInRedis(redis, {
335
- topic: 'deploy:api',
336
- outcome: 'success',
337
- what_worked: 'docker compose up -d --build api',
338
- what_failed: 'docker compose up (hangs on SSH timeout)',
339
- severity: 'critical',
340
- commands: ['docker compose up -d --build api'],
341
- tags: ['docker', 'deploy'],
342
- file_paths: [],
343
- });
344
-
345
- const raw = await redis.get('cachly:lesson:best:deploy:api');
346
- expect(raw).not.toBeNull();
347
- const lesson = JSON.parse(raw!) as LessonObj;
348
- expect(lesson.topic).toBe('deploy:api');
349
- expect(lesson.outcome).toBe('success');
350
- expect(lesson.what_worked).toBe('docker compose up -d --build api');
351
- expect(lesson.commands).toContain('docker compose up -d --build api');
352
- expect(lesson.confidence).toBe(1.0);
353
- expect(lesson.version).toBe(3);
354
- });
355
-
356
- it('appends to history list cachly:lessons:{topic}', async () => {
357
- await storeLessonInRedis(redis, {
358
- topic: 'fix:redis-timeout',
359
- outcome: 'success',
360
- what_worked: 'increase timeout to 30s',
361
- severity: 'major',
362
- commands: [],
363
- tags: [],
364
- file_paths: [],
365
- });
366
- await storeLessonInRedis(redis, {
367
- topic: 'fix:redis-timeout',
368
- outcome: 'partial',
369
- what_worked: 'add retry logic',
370
- severity: 'minor',
371
- commands: [],
372
- tags: [],
373
- file_paths: [],
374
- });
375
-
376
- const history = await redis.lrange('cachly:lessons:fix:redis-timeout', 0, -1);
377
- expect(history).toHaveLength(2);
378
- });
379
-
380
- it('failure does not overwrite an existing success lesson', async () => {
381
- await storeLessonInRedis(redis, {
382
- topic: 'infra:wireguard',
383
- outcome: 'success',
384
- what_worked: 'add WireGuard IP to TLS-SAN',
385
- severity: 'critical',
386
- commands: [],
387
- tags: [],
388
- file_paths: [],
389
- });
390
- await storeLessonInRedis(redis, {
391
- topic: 'infra:wireguard',
392
- outcome: 'failure',
393
- what_worked: '',
394
- severity: 'major',
395
- commands: [],
396
- tags: [],
397
- file_paths: [],
398
- });
399
-
400
- const best = JSON.parse((await redis.get('cachly:lesson:best:infra:wireguard'))!) as LessonObj;
401
- // The success lesson must win
402
- expect(best.outcome).toBe('success');
403
- });
404
-
405
- it('audit trail records creation and update', async () => {
406
- await storeLessonInRedis(redis, {
407
- topic: 'debug:clickhouse',
408
- outcome: 'failure',
409
- what_worked: '',
410
- severity: 'major',
411
- commands: [],
412
- tags: [],
413
- file_paths: [],
414
- });
415
- await storeLessonInRedis(redis, {
416
- topic: 'debug:clickhouse',
417
- outcome: 'success',
418
- what_worked: 'use 127.0.0.1 not localhost',
419
- severity: 'major',
420
- commands: [],
421
- tags: [],
422
- file_paths: [],
423
- });
424
-
425
- const best = JSON.parse((await redis.get('cachly:lesson:best:debug:clickhouse'))!) as LessonObj;
426
- expect(best.audit_trail.length).toBe(1);
427
- expect(best.audit_trail[0].action).toBe('updated');
428
- expect(best.audit_trail[0].prev_outcome).toBe('failure');
429
- });
430
- });
431
-
432
- // ── BM25 recall ─────────────────────────────────────────────────────────────
433
-
434
- describe('BM25 recall (recall_best_solution keyword search)', () => {
435
- beforeEach(async () => {
436
- // Seed lessons
437
- const lessons = [
438
- { topic: 'deploy:api', what_worked: 'nohup docker compose up -d --build api' },
439
- { topic: 'infra:clickhouse-ipv6', what_worked: 'use 127.0.0.1 not localhost in ClickHouse healthcheck' },
440
- { topic: 'fix:keycloak-auth', what_worked: 'set redirect_uri to https not http' },
441
- { topic: 'bash:macos-lowercase', what_worked: 'use tr instead of ${var,,} on macOS' },
442
- { topic: 'deploy:web', what_worked: 'rsync --checksum --delete with ssh port 2222' },
443
- ];
444
- for (const l of lessons) {
445
- await storeLessonInRedis(redis, {
446
- ...l,
447
- outcome: 'success',
448
- severity: 'critical',
449
- commands: [],
450
- tags: [],
451
- file_paths: [],
452
- });
453
- }
454
- });
455
-
456
- it('finds deploy lesson by keyword', async () => {
457
- const results = await keywordSearch(
458
- redis as unknown as Redis,
459
- ['cachly:lesson:best:*'],
460
- 'docker deploy api',
461
- 5,
462
- );
463
- expect(results.length).toBeGreaterThan(0);
464
- expect(results[0].content).toContain('docker');
465
- });
466
-
467
- it('finds clickhouse lesson by infrastructure topic', async () => {
468
- const results = await keywordSearch(
469
- redis as unknown as Redis,
470
- ['cachly:lesson:best:*'],
471
- 'clickhouse localhost healthcheck',
472
- 5,
473
- );
474
- expect(results.length).toBeGreaterThan(0);
475
- const topResult = results[0];
476
- expect(topResult.content.toLowerCase()).toContain('clickhouse');
477
- });
478
-
479
- it('ranks more specific matches higher', async () => {
480
- const results = await keywordSearch(
481
- redis as unknown as Redis,
482
- ['cachly:lesson:best:*'],
483
- 'macos bash lowercase variable',
484
- 5,
485
- );
486
- expect(results.length).toBeGreaterThan(0);
487
- // macOS bash lesson should rank highest since it matches all terms
488
- expect(results[0].content.toLowerCase()).toContain('macos');
489
- });
490
-
491
- it('returns empty array for completely unrelated query', async () => {
492
- const results = await keywordSearch(
493
- redis as unknown as Redis,
494
- ['cachly:lesson:best:*'],
495
- 'xyzzy quantum entanglement unobtanium',
496
- 5,
497
- );
498
- // Might return 0 or very low-score results; score should be minimal
499
- // Either empty or score near 0 is acceptable
500
- if (results.length > 0) {
501
- expect(results[0].score).toBeLessThan(0.1);
502
- }
503
- });
504
- });
505
-
506
- // ── session_start context ────────────────────────────────────────────────────
507
-
508
- describe('session_start lesson aggregation', () => {
509
- it('correctly sorts lessons by timestamp (newest first)', async () => {
510
- const topics = ['old:lesson', 'mid:lesson', 'new:lesson'];
511
- for (let i = 0; i < topics.length; i++) {
512
- await storeLessonInRedis(redis, {
513
- topic: topics[i],
514
- outcome: 'success',
515
- what_worked: `fix ${i}`,
516
- severity: 'minor',
517
- commands: [],
518
- tags: [],
519
- file_paths: [],
520
- });
521
- // Tiny delay to ensure different timestamps
522
- await new Promise(r => setTimeout(r, 5));
523
- }
524
-
525
- // Simulate what session_start does: scan all lesson keys, sort by ts descending
526
- const allKeys: string[] = [];
527
- await new Promise<void>((resolve, reject) => {
528
- const stream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
529
- stream.on('data', (batch: string[]) => allKeys.push(...batch));
530
- stream.on('end', resolve);
531
- stream.on('error', reject);
532
- });
533
-
534
- const lessons: LessonObj[] = [];
535
- for (const k of allKeys) {
536
- const raw = await redis.get(k);
537
- if (raw) lessons.push(JSON.parse(raw) as LessonObj);
538
- }
539
- lessons.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime());
540
-
541
- expect(lessons[0].topic).toBe('new:lesson');
542
- expect(lessons[lessons.length - 1].topic).toBe('old:lesson');
543
- });
544
-
545
- it('scanStream returns all stored lesson keys', async () => {
546
- await storeLessonInRedis(redis, { topic: 'alpha', outcome: 'success', what_worked: 'x', severity: 'minor', commands: [], tags: [], file_paths: [] });
547
- await storeLessonInRedis(redis, { topic: 'beta', outcome: 'success', what_worked: 'y', severity: 'minor', commands: [], tags: [], file_paths: [] });
548
- await storeLessonInRedis(redis, { topic: 'gamma', outcome: 'success', what_worked: 'z', severity: 'minor', commands: [], tags: [], file_paths: [] });
549
-
550
- const keys: string[] = [];
551
- await new Promise<void>((resolve, reject) => {
552
- const stream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 100 });
553
- stream.on('data', (batch: string[]) => keys.push(...batch));
554
- stream.on('end', resolve);
555
- stream.on('error', reject);
556
- });
557
-
558
- expect(keys).toHaveLength(3);
559
- expect(keys.some(k => k.includes('alpha'))).toBe(true);
560
- expect(keys.some(k => k.includes('beta'))).toBe(true);
561
- expect(keys.some(k => k.includes('gamma'))).toBe(true);
562
- });
563
- });
564
- });
565
-
566
- // ── End-to-end Brain flow integration ────────────────────────────────────────
567
-
568
- describe('Brain E2E: learn → CKG → recall', () => {
569
- let redis: MockRedis;
570
- beforeEach(() => { redis = new MockRedis(); });
571
-
572
- it('full cycle: store lesson + update CKG + retrieve via BM25', async () => {
573
- const r = redis as unknown as Redis;
574
-
575
- // 1. Learn: store a deploy lesson
576
- await storeLessonInRedis(redis, {
577
- topic: 'deploy:api',
578
- outcome: 'success',
579
- what_worked: 'nohup docker compose up -d --build api avoids SSH timeout',
580
- what_failed: 'plain docker compose up hangs when SSH connection drops',
581
- severity: 'critical',
582
- commands: ['nohup docker compose up -d --build api'],
583
- tags: ['docker', 'deploy', 'ssh'],
584
- file_paths: ['docker-compose.yml'],
585
- });
586
-
587
- // 2. CKG: record the causal connection
588
- await ckgUpsertNode(r, 'deploy-api', 'infra', 'concept');
589
- await ckgUpdateEdge(r, 'ssh-timeout', 'solved_by', 'nohup-docker', true);
590
-
591
- // 3. Recall via BM25
592
- const results = await keywordSearch(
593
- r,
594
- ['cachly:lesson:best:*'],
595
- 'docker compose ssh timeout deploy',
596
- 5,
597
- );
598
-
599
- expect(results.length).toBeGreaterThan(0);
600
- expect(results[0].content).toContain('nohup');
601
-
602
- // 4. Verify CKG state
603
- const edge = JSON.parse((await redis.get('cachly:ckg:edge:ssh-timeout:solved_by:nohup-docker'))!);
604
- expect(edge.confidence).toBeCloseTo(2 / 3, 5); // Beta(1+1, 1+2)
605
-
606
- // 5. Verify lesson persisted correctly
607
- const lesson = JSON.parse((await redis.get('cachly:lesson:best:deploy:api'))!) as LessonObj;
608
- expect(lesson.outcome).toBe('success');
609
- expect(lesson.commands).toContain('nohup docker compose up -d --build api');
610
- });
611
-
612
- it('contradiction: failure after success is detected', async () => {
613
- await storeLessonInRedis(redis, {
614
- topic: 'infra:k3s',
615
- outcome: 'success',
616
- what_worked: 'add WireGuard IP to k3s TLS-SAN',
617
- severity: 'critical',
618
- commands: [],
619
- tags: [],
620
- file_paths: [],
621
- });
622
- await storeLessonInRedis(redis, {
623
- topic: 'infra:k3s',
624
- outcome: 'failure',
625
- what_worked: '',
626
- severity: 'major',
627
- commands: [],
628
- tags: [],
629
- file_paths: [],
630
- });
631
-
632
- // Success lesson should still be the "best" lesson
633
- const best = JSON.parse((await redis.get('cachly:lesson:best:infra:k3s'))!) as LessonObj;
634
- expect(best.outcome).toBe('success');
635
-
636
- // But failure is logged in history
637
- const history = await redis.lrange('cachly:lessons:infra:k3s', 0, -1);
638
- expect(history).toHaveLength(2);
639
- const lastAttempt = JSON.parse(history[1]) as LessonObj;
640
- expect(lastAttempt.outcome).toBe('failure');
641
- });
642
- });
643
-
644
- // ── Extended Brain Flow Scenarios ─────────────────────────────────────────────
645
-
646
- describe('Brain Flow: advanced scenarios', () => {
647
- let redis: MockRedis;
648
- beforeEach(() => { redis = new MockRedis(); });
649
-
650
- describe('recall_count and lesson update flow', () => {
651
- it('partial outcome can later be upgraded to success', async () => {
652
- await storeLessonInRedis(redis, {
653
- topic: 'fix:timeout',
654
- outcome: 'partial',
655
- what_worked: 'increase timeout to 15s',
656
- severity: 'major',
657
- commands: [],
658
- tags: [],
659
- file_paths: [],
660
- });
661
- // Upgrade to success
662
- await storeLessonInRedis(redis, {
663
- topic: 'fix:timeout',
664
- outcome: 'success',
665
- what_worked: 'increase timeout to 30s + add retry logic',
666
- severity: 'major',
667
- commands: ['--timeout 30s', '--retry 3'],
668
- tags: ['reliability'],
669
- file_paths: [],
670
- });
671
-
672
- const best = JSON.parse((await redis.get('cachly:lesson:best:fix:timeout'))!) as LessonObj;
673
- expect(best.outcome).toBe('success');
674
- expect(best.what_worked).toContain('retry logic');
675
-
676
- const history = await redis.lrange('cachly:lessons:fix:timeout', 0, -1);
677
- expect(history).toHaveLength(2);
678
- });
679
-
680
- it('lesson version is always 3', async () => {
681
- await storeLessonInRedis(redis, {
682
- topic: 'deploy:mcp',
683
- outcome: 'success',
684
- what_worked: 'npm publish --access public',
685
- severity: 'minor',
686
- commands: [],
687
- tags: [],
688
- file_paths: [],
689
- });
690
- const lesson = JSON.parse((await redis.get('cachly:lesson:best:deploy:mcp'))!) as LessonObj;
691
- expect(lesson.version).toBe(3);
692
- });
693
-
694
- it('verified_at is set for success/partial outcomes', async () => {
695
- await storeLessonInRedis(redis, {
696
- topic: 'infra:ssl',
697
- outcome: 'success',
698
- what_worked: 'certbot renew',
699
- severity: 'critical',
700
- commands: [],
701
- tags: [],
702
- file_paths: [],
703
- });
704
- const lesson = JSON.parse((await redis.get('cachly:lesson:best:infra:ssl'))!) as LessonObj;
705
- expect(lesson.verified_at).toBeDefined();
706
- expect(new Date(lesson.verified_at!).getTime()).toBeLessThanOrEqual(Date.now());
707
- });
708
-
709
- it('verified_at is NOT set for failure outcomes', async () => {
710
- await storeLessonInRedis(redis, {
711
- topic: 'infra:ssl',
712
- outcome: 'failure',
713
- what_worked: '',
714
- severity: 'critical',
715
- commands: [],
716
- tags: [],
717
- file_paths: [],
718
- });
719
- const lesson = JSON.parse((await redis.get('cachly:lesson:best:infra:ssl'))!) as LessonObj;
720
- expect(lesson.verified_at).toBeUndefined();
721
- });
722
- });
723
-
724
- describe('multi-topic lesson management', () => {
725
- it('different topics do not interfere with each other', async () => {
726
- const topics = [
727
- { topic: 'deploy:api', what_worked: 'docker compose up api' },
728
- { topic: 'deploy:web', what_worked: 'rsync + docker build web' },
729
- { topic: 'deploy:mcp', what_worked: 'npm publish mcp server' },
730
- { topic: 'infra:nginx', what_worked: 'nginx -t && systemctl reload' },
731
- { topic: 'fix:memory', what_worked: 'increase heap to 4G' },
732
- ];
733
-
734
- for (const t of topics) {
735
- await storeLessonInRedis(redis, { ...t, outcome: 'success', severity: 'minor', commands: [], tags: [], file_paths: [] });
736
- }
737
-
738
- for (const t of topics) {
739
- const raw = await redis.get(`cachly:lesson:best:${t.topic}`);
740
- expect(raw, `lesson for ${t.topic} should exist`).not.toBeNull();
741
- const lesson = JSON.parse(raw!) as LessonObj;
742
- expect(lesson.what_worked).toBe(t.what_worked);
743
- }
744
- });
745
-
746
- it('all topics are reachable via scanStream', async () => {
747
- const topics = ['alpha:test', 'beta:test', 'gamma:test', 'delta:test'];
748
- for (const t of topics) {
749
- await storeLessonInRedis(redis, { topic: t, outcome: 'success', what_worked: 'x', severity: 'minor', commands: [], tags: [], file_paths: [] });
750
- }
751
-
752
- const keys: string[] = [];
753
- await new Promise<void>((resolve, reject) => {
754
- const stream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 100 });
755
- stream.on('data', (batch: string[]) => keys.push(...batch));
756
- stream.on('end', resolve);
757
- stream.on('error', reject);
758
- });
759
-
760
- expect(keys).toHaveLength(4);
761
- for (const t of topics) {
762
- expect(keys.some(k => k.endsWith(t))).toBe(true);
763
- }
764
- });
765
-
766
- it('pipeline fetches all lessons in a single batch', async () => {
767
- const topics = ['a:1', 'b:2', 'c:3'];
768
- for (const t of topics) {
769
- await storeLessonInRedis(redis, { topic: t, outcome: 'success', what_worked: 'fix', severity: 'minor', commands: [], tags: [], file_paths: [] });
770
- }
771
-
772
- const keys = topics.map(t => `cachly:lesson:best:${t}`);
773
- const pipeline = redis.pipeline();
774
- for (const k of keys) pipeline.get(k);
775
- const results = await pipeline.exec();
776
-
777
- expect(results).toHaveLength(3);
778
- for (const [err, val] of results) {
779
- expect(err).toBeNull();
780
- expect(val).not.toBeNull();
781
- const lesson = JSON.parse(val as string) as LessonObj;
782
- expect(lesson.outcome).toBe('success');
783
- }
784
- });
785
- });
786
-
787
- describe('BM25 recency boost integration', () => {
788
- it('newer lesson with same content ranks higher than older', async () => {
789
- // Seed two similar lessons — same keywords, different age
790
- const oldTs = new Date(Date.now() - 30 * 86400000).toISOString(); // 30 days ago
791
-
792
- // Store "old" lesson manually with backdated timestamp
793
- const oldLesson: LessonObj = {
794
- topic: 'deploy:old',
795
- outcome: 'success',
796
- what_worked: 'docker compose deploy redis service restart',
797
- what_failed: undefined,
798
- context: undefined,
799
- severity: 'minor',
800
- commands: [],
801
- tags: [],
802
- file_paths: [],
803
- recall_count: 0,
804
- ts: oldTs,
805
- verified_at: oldTs,
806
- confidence: 1.0,
807
- audit_trail: [{ ts: oldTs, action: 'created' }],
808
- version: 3,
809
- };
810
- await redis.rpush('cachly:lessons:deploy:old', JSON.stringify(oldLesson));
811
- await redis.set('cachly:lesson:best:deploy:old', JSON.stringify(oldLesson));
812
-
813
- // Store "new" lesson with current timestamp (similar content, different service)
814
- await storeLessonInRedis(redis, {
815
- topic: 'deploy:new',
816
- outcome: 'success',
817
- what_worked: 'docker compose deploy redis service restart',
818
- severity: 'minor',
819
- commands: [],
820
- tags: [],
821
- file_paths: [],
822
- });
823
-
824
- const results = await keywordSearch(
825
- redis as unknown as Redis,
826
- ['cachly:lesson:best:*'],
827
- 'docker compose redis restart',
828
- 5,
829
- );
830
-
831
- // Both should match; newer should rank higher or equal
832
- expect(results.length).toBe(2);
833
- const newIdx = results.findIndex(r => r.key.includes('deploy:new'));
834
- const oldIdx = results.findIndex(r => r.key.includes('deploy:old'));
835
- expect(newIdx).toBeLessThanOrEqual(oldIdx); // new ≤ old (higher rank = lower index)
836
- });
837
-
838
- it('lesson with matching topic-keyword gets higher BM25 score', async () => {
839
- await storeLessonInRedis(redis, {
840
- topic: 'fix:ssl-certificate',
841
- outcome: 'success',
842
- what_worked: 'renew SSL certificate via certbot on Nginx',
843
- severity: 'critical',
844
- commands: ['certbot renew'],
845
- tags: ['ssl', 'nginx'],
846
- file_paths: [],
847
- });
848
- await storeLessonInRedis(redis, {
849
- topic: 'deploy:api',
850
- outcome: 'success',
851
- what_worked: 'docker compose up -d api',
852
- severity: 'minor',
853
- commands: [],
854
- tags: [],
855
- file_paths: [],
856
- });
857
-
858
- const results = await keywordSearch(
859
- redis as unknown as Redis,
860
- ['cachly:lesson:best:*'],
861
- 'ssl certificate nginx renew',
862
- 5,
863
- );
864
-
865
- expect(results.length).toBeGreaterThan(0);
866
- expect(results[0].content.toLowerCase()).toContain('ssl');
867
- });
868
- });
869
-
870
- describe('CKG multi-edge scenarios', () => {
871
- it('same node can have multiple typed edges to different targets', async () => {
872
- const r = redis as unknown as Redis;
873
- await ckgUpsertNode(r, 'api-service', 'backend', 'concept');
874
- await ckgUpdateEdge(r, 'api-service', 'depends_on', 'postgres', true);
875
- await ckgUpdateEdge(r, 'api-service', 'depends_on', 'redis', true);
876
- await ckgUpdateEdge(r, 'api-service', 'deployed_with', 'docker', true);
877
-
878
- const fromIdx = await redis.smembers('cachly:ckg:idx:from:api-service');
879
- expect(fromIdx).toHaveLength(3);
880
- expect(fromIdx.some(k => k.includes('postgres'))).toBe(true);
881
- expect(fromIdx.some(k => k.includes('redis'))).toBe(true);
882
- expect(fromIdx.some(k => k.includes('docker'))).toBe(true);
883
- });
884
-
885
- it('same edge accumulates confidence across multiple successes', async () => {
886
- const r = redis as unknown as Redis;
887
- for (let i = 0; i < 10; i++) {
888
- await ckgUpdateEdge(r, 'problem', 'solved_by', 'solution', true);
889
- }
890
- const edge = JSON.parse((await redis.get('cachly:ckg:edge:problem:solved_by:solution'))!);
891
- // After 10 successes: (10+1)/(10+2) = 11/12 ≈ 0.9167
892
- expect(edge.confidence).toBeGreaterThan(0.9);
893
- expect(edge.trials).toBe(10);
894
- expect(edge.successes).toBe(10);
895
- });
896
-
897
- it('mixed success/failure converges to true rate', async () => {
898
- const r = redis as unknown as Redis;
899
- // 6 success, 4 failure = 60% actual rate
900
- for (let i = 0; i < 6; i++) await ckgUpdateEdge(r, 'X', 'leads_to', 'Y', true);
901
- for (let i = 0; i < 4; i++) await ckgUpdateEdge(r, 'X', 'leads_to', 'Y', false);
902
- const edge = JSON.parse((await redis.get('cachly:ckg:edge:X:leads_to:Y'))!);
903
- // Beta(6+1, 10+2) = 7/12 ≈ 0.583 — close to 60%
904
- expect(edge.confidence).toBeGreaterThan(0.5);
905
- expect(edge.confidence).toBeLessThan(0.75);
906
- });
907
- });
908
- });
909
-
910
- // ── New feature coverage (0.10.41 / 0.10.42) ─────────────────────────────────
911
- // Trust badges, Proven Laws crystallization, and focus-less predictive warning.
912
- // These exercise the REAL handleBrainTool code path with the MockRedis.
913
-
914
- const noopApiFetch = (async () => null) as unknown as Parameters<typeof handleBrainTool>[3];
915
-
916
- /** Seed a best-solution lesson with an explicit recall_count / authors / pinned. */
917
- async function seedLesson(
918
- redis: MockRedis,
919
- topic: string,
920
- opts: {
921
- outcome?: 'success' | 'failure' | 'partial';
922
- what_worked?: string;
923
- what_failed?: string;
924
- recall_count?: number;
925
- severity?: 'critical' | 'major' | 'minor';
926
- pinned?: boolean;
927
- author?: string;
928
- tags?: string[];
929
- } = {},
930
- ): Promise<void> {
931
- const ts = new Date().toISOString();
932
- const obj = {
933
- topic,
934
- outcome: opts.outcome ?? 'success',
935
- what_worked: opts.what_worked ?? `solution for ${topic}`,
936
- what_failed: opts.what_failed,
937
- severity: opts.severity ?? 'minor',
938
- recall_count: opts.recall_count ?? 0,
939
- ts,
940
- verified_at: ts,
941
- confidence: 1.0,
942
- tags: opts.tags ?? [],
943
- ...(opts.pinned ? { pinned: true } : {}),
944
- ...(opts.author ? { author: opts.author } : {}),
945
- };
946
- await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(obj));
947
- }
948
-
949
- describe('Trust badges (recall_best_solution)', () => {
950
- let redis: MockRedis;
951
- const getConn = async () => redis as unknown as Redis;
952
- beforeEach(() => { redis = new MockRedis(); });
953
-
954
- it('shows "Battle-tested" when recall_count crosses 10', async () => {
955
- await seedLesson(redis, 'redis:pool', { recall_count: 9, what_worked: 'set max_retries=3' });
956
- const out = (await handleBrainTool('recall_best_solution', { instance_id: 'i1', topic: 'redis:pool' }, getConn, noopApiFetch))!;
957
- expect(out).toContain('Battle-tested');
958
- expect(out).toContain('recalled 10×');
959
- });
960
-
961
- it('shows "Proven" when recall_count is between 5 and 9', async () => {
962
- await seedLesson(redis, 'api:auth', { recall_count: 4 });
963
- const out = (await handleBrainTool('recall_best_solution', { instance_id: 'i1', topic: 'api:auth' }, getConn, noopApiFetch))!;
964
- expect(out).toContain('Proven');
965
- expect(out).toContain('recalled 5×');
966
- expect(out).not.toContain('Battle-tested');
967
- });
968
-
969
- it('shows no trust badge for a fresh low-recall lesson', async () => {
970
- await seedLesson(redis, 'misc:thing', { recall_count: 0 });
971
- const out = (await handleBrainTool('recall_best_solution', { instance_id: 'i1', topic: 'misc:thing' }, getConn, noopApiFetch))!;
972
- expect(out).not.toContain('Battle-tested');
973
- expect(out).not.toContain('Proven —');
974
- expect(out).not.toContain('Team-verified');
975
- });
976
-
977
- it('increments recall_count and persists it', async () => {
978
- await seedLesson(redis, 'deploy:x', { recall_count: 2 });
979
- await handleBrainTool('recall_best_solution', { instance_id: 'i1', topic: 'deploy:x' }, getConn, noopApiFetch);
980
- const stored = JSON.parse((await redis.get('cachly:lesson:best:deploy:x'))!);
981
- expect(stored.recall_count).toBe(3);
982
- });
983
- });
984
-
985
- describe('Proven Laws crystallization (session_start)', () => {
986
- let redis: MockRedis;
987
- const getConn = async () => redis as unknown as Redis;
988
- beforeEach(async () => {
989
- redis = new MockRedis();
990
- // Provide a last session so session_start does not attempt git reconstruction.
991
- await redis.set('cachly:session:last', JSON.stringify({ summary: 'prior work', ts: new Date().toISOString(), files_changed: [] }));
992
- });
993
-
994
- it('surfaces lessons recalled >= 5 in the Proven Laws section', async () => {
995
- await seedLesson(redis, 'infra:k3s', { recall_count: 7, what_worked: 'add WireGuard IP to TLS-SAN' });
996
- await seedLesson(redis, 'random:note', { recall_count: 1 });
997
- const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
998
- expect(out).toContain('Proven Laws');
999
- expect(out).toContain('infra:k3s');
1000
- expect(out).toContain('recalled 7×');
1001
- });
1002
-
1003
- it('surfaces an explicitly pinned lesson even with low recall', async () => {
1004
- await seedLesson(redis, 'team:convention', { recall_count: 0, pinned: true, what_worked: 'always snake_case handlers' });
1005
- const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
1006
- expect(out).toContain('Proven Laws');
1007
- expect(out).toContain('team:convention');
1008
- expect(out).toContain('pinned');
1009
- });
1010
-
1011
- it('omits the Proven Laws section when nothing qualifies', async () => {
1012
- await seedLesson(redis, 'small:thing', { recall_count: 1 });
1013
- const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
1014
- expect(out).not.toContain('Proven Laws');
1015
- });
1016
-
1017
- it('does not crystallize failure lessons', async () => {
1018
- await seedLesson(redis, 'broken:flow', { recall_count: 9, outcome: 'failure', what_failed: 'never works' });
1019
- const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
1020
- expect(out).not.toContain('Proven Laws');
1021
- });
1022
- });
1023
-
1024
- describe('Predictive pre-warning without explicit focus (session_start)', () => {
1025
- let redis: MockRedis;
1026
- const getConn = async () => redis as unknown as Redis;
1027
- beforeEach(() => { redis = new MockRedis(); });
1028
-
1029
- it('derives danger area from last session files and warns', async () => {
1030
- // Last session touched payment files → likely to continue there.
1031
- await redis.set('cachly:session:last', JSON.stringify({
1032
- summary: 'worked on payment integration',
1033
- ts: new Date().toISOString(),
1034
- files_changed: ['api/payment/stripe_webhook.go'],
1035
- }));
1036
- // A known failure in the payment area.
1037
- await seedLesson(redis, 'payment:webhook', { outcome: 'failure', what_failed: 'missed idempotency key → double charge', tags: ['payment'] });
1038
- const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
1039
- expect(out).toContain('PRE-WARNING');
1040
- expect(out).toContain('payment:webhook');
1041
- });
1042
-
1043
- it('does not warn when last-session area has no known failures', async () => {
1044
- await redis.set('cachly:session:last', JSON.stringify({
1045
- summary: 'wrote docs',
1046
- ts: new Date().toISOString(),
1047
- files_changed: ['docs/readme.md'],
1048
- }));
1049
- await seedLesson(redis, 'payment:webhook', { outcome: 'failure', what_failed: 'x', tags: ['payment'] });
1050
- const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
1051
- expect(out).not.toContain('PRE-WARNING');
1052
- });
1053
-
1054
- it('filters path noise so common segments do not trigger false matches', async () => {
1055
- // Last session only touched generic noise paths (src/lib/index).
1056
- await redis.set('cachly:session:last', JSON.stringify({
1057
- summary: 'refactor',
1058
- ts: new Date().toISOString(),
1059
- files_changed: ['src/lib/index.ts'],
1060
- }));
1061
- // A failure tagged 'src' — should NOT match because 'src' is noise.
1062
- await seedLesson(redis, 'src:thing', { outcome: 'failure', what_failed: 'noise', tags: ['src'] });
1063
- const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
1064
- expect(out).not.toContain('PRE-WARNING');
1065
- });
1066
- });
1067
-
1068
- describe('Phase 3A: Person + File nodes in CKG (ckg.ts)', () => {
1069
- let redis: MockRedis;
1070
- beforeEach(() => { redis = new MockRedis(); });
1071
-
1072
- it('ckgUpsertPersonNode creates a person node with correct shape', async () => {
1073
- const id = await ckgUpsertPersonNode(redis as unknown as Redis, 'alice', 'fix');
1074
- expect(id).toBe('person:alice');
1075
- const raw = await redis.get('cachly:ckg:node:person:alice');
1076
- expect(raw).not.toBeNull();
1077
- const node = JSON.parse(raw!) as PersonNode;
1078
- expect(node.handle).toBe('alice');
1079
- expect(node.type).toBe('person');
1080
- expect(node.count).toBe(1);
1081
- expect(node.domain).toBe('fix');
1082
- expect(typeof node.last_active).toBe('string');
1083
- });
1084
-
1085
- it('ckgUpsertPersonNode increments count on repeated calls', async () => {
1086
- await ckgUpsertPersonNode(redis as unknown as Redis, 'bob', 'deploy');
1087
- await ckgUpsertPersonNode(redis as unknown as Redis, 'bob', 'deploy');
1088
- const raw = await redis.get('cachly:ckg:node:person:bob');
1089
- const node = JSON.parse(raw!) as PersonNode;
1090
- expect(node.count).toBe(2);
1091
- });
1092
-
1093
- it('ckgUpsertFileNode creates a file node with correct shape', async () => {
1094
- const id = await ckgUpsertFileNode(redis as unknown as Redis, 'src/auth/jwt.ts');
1095
- expect(id).toBe('file:src-auth-jwt-ts');
1096
- const raw = await redis.get('cachly:ckg:node:file:src-auth-jwt-ts');
1097
- expect(raw).not.toBeNull();
1098
- const node = JSON.parse(raw!) as FileNode;
1099
- expect(node.path).toBe('src/auth/jwt.ts');
1100
- expect(node.type).toBe('file');
1101
- expect(node.count).toBe(1);
1102
- });
1103
-
1104
- it('ckgUpsertFileNode increments count on repeated calls', async () => {
1105
- await ckgUpsertFileNode(redis as unknown as Redis, 'src/api.ts');
1106
- await ckgUpsertFileNode(redis as unknown as Redis, 'src/api.ts');
1107
- const raw = await redis.get(`cachly:ckg:node:file:${ckgSlug('src/api.ts')}`);
1108
- const node = JSON.parse(raw!) as FileNode;
1109
- expect(node.count).toBe(2);
1110
- });
1111
- });
1112
-
1113
- describe('Phase 3A: brain_who_knows', () => {
1114
- let redis: MockRedis;
1115
- const iid = 'i1';
1116
- const getConn = async () => redis as unknown as Redis;
1117
-
1118
- beforeEach(() => { redis = new MockRedis(); });
1119
-
1120
- it('returns empty-state message when no lessons have author attribution', async () => {
1121
- const out = await handleBrainTool('brain_who_knows', { instance_id: iid, topic: 'deploy:k8s' }, getConn, noopApiFetch);
1122
- expect(out).toContain('No attributed lessons');
1123
- expect(out).toContain('learn_from_attempts');
1124
- });
1125
-
1126
- it('returns ranked contributors after learn_from_attempts with author', async () => {
1127
- // Alice stored 2 successful deploy lessons
1128
- await handleBrainTool('learn_from_attempts', {
1129
- instance_id: iid, topic: 'deploy:k8s', outcome: 'success',
1130
- what_worked: 'kubectl apply with --dry-run first', author: 'alice',
1131
- }, getConn, noopApiFetch);
1132
- await handleBrainTool('learn_from_attempts', {
1133
- instance_id: iid, topic: 'deploy:helm', outcome: 'success',
1134
- what_worked: 'helm upgrade --atomic', author: 'alice',
1135
- }, getConn, noopApiFetch);
1136
- // Bob stored 1 lesson in same domain
1137
- await handleBrainTool('learn_from_attempts', {
1138
- instance_id: iid, topic: 'deploy:docker', outcome: 'success',
1139
- what_worked: 'multi-stage build', author: 'bob',
1140
- }, getConn, noopApiFetch);
1141
-
1142
- const out = await handleBrainTool('brain_who_knows', { instance_id: iid, topic: 'deploy' }, getConn, noopApiFetch);
1143
- expect(out).toContain('alice');
1144
- expect(out).toContain('bob');
1145
- expect(out).toContain('🥇');
1146
- });
1147
-
1148
- it('shows the most experienced author first', async () => {
1149
- // alice: 3 lessons, bob: 1 lesson
1150
- for (let i = 0; i < 3; i++) {
1151
- await handleBrainTool('learn_from_attempts', {
1152
- instance_id: iid, topic: `fix:bug${i}`, outcome: 'success',
1153
- what_worked: `solution ${i}`, author: 'alice',
1154
- }, getConn, noopApiFetch);
1155
- }
1156
- await handleBrainTool('learn_from_attempts', {
1157
- instance_id: iid, topic: 'fix:other', outcome: 'success',
1158
- what_worked: 'quick patch', author: 'bob',
1159
- }, getConn, noopApiFetch);
1160
-
1161
- const out = await handleBrainTool('brain_who_knows', { instance_id: iid, topic: 'fix' }, getConn, noopApiFetch);
1162
- // alice should appear before bob
1163
- const aliceIdx = out!.indexOf('alice');
1164
- const bobIdx = out!.indexOf('bob');
1165
- expect(aliceIdx).toBeGreaterThan(-1);
1166
- expect(aliceIdx).toBeLessThan(bobIdx);
1167
- });
1168
- });
1169
-
1170
- describe('Visibility scopes (learn_from_attempts)', () => {
1171
- let redis: MockRedis;
1172
- const iid = 'i1';
1173
- const getConn = async () => redis as unknown as Redis;
1174
-
1175
- beforeEach(() => { redis = new MockRedis(); });
1176
-
1177
- it('stores visibility=private on the lesson object', async () => {
1178
- await handleBrainTool('learn_from_attempts', {
1179
- instance_id: iid, topic: 'personal:note', outcome: 'success',
1180
- what_worked: 'local trick only I know', visibility: 'private',
1181
- }, getConn, noopApiFetch);
1182
- const raw = await redis.get('cachly:lesson:best:personal:note');
1183
- expect(raw).not.toBeNull();
1184
- const lesson = JSON.parse(raw!) as { visibility: string };
1185
- expect(lesson.visibility).toBe('private');
1186
- });
1187
-
1188
- it('defaults to visibility=team when not specified', async () => {
1189
- await handleBrainTool('learn_from_attempts', {
1190
- instance_id: iid, topic: 'team:lesson', outcome: 'success',
1191
- what_worked: 'shared approach',
1192
- }, getConn, noopApiFetch);
1193
- const raw = await redis.get('cachly:lesson:best:team:lesson');
1194
- const lesson = JSON.parse(raw!) as { visibility: string };
1195
- expect(lesson.visibility).toBe('team');
1196
- });
1197
-
1198
- it('private lessons are excluded from smart_recall results', async () => {
1199
- // Store a private lesson with distinctive keyword
1200
- await handleBrainTool('learn_from_attempts', {
1201
- instance_id: iid, topic: 'secret:api-key', outcome: 'success',
1202
- what_worked: 'xyzzy-unique-private-token', visibility: 'private',
1203
- }, getConn, noopApiFetch);
1204
- // Store a public lesson for the same query
1205
- await handleBrainTool('learn_from_attempts', {
1206
- instance_id: iid, topic: 'public:api-key', outcome: 'success',
1207
- what_worked: 'use env vars for xyzzy tokens', visibility: 'team',
1208
- }, getConn, noopApiFetch);
1209
-
1210
- const out = await handleBrainTool('smart_recall', {
1211
- instance_id: iid, query: 'xyzzy api key',
1212
- }, getConn, noopApiFetch);
1213
- expect(out).toContain('public:api-key');
1214
- expect(out).not.toContain('secret:api-key');
1215
- });
1216
- });
1217
-
1218
- describe('Phase 3B: brain_file_map', () => {
1219
- let redis: MockRedis;
1220
- const iid = 'i1';
1221
- const getConn = async () => redis as unknown as Redis;
1222
-
1223
- beforeEach(() => { redis = new MockRedis(); });
1224
-
1225
- it('returns a message for unknown files with no attribution', async () => {
1226
- const out = await handleBrainTool('brain_file_map', {
1227
- instance_id: iid, file_paths: ['src/unknown-file.ts'],
1228
- }, getConn, noopApiFetch);
1229
- expect(out).toContain('src/unknown-file.ts');
1230
- expect(out).toContain('None yet');
1231
- });
1232
-
1233
- it('shows expert after learn_from_attempts with author + file_paths', async () => {
1234
- await handleBrainTool('learn_from_attempts', {
1235
- instance_id: iid, topic: 'fix:payment-bug', outcome: 'success',
1236
- what_worked: 'null check before charge', author: 'carol',
1237
- file_paths: ['src/payments/stripe.ts'],
1238
- }, getConn, noopApiFetch);
1239
-
1240
- const out = await handleBrainTool('brain_file_map', {
1241
- instance_id: iid, file_paths: ['src/payments/stripe.ts'],
1242
- }, getConn, noopApiFetch);
1243
- expect(out).toContain('src/payments/stripe.ts');
1244
- expect(out).toContain('carol');
1245
- });
1246
-
1247
- it('returns error message for empty file_paths', async () => {
1248
- const out = await handleBrainTool('brain_file_map', {
1249
- instance_id: iid, file_paths: [],
1250
- }, getConn, noopApiFetch);
1251
- expect(out).toContain('at least one');
1252
- });
1253
- });
1254
-
1255
- describe('Phase 3B: team_expertise_map', () => {
1256
- let redis: MockRedis;
1257
- const iid = 'i1';
1258
- const getConn = async () => redis as unknown as Redis;
1259
-
1260
- beforeEach(() => { redis = new MockRedis(); });
1261
-
1262
- it('returns empty-state message when no contributors exist', async () => {
1263
- const out = await handleBrainTool('team_expertise_map', { instance_id: iid }, getConn, noopApiFetch);
1264
- expect(out).toContain('No contributors');
1265
- expect(out).toContain('learn_from_attempts');
1266
- });
1267
-
1268
- it('shows all contributors after learn_from_attempts with different authors', async () => {
1269
- await handleBrainTool('learn_from_attempts', {
1270
- instance_id: iid, topic: 'deploy:k8s', outcome: 'success',
1271
- what_worked: 'use rolling update', author: 'alice',
1272
- }, getConn, noopApiFetch);
1273
- await handleBrainTool('learn_from_attempts', {
1274
- instance_id: iid, topic: 'fix:auth', outcome: 'success',
1275
- what_worked: 'check token expiry', author: 'bob',
1276
- }, getConn, noopApiFetch);
1277
-
1278
- const out = await handleBrainTool('team_expertise_map', { instance_id: iid }, getConn, noopApiFetch);
1279
- expect(out).toContain('alice');
1280
- expect(out).toContain('bob');
1281
- expect(out).toContain('Team Expertise Map');
1282
- });
1283
- });
1284
-
1285
- describe('Phase 3C: skill_gaps', () => {
1286
- let redis: MockRedis;
1287
- const iid = 'i1';
1288
- const getConn = async () => redis as unknown as Redis;
1289
-
1290
- beforeEach(() => { redis = new MockRedis(); });
1291
-
1292
- it('returns clean message when no gaps exist', async () => {
1293
- await handleBrainTool('learn_from_attempts', {
1294
- instance_id: iid, topic: 'fix:auth', outcome: 'success',
1295
- what_worked: 'rotate the token', author: 'alice',
1296
- }, getConn, noopApiFetch);
1297
- const out = await handleBrainTool('skill_gaps', { instance_id: iid }, getConn, noopApiFetch);
1298
- expect(out).toContain('No significant knowledge gaps');
1299
- });
1300
-
1301
- it('flags domain with failures and no success lessons', async () => {
1302
- await handleBrainTool('learn_from_attempts', {
1303
- instance_id: iid, topic: 'deploy:prod', outcome: 'failure',
1304
- what_failed: 'oom kill', what_worked: 'tried increasing limits',
1305
- }, getConn, noopApiFetch);
1306
- const out = await handleBrainTool('skill_gaps', { instance_id: iid }, getConn, noopApiFetch);
1307
- expect(out).toContain('deploy');
1308
- expect(out).toContain('unresolved failures');
1309
- });
1310
-
1311
- it('flags domain with no attribution when lessons exist but no author', async () => {
1312
- for (let i = 0; i < 4; i++) {
1313
- await handleBrainTool('learn_from_attempts', {
1314
- instance_id: iid, topic: `infra:node${i}`, outcome: 'success',
1315
- what_worked: `solution ${i}`,
1316
- // no author
1317
- }, getConn, noopApiFetch);
1318
- }
1319
- const out = await handleBrainTool('skill_gaps', { instance_id: iid }, getConn, noopApiFetch);
1320
- expect(out).toContain('infra');
1321
- expect(out).toContain('no attribution');
1322
- });
1323
- });
1324
-
1325
- describe('Phase 3C: brain_coverage', () => {
1326
- let redis: MockRedis;
1327
- const iid = 'i1';
1328
- const getConn = async () => redis as unknown as Redis;
1329
-
1330
- beforeEach(() => { redis = new MockRedis(); });
1331
-
1332
- it('returns a coverage report with score', async () => {
1333
- await handleBrainTool('learn_from_attempts', {
1334
- instance_id: iid, topic: 'fix:bug', outcome: 'success',
1335
- what_worked: 'fixed it', author: 'alice',
1336
- }, getConn, noopApiFetch);
1337
- const out = await handleBrainTool('brain_coverage', { instance_id: iid }, getConn, noopApiFetch);
1338
- expect(out).toContain('Brain Coverage Report');
1339
- expect(out).toContain('Overall score:');
1340
- expect(out).toContain('/100');
1341
- });
1342
-
1343
- it('shows 0/100 score when no lessons exist', async () => {
1344
- const out = await handleBrainTool('brain_coverage', { instance_id: iid }, getConn, noopApiFetch);
1345
- expect(out).toContain('0/100');
1346
- });
1347
-
1348
- it('score increases with more lessons and attribution', async () => {
1349
- const outBefore = await handleBrainTool('brain_coverage', { instance_id: iid }, getConn, noopApiFetch);
1350
- const scoreBefore = parseInt((outBefore!.match(/Overall score: (\d+)/) ?? ['', '0'])[1]!);
1351
-
1352
- for (let i = 0; i < 5; i++) {
1353
- await handleBrainTool('learn_from_attempts', {
1354
- instance_id: iid, topic: `fix:issue${i}`, outcome: 'success',
1355
- what_worked: `solution ${i}`, author: `dev${i % 3}`,
1356
- }, getConn, noopApiFetch);
1357
- }
1358
- const outAfter = await handleBrainTool('brain_coverage', { instance_id: iid }, getConn, noopApiFetch);
1359
- const scoreAfter = parseInt((outAfter!.match(/Overall score: (\d+)/) ?? ['', '0'])[1]!);
1360
- expect(scoreAfter).toBeGreaterThan(scoreBefore);
1361
- });
1362
- });
1363
-
1364
- describe('Phase 3 stability: input guards', () => {
1365
- let redis: MockRedis;
1366
- const iid = 'i1';
1367
- const getConn = async () => redis as unknown as Redis;
1368
-
1369
- beforeEach(() => { redis = new MockRedis(); });
1370
-
1371
- it('brain_who_knows rejects empty topic without crashing', async () => {
1372
- const out = await handleBrainTool('brain_who_knows', { instance_id: iid, topic: '' }, getConn, noopApiFetch);
1373
- expect(out).toContain('requires a non-empty');
1374
- });
1375
-
1376
- it('brain_who_knows rejects undefined topic without crashing', async () => {
1377
- const out = await handleBrainTool('brain_who_knows', { instance_id: iid }, getConn, noopApiFetch);
1378
- expect(out).toContain('requires a non-empty');
1379
- });
1380
-
1381
- it('brain_who_knows clamps an absurd limit', async () => {
1382
- await handleBrainTool('learn_from_attempts', {
1383
- instance_id: iid, topic: 'fix:x', outcome: 'success',
1384
- what_worked: 'patch', author: 'alice',
1385
- }, getConn, noopApiFetch);
1386
- const out = await handleBrainTool('brain_who_knows', { instance_id: iid, topic: 'fix', limit: 99999 }, getConn, noopApiFetch);
1387
- expect(out).toContain('alice');
1388
- });
1389
-
1390
- it('brain_file_map filters out empty path strings', async () => {
1391
- const out = await handleBrainTool('brain_file_map', { instance_id: iid, file_paths: ['', ' ', ''] }, getConn, noopApiFetch);
1392
- expect(out).toContain('Pass at least one file path');
1393
- });
1394
-
1395
- it('brain_file_map handles non-array file_paths gracefully', async () => {
1396
- const out = await handleBrainTool('brain_file_map', { instance_id: iid, file_paths: 'not-an-array' as unknown as string[] }, getConn, noopApiFetch);
1397
- expect(out).toContain('Pass at least one file path');
1398
- });
1399
- });
1400
-
1401
- describe('Phase 3C: brain_metrics — the three decisive metrics', () => {
1402
- let redis: MockRedis;
1403
- const iid = 'i1';
1404
- const getConn = async () => redis as unknown as Redis;
1405
-
1406
- beforeEach(() => { redis = new MockRedis(); });
1407
-
1408
- it('reports "not enough data" before any learning', async () => {
1409
- const out = await handleBrainTool('brain_metrics', { instance_id: iid }, getConn, noopApiFetch);
1410
- expect(out).toContain('Brain Metrics');
1411
- expect(out).toContain('Not enough data');
1412
- });
1413
-
1414
- it('always shows the published recall-lift moat number', async () => {
1415
- const out = await handleBrainTool('brain_metrics', { instance_id: iid }, getConn, noopApiFetch);
1416
- expect(out).toContain('+22.2 % Precision@1');
1417
- });
1418
-
1419
- it('records born_at on first learn (first-wins NX semantics)', async () => {
1420
- await handleBrainTool('learn_from_attempts', {
1421
- instance_id: iid, topic: 'fix:a', outcome: 'success', what_worked: 'x', author: 'alice',
1422
- }, getConn, noopApiFetch);
1423
- const born1 = await redis.get(`cachly:stats:born_at:${iid}`);
1424
- expect(born1).not.toBeNull();
1425
- // Second learn must NOT overwrite born_at
1426
- await handleBrainTool('learn_from_attempts', {
1427
- instance_id: iid, topic: 'fix:b', outcome: 'success', what_worked: 'y', author: 'bob',
1428
- }, getConn, noopApiFetch);
1429
- const born2 = await redis.get(`cachly:stats:born_at:${iid}`);
1430
- expect(born2).toBe(born1);
1431
- });
1432
-
1433
- it('tracks cross-author reuse when one person recalls another\'s lesson', async () => {
1434
- // Alice stores a proven lesson, recalled once so it counts as "proven"
1435
- await handleBrainTool('learn_from_attempts', {
1436
- instance_id: iid, topic: 'fix:race', outcome: 'success',
1437
- what_worked: 'add mutex around xyzzy counter', author: 'alice',
1438
- }, getConn, noopApiFetch);
1439
- // Bob recalls it (twice → first recall marks proven, second counts reuse on a proven lesson)
1440
- await handleBrainTool('smart_recall', { instance_id: iid, query: 'xyzzy counter mutex', author: 'bob' }, getConn, noopApiFetch);
1441
- await handleBrainTool('smart_recall', { instance_id: iid, query: 'xyzzy counter mutex', author: 'bob' }, getConn, noopApiFetch);
1442
-
1443
- const cross = await redis.get(`cachly:stats:cross_author_recalls:${iid}`);
1444
- expect(Number(cross)).toBeGreaterThanOrEqual(1);
1445
-
1446
- const out = await handleBrainTool('brain_metrics', { instance_id: iid }, getConn, noopApiFetch);
1447
- expect(out).toContain('Team-knowledge-reuse');
1448
- expect(out).toContain('distinct reuse relationship');
1449
- });
1450
-
1451
- it('does not count self-recall as cross-author reuse', async () => {
1452
- await handleBrainTool('learn_from_attempts', {
1453
- instance_id: iid, topic: 'fix:solo', outcome: 'success',
1454
- what_worked: 'fixed the wibble myself', author: 'alice',
1455
- }, getConn, noopApiFetch);
1456
- await handleBrainTool('smart_recall', { instance_id: iid, query: 'wibble', author: 'alice' }, getConn, noopApiFetch);
1457
- await handleBrainTool('smart_recall', { instance_id: iid, query: 'wibble', author: 'alice' }, getConn, noopApiFetch);
1458
- const cross = await redis.get(`cachly:stats:cross_author_recalls:${iid}`);
1459
- expect(Number(cross ?? 0)).toBe(0);
1460
- });
1461
-
1462
- it('surfaces the team-reuse banner inline in smart_recall', async () => {
1463
- await handleBrainTool('learn_from_attempts', {
1464
- instance_id: iid, topic: 'fix:plimsoll', outcome: 'success',
1465
- what_worked: 'adjust the plimsoll threshold', author: 'carol',
1466
- }, getConn, noopApiFetch);
1467
- // First recall by dave marks it proven; second recall shows the reuse banner
1468
- await handleBrainTool('smart_recall', { instance_id: iid, query: 'plimsoll threshold', author: 'dave' }, getConn, noopApiFetch);
1469
- const out = await handleBrainTool('smart_recall', { instance_id: iid, query: 'plimsoll threshold', author: 'dave' }, getConn, noopApiFetch);
1470
- expect(out).toContain('Team knowledge reuse');
1471
- });
1472
- });