@cachly-dev/mcp-server 0.10.48 → 0.10.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/eas-build-android.yml +58 -0
- package/.github/workflows/eas-update.yml +55 -0
- package/BENCH.md +80 -0
- package/README.md +5 -33
- package/STRATEGY.md +218 -0
- package/apps/travel-chaos-organizer/.env.example +49 -0
- package/apps/travel-chaos-organizer/DEPLOY.md +328 -0
- package/apps/travel-chaos-organizer/Makefile +57 -0
- package/apps/travel-chaos-organizer/README.md +159 -0
- package/apps/travel-chaos-organizer/backend/.env.example +10 -0
- package/apps/travel-chaos-organizer/backend/Dockerfile +25 -0
- package/apps/travel-chaos-organizer/backend/alembic/env.py +56 -0
- package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +24 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +24 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +24 -0
- package/apps/travel-chaos-organizer/backend/alembic.ini +39 -0
- package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +81 -0
- package/apps/travel-chaos-organizer/backend/app/config.py +66 -0
- package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/db/database.py +20 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +71 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +31 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +4 -0
- package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +5 -0
- package/apps/travel-chaos-organizer/backend/app/limiter.py +4 -0
- package/apps/travel-chaos-organizer/backend/app/logging_config.py +25 -0
- package/apps/travel-chaos-organizer/backend/app/main.py +77 -0
- package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/models/schemas.py +142 -0
- package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/routers/admin.py +237 -0
- package/apps/travel-chaos-organizer/backend/app/routers/cache.py +13 -0
- package/apps/travel-chaos-organizer/backend/app/routers/events.py +29 -0
- package/apps/travel-chaos-organizer/backend/app/routers/export.py +122 -0
- package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +122 -0
- package/apps/travel-chaos-organizer/backend/app/routers/items.py +130 -0
- package/apps/travel-chaos-organizer/backend/app/routers/mail.py +108 -0
- package/apps/travel-chaos-organizer/backend/app/routers/parse.py +303 -0
- package/apps/travel-chaos-organizer/backend/app/routers/payments.py +179 -0
- package/apps/travel-chaos-organizer/backend/app/routers/trips.py +134 -0
- package/apps/travel-chaos-organizer/backend/app/routers/users.py +169 -0
- package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +37 -0
- package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/services/cache.py +131 -0
- package/apps/travel-chaos-organizer/backend/app/services/drip.py +128 -0
- package/apps/travel-chaos-organizer/backend/app/services/email.py +61 -0
- package/apps/travel-chaos-organizer/backend/app/services/errors.py +44 -0
- package/apps/travel-chaos-organizer/backend/app/services/notifier.py +10 -0
- package/apps/travel-chaos-organizer/backend/app/services/ollama.py +101 -0
- package/apps/travel-chaos-organizer/backend/app/services/parser.py +22 -0
- package/apps/travel-chaos-organizer/backend/app/services/quota.py +85 -0
- package/apps/travel-chaos-organizer/backend/app/services/sentry.py +11 -0
- package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +35 -0
- package/apps/travel-chaos-organizer/backend/pytest.ini +4 -0
- package/apps/travel-chaos-organizer/backend/requirements-dev.txt +6 -0
- package/apps/travel-chaos-organizer/backend/requirements.txt +25 -0
- package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/tests/conftest.py +223 -0
- package/apps/travel-chaos-organizer/backend/tests/test_admin.py +99 -0
- package/apps/travel-chaos-organizer/backend/tests/test_cache.py +142 -0
- package/apps/travel-chaos-organizer/backend/tests/test_drip.py +32 -0
- package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +121 -0
- package/apps/travel-chaos-organizer/backend/tests/test_export.py +48 -0
- package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +107 -0
- package/apps/travel-chaos-organizer/backend/tests/test_items.py +138 -0
- package/apps/travel-chaos-organizer/backend/tests/test_mail.py +113 -0
- package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +145 -0
- package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +98 -0
- package/apps/travel-chaos-organizer/backend/tests/test_parser.py +72 -0
- package/apps/travel-chaos-organizer/backend/tests/test_payments.py +163 -0
- package/apps/travel-chaos-organizer/backend/tests/test_quota.py +46 -0
- package/apps/travel-chaos-organizer/backend/tests/test_search.py +67 -0
- package/apps/travel-chaos-organizer/backend/tests/test_trips.py +123 -0
- package/apps/travel-chaos-organizer/backend/tests/test_users.py +52 -0
- package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/caddy/Caddyfile +27 -0
- package/apps/travel-chaos-organizer/docker-compose.node1.yml +31 -0
- package/apps/travel-chaos-organizer/docker-compose.override.yml +14 -0
- package/apps/travel-chaos-organizer/docker-compose.yml +128 -0
- package/apps/travel-chaos-organizer/frontend/.env.example +13 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +80 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +166 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +130 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +381 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +346 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +363 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +34 -0
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +35 -0
- package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +91 -0
- package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +71 -0
- package/apps/travel-chaos-organizer/frontend/app/auth.tsx +29 -0
- package/apps/travel-chaos-organizer/frontend/app/index.tsx +16 -0
- package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +119 -0
- package/apps/travel-chaos-organizer/frontend/app.config.ts +54 -0
- package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +66 -0
- package/apps/travel-chaos-organizer/frontend/assets/icon.svg +79 -0
- package/apps/travel-chaos-organizer/frontend/assets/splash.svg +15 -0
- package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +102 -0
- package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +107 -0
- package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +109 -0
- package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +45 -0
- package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +19 -0
- package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +155 -0
- package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +55 -0
- package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +80 -0
- package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +39 -0
- package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +39 -0
- package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +70 -0
- package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +121 -0
- package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +70 -0
- package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +73 -0
- package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +130 -0
- package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +60 -0
- package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +42 -0
- package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +146 -0
- package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +70 -0
- package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +44 -0
- package/apps/travel-chaos-organizer/frontend/eas.json +22 -0
- package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +24 -0
- package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +60 -0
- package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +112 -0
- package/apps/travel-chaos-organizer/frontend/lib/api.ts +223 -0
- package/apps/travel-chaos-organizer/frontend/lib/auth.ts +93 -0
- package/apps/travel-chaos-organizer/frontend/lib/db.ts +86 -0
- package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +54 -0
- package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +13 -0
- package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +112 -0
- package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +145 -0
- package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +43 -0
- package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +28 -0
- package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +44 -0
- package/apps/travel-chaos-organizer/frontend/package-lock.json +13221 -0
- package/apps/travel-chaos-organizer/frontend/package.json +44 -0
- package/apps/travel-chaos-organizer/frontend/tsconfig.json +23 -0
- package/apps/travel-chaos-organizer/keycloak/realm-export.json +59 -0
- package/apps/travel-chaos-organizer/landing/index.html +773 -0
- package/apps/travel-chaos-organizer/scripts/backup.sh +14 -0
- package/apps/travel-chaos-organizer/scripts/configure-env.sh +82 -0
- package/apps/travel-chaos-organizer/scripts/deploy.sh +54 -0
- package/apps/travel-chaos-organizer/scripts/health-check.sh +36 -0
- package/apps/travel-chaos-organizer/scripts/migrate.sh +7 -0
- package/apps/travel-chaos-organizer/scripts/server-setup.sh +85 -0
- package/apps/travel-chaos-organizer/scripts/smoke-test.sh +29 -0
- package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +71 -0
- package/apps/travel-chaos-organizer/telegram-bot/.env.example +3 -0
- package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +6 -0
- package/apps/travel-chaos-organizer/telegram-bot/bot.py +504 -0
- package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +3 -0
- package/dist/packages/telegram-notify/client.d.ts +19 -0
- package/dist/packages/telegram-notify/client.js +75 -0
- package/dist/packages/telegram-notify/client.js.map +1 -0
- package/dist/src/__tests__/auth.test.js.map +1 -0
- package/dist/{__tests__ → src/__tests__}/brain-flow.test.js +141 -0
- package/dist/src/__tests__/brain-flow.test.js.map +1 -0
- package/dist/src/__tests__/confidence.test.js.map +1 -0
- package/dist/src/__tests__/e2e/api-auth.test.js.map +1 -0
- package/dist/src/__tests__/e2e/api-health.test.js.map +1 -0
- package/dist/src/__tests__/e2e/cli.test.js.map +1 -0
- package/dist/src/__tests__/e2e/config.js.map +1 -0
- package/dist/src/__tests__/e2e/funnel.test.js.map +1 -0
- package/dist/src/__tests__/e2e/mcp-tools.test.js.map +1 -0
- package/dist/src/__tests__/e2e/provisioning.test.js.map +1 -0
- package/dist/{__tests__ → src/__tests__}/handlers-unit.test.js +0 -2
- package/dist/src/__tests__/handlers-unit.test.js.map +1 -0
- package/dist/src/__tests__/namespace.test.js.map +1 -0
- package/dist/src/__tests__/rerank.test.js +98 -0
- package/dist/src/__tests__/rerank.test.js.map +1 -0
- package/dist/src/auth.js.map +1 -0
- package/dist/src/bench/cachly-bench.d.ts +35 -0
- package/dist/src/bench/cachly-bench.js +182 -0
- package/dist/src/bench/cachly-bench.js.map +1 -0
- package/dist/src/bench/fixtures.d.ts +34 -0
- package/dist/src/bench/fixtures.js +199 -0
- package/dist/src/bench/fixtures.js.map +1 -0
- package/dist/src/ckg.js.map +1 -0
- package/dist/src/confidence.js.map +1 -0
- package/dist/src/embeddings.js.map +1 -0
- package/dist/src/handlers/advanced.js.map +1 -0
- package/dist/{handlers → src/handlers}/brain.js +261 -87
- package/dist/src/handlers/brain.js.map +1 -0
- package/dist/src/handlers/cache.js.map +1 -0
- package/dist/{handlers → src/handlers}/context.js +9 -4
- package/dist/src/handlers/context.js.map +1 -0
- package/dist/{handlers → src/handlers}/fedbrain.d.ts +6 -0
- package/dist/{handlers → src/handlers}/fedbrain.js +19 -7
- package/dist/src/handlers/fedbrain.js.map +1 -0
- package/dist/src/handlers/instances.js.map +1 -0
- package/dist/{handlers → src/handlers}/roadmap.js +7 -3
- package/dist/src/handlers/roadmap.js.map +1 -0
- package/dist/src/handlers/syndicate.js.map +1 -0
- package/dist/src/handlers/tco.d.ts +16 -0
- package/dist/src/handlers/tco.js +303 -0
- package/dist/src/handlers/tco.js.map +1 -0
- package/dist/src/handlers/team.js.map +1 -0
- package/dist/{index.js → src/index.js} +153 -218
- package/dist/src/index.js.map +1 -0
- package/dist/src/namespace.js.map +1 -0
- package/dist/src/notifier.d.ts +5 -0
- package/dist/src/notifier.js +6 -0
- package/dist/src/notifier.js.map +1 -0
- package/dist/src/rerank.d.ts +50 -0
- package/dist/src/rerank.js +92 -0
- package/dist/src/rerank.js.map +1 -0
- package/dist/src/search.js.map +1 -0
- package/dist/src/search.test.d.ts +1 -0
- package/dist/src/search.test.js.map +1 -0
- package/dist/{tools.d.ts → src/tools.d.ts} +196 -0
- package/dist/{tools.js → src/tools.js} +178 -0
- package/dist/src/tools.js.map +1 -0
- package/dist/src/utils.js.map +1 -0
- package/package.json +4 -3
- package/packages/telegram-notify/SPLIT_PLAN.md +106 -0
- package/packages/telegram-notify/client.ts +88 -0
- package/packages/telegram-notify/emojis.json +22 -0
- package/packages/telegram-notify/pyproject.toml +17 -0
- package/packages/telegram-notify/telegram_notify/__init__.py +3 -0
- package/packages/telegram-notify/telegram_notify/client.py +87 -0
- package/packages/telegram-notify/telegram_notify/emojis.json +22 -0
- package/scripts/generate-icons.js +28 -0
- package/smithery.yaml +21 -8
- package/src/__tests__/brain-flow.test.ts +174 -0
- package/src/__tests__/handlers-unit.test.ts +1 -2
- package/src/__tests__/rerank.test.ts +111 -0
- package/src/bench/cachly-bench.ts +217 -0
- package/src/bench/fixtures.ts +226 -0
- package/src/handlers/brain.ts +276 -92
- package/src/handlers/context.ts +9 -4
- package/src/handlers/fedbrain.ts +21 -7
- package/src/handlers/roadmap.ts +7 -3
- package/src/handlers/tco.ts +347 -0
- package/src/index.ts +139 -220
- package/src/notifier.ts +5 -0
- package/src/rerank.ts +123 -0
- package/src/tools.ts +191 -0
- package/tsconfig.json +2 -3
- package/dist/__tests__/auth.test.js.map +0 -1
- package/dist/__tests__/brain-flow.test.js.map +0 -1
- package/dist/__tests__/brain-predict-failures.test.d.ts +0 -17
- package/dist/__tests__/brain-predict-failures.test.js +0 -339
- package/dist/__tests__/brain-predict-failures.test.js.map +0 -1
- package/dist/__tests__/confidence.test.js.map +0 -1
- package/dist/__tests__/e2e/api-auth.test.js.map +0 -1
- package/dist/__tests__/e2e/api-health.test.js.map +0 -1
- package/dist/__tests__/e2e/cli.test.js.map +0 -1
- package/dist/__tests__/e2e/config.js.map +0 -1
- package/dist/__tests__/e2e/funnel.test.js.map +0 -1
- package/dist/__tests__/e2e/mcp-tools.test.js.map +0 -1
- package/dist/__tests__/e2e/provisioning.test.js.map +0 -1
- package/dist/__tests__/handlers-unit.test.js.map +0 -1
- package/dist/__tests__/namespace.test.js.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/ckg.js.map +0 -1
- package/dist/confidence.js.map +0 -1
- package/dist/embeddings.js.map +0 -1
- package/dist/handlers/advanced.js.map +0 -1
- package/dist/handlers/brain.js.map +0 -1
- package/dist/handlers/cache.js.map +0 -1
- package/dist/handlers/context.js.map +0 -1
- package/dist/handlers/fedbrain.js.map +0 -1
- package/dist/handlers/instances.js.map +0 -1
- package/dist/handlers/roadmap.js.map +0 -1
- package/dist/handlers/syndicate.js.map +0 -1
- package/dist/handlers/team.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/namespace.js.map +0 -1
- package/dist/search.js.map +0 -1
- package/dist/search.test.js.map +0 -1
- package/dist/tools.js.map +0 -1
- package/dist/utils.js.map +0 -1
- package/src/__tests__/brain-predict-failures.test.ts +0 -430
- /package/dist/{__tests__ → src/__tests__}/auth.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/auth.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/brain-flow.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/confidence.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/confidence.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-auth.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/api-health.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/cli.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/config.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/config.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/funnel.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/mcp-tools.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/e2e/provisioning.test.js +0 -0
- /package/dist/{__tests__ → src/__tests__}/handlers-unit.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/namespace.test.d.ts +0 -0
- /package/dist/{__tests__ → src/__tests__}/namespace.test.js +0 -0
- /package/dist/{search.test.d.ts → src/__tests__/rerank.test.d.ts} +0 -0
- /package/dist/{auth.d.ts → src/auth.d.ts} +0 -0
- /package/dist/{auth.js → src/auth.js} +0 -0
- /package/dist/{ckg.d.ts → src/ckg.d.ts} +0 -0
- /package/dist/{ckg.js → src/ckg.js} +0 -0
- /package/dist/{confidence.d.ts → src/confidence.d.ts} +0 -0
- /package/dist/{confidence.js → src/confidence.js} +0 -0
- /package/dist/{embeddings.d.ts → src/embeddings.d.ts} +0 -0
- /package/dist/{embeddings.js → src/embeddings.js} +0 -0
- /package/dist/{handlers → src/handlers}/advanced.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/advanced.js +0 -0
- /package/dist/{handlers → src/handlers}/brain.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/cache.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/cache.js +0 -0
- /package/dist/{handlers → src/handlers}/context.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/instances.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/instances.js +0 -0
- /package/dist/{handlers → src/handlers}/roadmap.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/syndicate.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/syndicate.js +0 -0
- /package/dist/{handlers → src/handlers}/team.d.ts +0 -0
- /package/dist/{handlers → src/handlers}/team.js +0 -0
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
- /package/dist/{namespace.d.ts → src/namespace.d.ts} +0 -0
- /package/dist/{namespace.js → src/namespace.js} +0 -0
- /package/dist/{search.d.ts → src/search.d.ts} +0 -0
- /package/dist/{search.js → src/search.js} +0 -0
- /package/dist/{search.test.js → src/search.test.js} +0 -0
- /package/dist/{utils.d.ts → src/utils.d.ts} +0 -0
- /package/dist/{utils.js → src/utils.js} +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tool_call": "🔧",
|
|
3
|
+
"tool_error": "🔴",
|
|
4
|
+
"cache_hit": "⚡",
|
|
5
|
+
"cache_miss": "💭",
|
|
6
|
+
"auth_ok": "🔑",
|
|
7
|
+
"auth_fail": "🚫",
|
|
8
|
+
"startup": "🚀",
|
|
9
|
+
"waitlist_signup": "📬",
|
|
10
|
+
"parse_file": "📄",
|
|
11
|
+
"parse_text": "📝",
|
|
12
|
+
"parse_url": "🔗",
|
|
13
|
+
"trip_created": "🗺️",
|
|
14
|
+
"trip_deleted": "🗑️",
|
|
15
|
+
"error": "🔴",
|
|
16
|
+
"stripe_payment": "💳",
|
|
17
|
+
"stripe_checkout": "🛒",
|
|
18
|
+
"new_user": "🆕",
|
|
19
|
+
"pdf_export": "📑",
|
|
20
|
+
"trip_search": "🔍",
|
|
21
|
+
"default": "🔔"
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Converts SVG icon assets to PNG for Expo.
|
|
4
|
+
* Run: node scripts/generate-icons.js
|
|
5
|
+
* Requires: npm install -g sharp-cli OR npx sharp-cli
|
|
6
|
+
*/
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const frontend = path.join(__dirname, '..', 'apps', 'travel-chaos-organizer', 'frontend');
|
|
11
|
+
|
|
12
|
+
const conversions = [
|
|
13
|
+
{ input: 'assets/icon.svg', output: 'assets/icon.png', width: 1024, height: 1024 },
|
|
14
|
+
{ input: 'assets/adaptive-icon.svg', output: 'assets/adaptive-icon.png', width: 1024, height: 1024 },
|
|
15
|
+
{ input: 'assets/icon.svg', output: 'assets/splash.png', width: 1284, height: 2778 },
|
|
16
|
+
{ input: 'assets/icon.svg', output: 'assets/favicon.png', width: 32, height: 32 },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
for (const { input, output, width, height } of conversions) {
|
|
20
|
+
const src = path.join(frontend, input);
|
|
21
|
+
const dst = path.join(frontend, output);
|
|
22
|
+
try {
|
|
23
|
+
execSync(`npx sharp-cli --input "${src}" --output "${dst}" resize ${width} ${height}`, { stdio: 'inherit' });
|
|
24
|
+
console.log(`✓ ${output}`);
|
|
25
|
+
} catch {
|
|
26
|
+
console.error(`✗ ${output} — install sharp-cli: npm i -g sharp-cli`);
|
|
27
|
+
}
|
|
28
|
+
}
|
package/smithery.yaml
CHANGED
|
@@ -7,19 +7,15 @@ description: >
|
|
|
7
7
|
BM25+ search in 11 languages. 80 MCP tools. Free tier. GDPR. German servers.
|
|
8
8
|
Zero-config: sign in once via browser, auto-provisions your Brain instance.
|
|
9
9
|
|
|
10
|
-
# Streamable HTTP — hosted on our infrastructure (mcp.cachly.dev).
|
|
11
|
-
# Multi-tenant: per-request CACHLY_JWT + instance ID resolved from
|
|
12
|
-
# Authorization header + X-Cachly-Instance-Id, or Smithery ?config= param.
|
|
13
10
|
startCommand:
|
|
14
|
-
type:
|
|
15
|
-
url: https://mcp.cachly.dev/mcp
|
|
11
|
+
type: stdio
|
|
16
12
|
configSchema:
|
|
17
13
|
type: object
|
|
18
14
|
properties:
|
|
19
15
|
CACHLY_JWT:
|
|
20
16
|
type: string
|
|
21
|
-
title: "API Key"
|
|
22
|
-
description: "
|
|
17
|
+
title: "API Key (optional — sign in via browser on first use)"
|
|
18
|
+
description: "Leave blank for zero-config: on your first tool call the server opens a browser sign-in (OAuth Device Flow) and auto-provisions your Brain instance. Or pre-fill with your API key from cachly.dev/setup-ai."
|
|
23
19
|
examples:
|
|
24
20
|
- "cky_live_abc123..."
|
|
25
21
|
CACHLY_BRAIN_INSTANCE_ID:
|
|
@@ -29,4 +25,21 @@ startCommand:
|
|
|
29
25
|
pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
|
30
26
|
examples:
|
|
31
27
|
- "9d4077aa-bfa2-468b-89cd-0a8d8f3ec483"
|
|
32
|
-
|
|
28
|
+
CACHLY_API_URL:
|
|
29
|
+
type: string
|
|
30
|
+
title: "API URL (optional)"
|
|
31
|
+
description: "Leave blank unless you self-host Cachly."
|
|
32
|
+
default: "https://api.cachly.dev"
|
|
33
|
+
required: []
|
|
34
|
+
commandFunction: |-
|
|
35
|
+
(config) => ({
|
|
36
|
+
command: "npx",
|
|
37
|
+
args: ["-y", "@cachly-dev/mcp-server@latest"],
|
|
38
|
+
env: Object.fromEntries(
|
|
39
|
+
Object.entries({
|
|
40
|
+
CACHLY_JWT: config.CACHLY_JWT,
|
|
41
|
+
CACHLY_BRAIN_INSTANCE_ID: config.CACHLY_BRAIN_INSTANCE_ID,
|
|
42
|
+
CACHLY_API_URL: config.CACHLY_API_URL ?? "https://api.cachly.dev",
|
|
43
|
+
}).filter(([, v]) => v != null && v !== "")
|
|
44
|
+
)
|
|
45
|
+
})
|
|
@@ -17,6 +17,7 @@ import { EventEmitter } from 'node:events';
|
|
|
17
17
|
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge } from '../ckg.js';
|
|
18
18
|
import { EMBED_PROVIDER, hasEmbedProvider, embedProviderHint, embedConfig, setEmbedJwt } from '../embeddings.js';
|
|
19
19
|
import { keywordSearch } from '../search.js';
|
|
20
|
+
import { handleBrainTool } from '../handlers/brain.js';
|
|
20
21
|
import type { Redis } from 'ioredis';
|
|
21
22
|
|
|
22
23
|
// ── In-memory Redis mock ──────────────────────────────────────────────────────
|
|
@@ -67,6 +68,21 @@ class MockRedis {
|
|
|
67
68
|
return [...(this.sets.get(key) ?? [])];
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
async mget(...keys: string[]): Promise<(string | null)[]> {
|
|
72
|
+
return keys.map(k => this.store.get(k) ?? null);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async exists(key: string): Promise<number> {
|
|
76
|
+
return this.store.has(key) ? 1 : 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async incrbyfloat(key: string, increment: number): Promise<string> {
|
|
80
|
+
const cur = parseFloat(this.store.get(key) ?? '0');
|
|
81
|
+
const next = cur + increment;
|
|
82
|
+
this.store.set(key, String(next));
|
|
83
|
+
return String(next);
|
|
84
|
+
}
|
|
85
|
+
|
|
70
86
|
/** Simplified scanStream: returns all matching keys in a single 'data' event. */
|
|
71
87
|
scanStream(opts: { match: string; count?: number }): EventEmitter {
|
|
72
88
|
const emitter = new EventEmitter();
|
|
@@ -872,3 +888,161 @@ describe('Brain Flow: advanced scenarios', () => {
|
|
|
872
888
|
});
|
|
873
889
|
});
|
|
874
890
|
});
|
|
891
|
+
|
|
892
|
+
// ── New feature coverage (0.10.41 / 0.10.42) ─────────────────────────────────
|
|
893
|
+
// Trust badges, Proven Laws crystallization, and focus-less predictive warning.
|
|
894
|
+
// These exercise the REAL handleBrainTool code path with the MockRedis.
|
|
895
|
+
|
|
896
|
+
const noopApiFetch = (async () => null) as unknown as Parameters<typeof handleBrainTool>[3];
|
|
897
|
+
|
|
898
|
+
/** Seed a best-solution lesson with an explicit recall_count / authors / pinned. */
|
|
899
|
+
async function seedLesson(
|
|
900
|
+
redis: MockRedis,
|
|
901
|
+
topic: string,
|
|
902
|
+
opts: {
|
|
903
|
+
outcome?: 'success' | 'failure' | 'partial';
|
|
904
|
+
what_worked?: string;
|
|
905
|
+
what_failed?: string;
|
|
906
|
+
recall_count?: number;
|
|
907
|
+
severity?: 'critical' | 'major' | 'minor';
|
|
908
|
+
pinned?: boolean;
|
|
909
|
+
author?: string;
|
|
910
|
+
tags?: string[];
|
|
911
|
+
} = {},
|
|
912
|
+
): Promise<void> {
|
|
913
|
+
const ts = new Date().toISOString();
|
|
914
|
+
const obj = {
|
|
915
|
+
topic,
|
|
916
|
+
outcome: opts.outcome ?? 'success',
|
|
917
|
+
what_worked: opts.what_worked ?? `solution for ${topic}`,
|
|
918
|
+
what_failed: opts.what_failed,
|
|
919
|
+
severity: opts.severity ?? 'minor',
|
|
920
|
+
recall_count: opts.recall_count ?? 0,
|
|
921
|
+
ts,
|
|
922
|
+
verified_at: ts,
|
|
923
|
+
confidence: 1.0,
|
|
924
|
+
tags: opts.tags ?? [],
|
|
925
|
+
...(opts.pinned ? { pinned: true } : {}),
|
|
926
|
+
...(opts.author ? { author: opts.author } : {}),
|
|
927
|
+
};
|
|
928
|
+
await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(obj));
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
describe('Trust badges (recall_best_solution)', () => {
|
|
932
|
+
let redis: MockRedis;
|
|
933
|
+
const getConn = async () => redis as unknown as Redis;
|
|
934
|
+
beforeEach(() => { redis = new MockRedis(); });
|
|
935
|
+
|
|
936
|
+
it('shows "Battle-tested" when recall_count crosses 10', async () => {
|
|
937
|
+
await seedLesson(redis, 'redis:pool', { recall_count: 9, what_worked: 'set max_retries=3' });
|
|
938
|
+
const out = (await handleBrainTool('recall_best_solution', { instance_id: 'i1', topic: 'redis:pool' }, getConn, noopApiFetch))!;
|
|
939
|
+
expect(out).toContain('Battle-tested');
|
|
940
|
+
expect(out).toContain('recalled 10×');
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
it('shows "Proven" when recall_count is between 5 and 9', async () => {
|
|
944
|
+
await seedLesson(redis, 'api:auth', { recall_count: 4 });
|
|
945
|
+
const out = (await handleBrainTool('recall_best_solution', { instance_id: 'i1', topic: 'api:auth' }, getConn, noopApiFetch))!;
|
|
946
|
+
expect(out).toContain('Proven');
|
|
947
|
+
expect(out).toContain('recalled 5×');
|
|
948
|
+
expect(out).not.toContain('Battle-tested');
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
it('shows no trust badge for a fresh low-recall lesson', async () => {
|
|
952
|
+
await seedLesson(redis, 'misc:thing', { recall_count: 0 });
|
|
953
|
+
const out = (await handleBrainTool('recall_best_solution', { instance_id: 'i1', topic: 'misc:thing' }, getConn, noopApiFetch))!;
|
|
954
|
+
expect(out).not.toContain('Battle-tested');
|
|
955
|
+
expect(out).not.toContain('Proven —');
|
|
956
|
+
expect(out).not.toContain('Team-verified');
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
it('increments recall_count and persists it', async () => {
|
|
960
|
+
await seedLesson(redis, 'deploy:x', { recall_count: 2 });
|
|
961
|
+
await handleBrainTool('recall_best_solution', { instance_id: 'i1', topic: 'deploy:x' }, getConn, noopApiFetch);
|
|
962
|
+
const stored = JSON.parse((await redis.get('cachly:lesson:best:deploy:x'))!);
|
|
963
|
+
expect(stored.recall_count).toBe(3);
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
describe('Proven Laws crystallization (session_start)', () => {
|
|
968
|
+
let redis: MockRedis;
|
|
969
|
+
const getConn = async () => redis as unknown as Redis;
|
|
970
|
+
beforeEach(async () => {
|
|
971
|
+
redis = new MockRedis();
|
|
972
|
+
// Provide a last session so session_start does not attempt git reconstruction.
|
|
973
|
+
await redis.set('cachly:session:last', JSON.stringify({ summary: 'prior work', ts: new Date().toISOString(), files_changed: [] }));
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
it('surfaces lessons recalled >= 5 in the Proven Laws section', async () => {
|
|
977
|
+
await seedLesson(redis, 'infra:k3s', { recall_count: 7, what_worked: 'add WireGuard IP to TLS-SAN' });
|
|
978
|
+
await seedLesson(redis, 'random:note', { recall_count: 1 });
|
|
979
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
980
|
+
expect(out).toContain('Proven Laws');
|
|
981
|
+
expect(out).toContain('infra:k3s');
|
|
982
|
+
expect(out).toContain('recalled 7×');
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
it('surfaces an explicitly pinned lesson even with low recall', async () => {
|
|
986
|
+
await seedLesson(redis, 'team:convention', { recall_count: 0, pinned: true, what_worked: 'always snake_case handlers' });
|
|
987
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
988
|
+
expect(out).toContain('Proven Laws');
|
|
989
|
+
expect(out).toContain('team:convention');
|
|
990
|
+
expect(out).toContain('pinned');
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
it('omits the Proven Laws section when nothing qualifies', async () => {
|
|
994
|
+
await seedLesson(redis, 'small:thing', { recall_count: 1 });
|
|
995
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
996
|
+
expect(out).not.toContain('Proven Laws');
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
it('does not crystallize failure lessons', async () => {
|
|
1000
|
+
await seedLesson(redis, 'broken:flow', { recall_count: 9, outcome: 'failure', what_failed: 'never works' });
|
|
1001
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
1002
|
+
expect(out).not.toContain('Proven Laws');
|
|
1003
|
+
});
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
describe('Predictive pre-warning without explicit focus (session_start)', () => {
|
|
1007
|
+
let redis: MockRedis;
|
|
1008
|
+
const getConn = async () => redis as unknown as Redis;
|
|
1009
|
+
beforeEach(() => { redis = new MockRedis(); });
|
|
1010
|
+
|
|
1011
|
+
it('derives danger area from last session files and warns', async () => {
|
|
1012
|
+
// Last session touched payment files → likely to continue there.
|
|
1013
|
+
await redis.set('cachly:session:last', JSON.stringify({
|
|
1014
|
+
summary: 'worked on payment integration',
|
|
1015
|
+
ts: new Date().toISOString(),
|
|
1016
|
+
files_changed: ['api/payment/stripe_webhook.go'],
|
|
1017
|
+
}));
|
|
1018
|
+
// A known failure in the payment area.
|
|
1019
|
+
await seedLesson(redis, 'payment:webhook', { outcome: 'failure', what_failed: 'missed idempotency key → double charge', tags: ['payment'] });
|
|
1020
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
1021
|
+
expect(out).toContain('PRE-WARNING');
|
|
1022
|
+
expect(out).toContain('payment:webhook');
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
it('does not warn when last-session area has no known failures', async () => {
|
|
1026
|
+
await redis.set('cachly:session:last', JSON.stringify({
|
|
1027
|
+
summary: 'wrote docs',
|
|
1028
|
+
ts: new Date().toISOString(),
|
|
1029
|
+
files_changed: ['docs/readme.md'],
|
|
1030
|
+
}));
|
|
1031
|
+
await seedLesson(redis, 'payment:webhook', { outcome: 'failure', what_failed: 'x', tags: ['payment'] });
|
|
1032
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
1033
|
+
expect(out).not.toContain('PRE-WARNING');
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
it('filters path noise so common segments do not trigger false matches', async () => {
|
|
1037
|
+
// Last session only touched generic noise paths (src/lib/index).
|
|
1038
|
+
await redis.set('cachly:session:last', JSON.stringify({
|
|
1039
|
+
summary: 'refactor',
|
|
1040
|
+
ts: new Date().toISOString(),
|
|
1041
|
+
files_changed: ['src/lib/index.ts'],
|
|
1042
|
+
}));
|
|
1043
|
+
// A failure tagged 'src' — should NOT match because 'src' is noise.
|
|
1044
|
+
await seedLesson(redis, 'src:thing', { outcome: 'failure', what_failed: 'noise', tags: ['src'] });
|
|
1045
|
+
const out = (await handleBrainTool('session_start', { instance_id: 'i1' }, getConn, noopApiFetch))!;
|
|
1046
|
+
expect(out).not.toContain('PRE-WARNING');
|
|
1047
|
+
});
|
|
1048
|
+
});
|
|
@@ -334,8 +334,7 @@ describe('handleRoadmapTool', () => {
|
|
|
334
334
|
// Add and complete one item
|
|
335
335
|
const addResult = await handleRoadmapTool('roadmap_add', { instance_id: INSTANCE, title: 'Finished task', priority: 'low' }, getConnection, apiFetch);
|
|
336
336
|
const idMatch = addResult!.match(/`(rm_[a-z0-9_]+)`/);
|
|
337
|
-
|
|
338
|
-
const id = idMatch[1];
|
|
337
|
+
const id = idMatch![1];
|
|
339
338
|
await handleRoadmapTool('roadmap_update', { instance_id: INSTANCE, id, status: 'done' }, getConnection, apiFetch);
|
|
340
339
|
|
|
341
340
|
const result = await handleRoadmapTool('roadmap_list', { instance_id: INSTANCE, status: 'done' }, getConnection, apiFetch);
|
|
@@ -0,0 +1,111 @@
|
|
|
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' });
|
|
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
|
+
|
|
45
|
+
describe('extractLessonQuality', () => {
|
|
46
|
+
it('parses lesson JSON for lesson keys', () => {
|
|
47
|
+
const q = extractLessonQuality({ key: 'cachly:lesson:best:foo', content: '{"outcome":"success"}' });
|
|
48
|
+
expect(q?.outcome).toBe('success');
|
|
49
|
+
});
|
|
50
|
+
it('returns null for non-lesson keys (e.g. context)', () => {
|
|
51
|
+
expect(extractLessonQuality({ key: 'cachly:ctx:custom:foo', content: 'plain text' })).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
it('returns null for corrupt lesson JSON', () => {
|
|
54
|
+
expect(extractLessonQuality({ key: 'cachly:lesson:best:foo', content: '{not json' })).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('rerankByQuality', () => {
|
|
59
|
+
it('promotes a proven success over a higher-BM25 failure distractor', () => {
|
|
60
|
+
const matches: KeywordMatch[] = [
|
|
61
|
+
lessonMatch('symptom-failure', 10, { outcome: 'failure', confidence: 0.2, recall_count: 0 }),
|
|
62
|
+
lessonMatch('proven-success', 8, { outcome: 'success', confidence: 0.95, recall_count: 12 }),
|
|
63
|
+
];
|
|
64
|
+
const ranked = rerankByQuality(matches);
|
|
65
|
+
expect(ranked[0].key).toBe('cachly:lesson:best:proven-success');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('keeps a strong text match competitive (does not bury it on quality alone)', () => {
|
|
69
|
+
const matches: KeywordMatch[] = [
|
|
70
|
+
lessonMatch('weak-but-perfect', 1, { outcome: 'success', confidence: 1, recall_count: 100 }),
|
|
71
|
+
lessonMatch('strong-text', 100, { outcome: 'partial', confidence: 0.6, recall_count: 0 }),
|
|
72
|
+
];
|
|
73
|
+
const ranked = rerankByQuality(matches);
|
|
74
|
+
expect(ranked[0].key).toBe('cachly:lesson:best:strong-text');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('is stable and returns every input', () => {
|
|
78
|
+
const matches: KeywordMatch[] = [
|
|
79
|
+
lessonMatch('a', 5, { outcome: 'success' }),
|
|
80
|
+
lessonMatch('b', 5, { outcome: 'success' }),
|
|
81
|
+
];
|
|
82
|
+
const ranked = rerankByQuality(matches);
|
|
83
|
+
expect(ranked).toHaveLength(2);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('handles empty input', () => {
|
|
87
|
+
expect(rerankByQuality([])).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('Cachly-Bench regression guard', () => {
|
|
92
|
+
it('quality rerank never regresses headline metrics vs baseline', async () => {
|
|
93
|
+
const r = await runBenchmark();
|
|
94
|
+
// Cachly must be >= baseline on the headline metrics (the moat must not hurt).
|
|
95
|
+
expect(r.cachly.mrr).toBeGreaterThanOrEqual(r.baseline.mrr - 1e-9);
|
|
96
|
+
expect(r.cachly.precisionAt1).toBeGreaterThanOrEqual(r.baseline.precisionAt1 - 1e-9);
|
|
97
|
+
expect(r.cachly.ndcgAt5).toBeGreaterThanOrEqual(r.baseline.ndcgAt5 - 1e-9);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('demonstrates a positive lift on Precision@1 and MRR (the proof)', async () => {
|
|
101
|
+
const r = await runBenchmark();
|
|
102
|
+
expect(r.lift.precisionAt1).toBeGreaterThan(0);
|
|
103
|
+
expect(r.lift.mrr).toBeGreaterThan(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('baseline is already strong (corpus is not rigged to make rerank look good)', async () => {
|
|
107
|
+
const r = await runBenchmark();
|
|
108
|
+
// A fair benchmark: BM25 alone already gets most queries right.
|
|
109
|
+
expect(r.baseline.mrr).toBeGreaterThan(0.7);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
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
|
+
baseline: BenchMetrics;
|
|
106
|
+
cachly: BenchMetrics;
|
|
107
|
+
/** Relative lift per metric, e.g. { mrr: 0.23 } = +23%. */
|
|
108
|
+
lift: BenchMetrics;
|
|
109
|
+
queryCount: number;
|
|
110
|
+
corpusSize: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const matchTopic = (m: KeywordMatch): string => m.key.replace(LESSON_PREFIX, '');
|
|
114
|
+
|
|
115
|
+
function mean(xs: number[]): number {
|
|
116
|
+
return xs.length === 0 ? 0 : xs.reduce((a, b) => a + b, 0) / xs.length;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function aggregate(rows: BenchMetrics[]): BenchMetrics {
|
|
120
|
+
return {
|
|
121
|
+
precisionAt1: mean(rows.map(r => r.precisionAt1)),
|
|
122
|
+
precisionAt3: mean(rows.map(r => r.precisionAt3)),
|
|
123
|
+
recallAt3: mean(rows.map(r => r.recallAt3)),
|
|
124
|
+
mrr: mean(rows.map(r => r.mrr)),
|
|
125
|
+
ndcgAt5: mean(rows.map(r => r.ndcgAt5)),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function scoreRanking(ranked: string[], relevant: Set<string>): BenchMetrics {
|
|
130
|
+
return {
|
|
131
|
+
precisionAt1: precisionAtK(ranked, relevant, 1),
|
|
132
|
+
precisionAt3: precisionAtK(ranked, relevant, 3),
|
|
133
|
+
recallAt3: recallAtK(ranked, relevant, 3),
|
|
134
|
+
mrr: reciprocalRank(ranked, relevant),
|
|
135
|
+
ndcgAt5: ndcgAtK(ranked, relevant, 5),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Run the benchmark over the fixture corpus. Pure-ish: no network, no real Redis. */
|
|
140
|
+
export async function runBenchmark(): Promise<BenchResult> {
|
|
141
|
+
const redis = indexCorpus(BENCH_LESSONS);
|
|
142
|
+
const baselineRows: BenchMetrics[] = [];
|
|
143
|
+
const cachlyRows: BenchMetrics[] = [];
|
|
144
|
+
|
|
145
|
+
for (const q of BENCH_QUERIES) {
|
|
146
|
+
const relevant = new Set(q.relevant);
|
|
147
|
+
|
|
148
|
+
// Same candidate set for both rankers — isolates the ranking contribution.
|
|
149
|
+
const matches = await keywordSearch(redis as never, [`${LESSON_PREFIX}*`], q.query, 10);
|
|
150
|
+
|
|
151
|
+
const baselineRanked = matches.map(matchTopic);
|
|
152
|
+
const cachlyRanked = rerankByQuality(matches).map(matchTopic);
|
|
153
|
+
|
|
154
|
+
baselineRows.push(scoreRanking(baselineRanked, relevant));
|
|
155
|
+
cachlyRows.push(scoreRanking(cachlyRanked, relevant));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const baseline = aggregate(baselineRows);
|
|
159
|
+
const cachly = aggregate(cachlyRows);
|
|
160
|
+
const liftOf = (b: number, c: number) => (b === 0 ? (c > 0 ? 1 : 0) : (c - b) / b);
|
|
161
|
+
const lift: BenchMetrics = {
|
|
162
|
+
precisionAt1: liftOf(baseline.precisionAt1, cachly.precisionAt1),
|
|
163
|
+
precisionAt3: liftOf(baseline.precisionAt3, cachly.precisionAt3),
|
|
164
|
+
recallAt3: liftOf(baseline.recallAt3, cachly.recallAt3),
|
|
165
|
+
mrr: liftOf(baseline.mrr, cachly.mrr),
|
|
166
|
+
ndcgAt5: liftOf(baseline.ndcgAt5, cachly.ndcgAt5),
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return { baseline, cachly, lift, queryCount: BENCH_QUERIES.length, corpusSize: BENCH_LESSONS.length };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Reporting ──────────────────────────────────────────────────────────────────
|
|
173
|
+
const pct = (x: number) => `${(x * 100).toFixed(1)}%`;
|
|
174
|
+
const signedPct = (x: number) => `${x >= 0 ? '+' : ''}${(x * 100).toFixed(1)}%`;
|
|
175
|
+
|
|
176
|
+
export function formatReport(r: BenchResult): string {
|
|
177
|
+
const row = (label: string, b: number, c: number, l: number) =>
|
|
178
|
+
` ${label.padEnd(14)} ${pct(b).padStart(8)} → ${pct(c).padStart(8)} ${signedPct(l).padStart(8)}`;
|
|
179
|
+
return [
|
|
180
|
+
'',
|
|
181
|
+
'🧠 Cachly-Bench — recall quality (BM25 baseline vs quality-aware rerank)',
|
|
182
|
+
'────────────────────────────────────────────────────────────────────────',
|
|
183
|
+
` corpus: ${r.corpusSize} lessons · queries: ${r.queryCount}`,
|
|
184
|
+
'',
|
|
185
|
+
` ${'metric'.padEnd(14)} ${'baseline'.padStart(8)} ${'cachly'.padStart(8)} ${'lift'.padStart(8)}`,
|
|
186
|
+
row('Precision@1', r.baseline.precisionAt1, r.cachly.precisionAt1, r.lift.precisionAt1),
|
|
187
|
+
row('Precision@3', r.baseline.precisionAt3, r.cachly.precisionAt3, r.lift.precisionAt3),
|
|
188
|
+
row('Recall@3', r.baseline.recallAt3, r.cachly.recallAt3, r.lift.recallAt3),
|
|
189
|
+
row('MRR', r.baseline.mrr, r.cachly.mrr, r.lift.mrr),
|
|
190
|
+
row('nDCG@5', r.baseline.ndcgAt5, r.cachly.ndcgAt5, r.lift.ndcgAt5),
|
|
191
|
+
'────────────────────────────────────────────────────────────────────────',
|
|
192
|
+
` Headline: MRR ${signedPct(r.lift.mrr)} · Precision@1 ${signedPct(r.lift.precisionAt1)}`,
|
|
193
|
+
'',
|
|
194
|
+
].join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── CLI entry ───────────────────────────────────────────────────────────────────
|
|
198
|
+
const isMain = (() => {
|
|
199
|
+
try {
|
|
200
|
+
return import.meta.url === `file://${process.argv[1]}`
|
|
201
|
+
|| process.argv[1]?.endsWith('cachly-bench.ts')
|
|
202
|
+
|| process.argv[1]?.endsWith('cachly-bench.js');
|
|
203
|
+
} catch { return false; }
|
|
204
|
+
})();
|
|
205
|
+
|
|
206
|
+
if (isMain) {
|
|
207
|
+
runBenchmark().then(result => {
|
|
208
|
+
if (process.argv.includes('--json')) {
|
|
209
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
210
|
+
} else {
|
|
211
|
+
process.stdout.write(formatReport(result));
|
|
212
|
+
}
|
|
213
|
+
}).catch(err => {
|
|
214
|
+
process.stderr.write(`bench failed: ${err instanceof Error ? err.stack : String(err)}\n`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
});
|
|
217
|
+
}
|