diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/.last-refresh.log b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/.last-refresh.log index 14efa308..37efdd35 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/.last-refresh.log +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/.last-refresh.log @@ -1,9 +1,9 @@ -Last refresh: 2026-02-28T10:22:33Z (2026-02-28 02:22:33 PST) -Cascade conversations: 50 (548M) -Memories: 48 +Last refresh: 2026-03-01T07:00:13Z (2026-02-28 23:00:13 PST) +Cascade conversations: 50 (495M) +Memories: 56 Implicit context: 20 -Code tracker dirs: 196 -File edit history: 1868 entries +Code tracker dirs: 182 +File edit history: 2010 entries Workspace storage: 28 workspaces -Repo docs: 13 files across 3 repos -Repo workflows: 23 files across 4 repos +Repo docs: 14 files across 3 repos +Repo workflows: 28 files across 5 repos diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_ai_common_plat/AUTH_CROSS_PRODUCT_ANALYSIS.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_ai_common_plat/AUTH_CROSS_PRODUCT_ANALYSIS.md new file mode 100644 index 00000000..23ab8ad3 --- /dev/null +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_ai_common_plat/AUTH_CROSS_PRODUCT_ANALYSIS.md @@ -0,0 +1,239 @@ +# Auth Cross-Product Analysis — Full Workspace Audit + +> **Date:** 2026-02-28 +> **Scope:** All 4 product repos + common platform +> **Question:** Do all apps share the same auth? Can a ChronoMind user sign in to NomGap? What's missing? + +--- + +## 1. Backend Architecture (Single Source of Truth) + +All products share **one** platform-service (port 4003) in `learning_ai_common_plat`. + +### Auth endpoints available: + +| Endpoint | Status | Notes | +| -------------------------------- | -------------- | --------------------------------------------------- | +| `POST /auth/login` | ✅ Implemented | Requires `{ email, password, productId }` | +| `POST /auth/register` | ✅ Implemented | Creates user + subscription + license | +| `POST /auth/refresh` | ✅ Implemented | Exchanges refresh token for new pair | +| `GET /auth/me` | ✅ Implemented | Returns user from Bearer token | +| `PUT /auth/profile` | ✅ Implemented | Self-service profile update | +| `POST /auth/sso` | ✅ Implemented | Microsoft/Google OAuth (find-or-create) | +| `POST /auth/verify` | ✅ Implemented | Service-to-service token check | +| `POST /auth/forgot-password` | ✅ Implemented | Generates reset token (logs it, no email sent) | +| `POST /auth/reset-password` | ✅ Implemented | Resets password with token | +| `POST /auth/verify-email` | ✅ Implemented | Verifies email with token | +| `POST /auth/resend-verification` | ✅ Implemented | Resends verification email (logs it, no email sent) | +| Admin CRUD (`/auth/users/*`) | ✅ Implemented | List, count, get, update, delete | + +### Database: Single Cosmos DB + +- **Container:** `users` — all users across all products +- **Partition key:** user `id` +- **Product isolation:** Every user doc has a `productId` field +- **Lookup:** `getByEmail(email, productId)` — queries by BOTH email AND productId + +### JWT tokens + +- **Issuer:** `bytelyst-platform` +- **Access token:** 1 hour, contains `{ sub, email, role, productId, plan }` +- **Refresh token:** 7 days, contains `{ sub, productId }` +- **Secret:** Single shared `JWT_SECRET` env var + +--- + +## 2. The Cross-Product Sign-In Question + +### Current design: Users are **per-product** + +The `getByEmail()` function queries: + +```sql +SELECT * FROM c WHERE c.productId = @productId AND c.email = @email +``` + +This means: + +- **A user who registers on ChronoMind (productId: `chronomind`) is a DIFFERENT user than the same email on NomGap (productId: `nomgap`)** +- Same email can have separate accounts with different passwords on each product +- Each registration creates a separate subscription + license record per product +- JWT tokens are scoped to a productId — a ChronoMind token cannot be used for NomGap API calls + +### Is this the right design? + +**Yes, for now.** Here's why: + +1. **Different products = different plans/subscriptions** — A user might be on Pro for ChronoMind but Free for NomGap +2. **Clean data isolation** — each product's user data doesn't leak across +3. **Independent license/device management** — device limits are per-product +4. **Simpler admin** — admin dashboard shows users per product + +### Future consideration: ByteLyst Account (cross-product SSO) + +If/when you want "sign in once, use all ByteLyst apps": + +- Add a `byteLystAccountId` linking field to user docs +- Add a `/auth/link-account` endpoint +- This is a P3 feature, not needed now + +--- + +## 3. Per-App Auth Inventory + +### Legend + +- ✅ = Implemented and working +- ⚠️ = Partially implemented (missing features) +- ❌ = Not implemented + +### 3.1 LysnrAI (`learning_voice_ai_agent`) + +| Surface | Login | Register | Refresh | Forgot Password | Email Verify | SSO | +| --------------------------- | ----- | --------------- | ---------------- | --------------- | ------------ | ----------------------- | +| **User Dashboard (web)** | ✅ | ✅ | ✅ (cookie) | ❌ | ❌ | ✅ (Google + Microsoft) | +| **Admin Dashboard (web)** | ✅ | ❌ (admin-only) | ✅ (cookie) | ❌ | ❌ | ❌ | +| **Tracker Dashboard (web)** | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | +| **iOS mobile** | ✅ | ✅ | ✅ (Keychain) | ❌ | ❌ | ✅ (Apple, Google) | +| **Android mobile** | ✅ | ✅ | ✅ (SharedPrefs) | ❌ | ❌ | ✅ (Google) | +| **Desktop (Python)** | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | + +**productId:** `lysnrai` + +### 3.2 ChronoMind (`learning_ai_clock`) + +| Surface | Login | Register | Refresh | Forgot Password | Email Verify | SSO | +| ----------- | ----- | -------- | -------------------------- | --------------- | ------------ | --- | +| **Web PWA** | ✅ | ✅ | ❌ (no auto-refresh) | ❌ | ❌ | ❌ | +| **iOS** | ✅ | ✅ | ✅ (Keychain, 45min timer) | ❌ | ❌ | ❌ | +| **Android** | ✅ | ✅ | ✅ (SharedPrefs) | ❌ | ❌ | ❌ | + +**productId:** `chronomind` + +### 3.3 NomGap (`learning_ai_fastgap`) + +| Surface | Login | Register | Refresh | Forgot Password | Email Verify | SSO | +| ----------------------- | ---------- | ---------- | ----------------- | --------------- | ------------ | --- | +| **React Native (Expo)** | ✅ (store) | ✅ (store) | ⚠️ (hydrate only) | ❌ | ❌ | ❌ | + +**productId:** `nomgap` +**Note:** Auth store actions + ProfileScreen UI are wired. `hydrateFromToken()` calls `/auth/me` but there's no proactive refresh timer. No dedicated login screen — auth is inline in ProfileScreen. + +### 3.4 MindLyst (`learning_multimodal_memory_agents`) + +| Surface | Login | Register | Refresh | Forgot Password | Email Verify | SSO | +| ----------------- | ----- | -------- | -------------------------- | --------------- | ------------ | --- | +| **Web (Next.js)** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| **iOS** | ✅ | ✅ | ✅ (Keychain, 45min timer) | ❌ | ❌ | ❌ | +| **Android** | ✅ | ✅ | ✅ (SharedPrefs) | ❌ | ❌ | ❌ | + +**productId:** `mindlyst` +**Note:** MindLyst web has NO auth at all — API routes use in-memory fallback or direct Cosmos, no platform-service integration. + +### 3.5 Dashboards (common platform) + +| Dashboard | Login | Register | Refresh | Forgot Password | SSO | +| ----------------------- | ----- | -------- | ------- | --------------- | --- | +| **Admin (port 3001)** | ✅ | ❌ | ✅ | ❌ | ❌ | +| **Tracker (port 3003)** | ✅ | ✅ | ✅ | ❌ | ❌ | + +--- + +## 4. Gaps — Prioritized Action List + +### P0: Critical (all users hit these) + +| # | Gap | Affected | Fix | +| ------ | ------------------------------------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **G1** | **No "Forgot Password" UI anywhere** | ALL 4 products, ALL surfaces | Backend endpoints exist (`/auth/forgot-password`, `/auth/reset-password`) but ZERO clients call them. Need: forgot password form + reset password page in every app. | +| **G2** | **No email delivery for password reset / email verification** | ALL | Backend generates tokens but only LOGS them (`req.log.info`). The `TODO: Send email via delivery module` comment is still there. Need: wire delivery module (SendGrid/SES) or at minimum an SMTP transport. | +| **G3** | **MindLyst web has NO auth** | MindLyst web | Web dashboard has no login/register at all. API routes bypass platform-service entirely. Need: add auth flow matching ChronoMind web pattern. | + +### P1: Important (poor UX without these) + +| # | Gap | Affected | Fix | +| ------ | ----------------------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **G4** | **No email verification UI** | ALL | Backend has `/auth/verify-email` + `/auth/resend-verification` but no client calls them. Users register with `emailVerified: false` and it's never checked/enforced. | +| **G5** | **ChronoMind web missing token refresh** | ChronoMind web | Web stores token in localStorage but never refreshes it. After 1 hour the token expires silently. Need: add refresh logic (like the iOS 45min timer). | +| **G6** | **NomGap missing proactive token refresh** | NomGap mobile | `hydrateFromToken()` calls `/auth/me` on startup but there's no periodic refresh. Token expires after 1 hour. Need: add refresh timer or intercept 401s. | +| **G7** | **No "Change Password" in any settings screen** | ALL | Users can only reset password via forgot-password flow (which doesn't work yet per G2). Need: `PUT /auth/profile` or new endpoint for authenticated password change. | + +### P2: Consistency (works but inconsistent) + +| # | Gap | Affected | Fix | +| ------- | --------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **G8** | **Password validation inconsistent across clients** | ALL | Backend requires `min(8)`. iOS/Android enforce 8+ chars, uppercase, lowercase, digit. ChronoMind web has no client-side validation. NomGap ProfileScreen has no validation. Standardize. | +| **G9** | **Token storage inconsistent** | Mixed | LysnrAI iOS/Android: Keychain/EncryptedSharedPrefs. ChronoMind: Keychain/plain SharedPrefs. MindLyst: Keychain/plain SharedPrefs. NomGap: MMKV. ChronoMind web: localStorage. Dashboards: httpOnly cookies. Consider standardizing mobile to Keychain + EncryptedSharedPrefs. | +| **G10** | **No SSO on ChronoMind, NomGap, or MindLyst** | 3 products | Only LysnrAI has Google/Microsoft/Apple SSO. Backend supports `/auth/sso`. Could add SSO to other products later. | +| **G11** | **Inconsistent `x-product-id` header** | Various | iOS `PlatformSyncManager` for ChronoMind doesn't send `x-product-id`. Some Android clients send it lowercase, some uppercase. Standardize. | +| **G12** | **No "Delete Account" in any app** | ALL | GDPR/privacy requirement. Backend has `DELETE /auth/users/:id` (admin only). Need: self-service account deletion endpoint + UI. | + +### P3: Nice-to-have + +| # | Gap | Affected | Fix | +| ------- | -------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------ | +| **G13** | **No cross-product ByteLyst account linking** | Future | If same user uses ChronoMind + NomGap, they have 2 separate accounts. Could add account linking later. | +| **G14** | **No rate limiting on auth endpoints from clients** | ALL | Backend has rate limiting module but clients don't handle 429 gracefully. | +| **G15** | **No biometric auth (FaceID/TouchID) on any mobile app** | iOS/Android | Could add biometric unlock after initial login. | + +--- + +## 5. Architecture Diagram — Current State + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ platform-service (:4003) │ +│ │ +│ /auth/login ← email + password + productId │ +│ /auth/register ← email + password + displayName + productId │ +│ /auth/refresh ← refreshToken │ +│ /auth/me ← Bearer token │ +│ /auth/sso ← email + productId + provider │ +│ /auth/forgot-password ← email + productId (⚠️ no email sent) │ +│ /auth/reset-password ← token + newPassword (⚠️ no UI calls) │ +│ /auth/verify-email ← token (⚠️ no UI calls) │ +│ │ +│ Cosmos DB: users container (partitioned by id) │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ { id, productId, email, passwordHash, ... } │ │ +│ │ │ │ +│ │ productId="lysnrai" → LysnrAI users │ │ +│ │ productId="chronomind" → ChronoMind users │ │ +│ │ productId="nomgap" → NomGap users │ │ +│ │ productId="mindlyst" → MindLyst users │ │ +│ └─────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────┘ + ▲ ▲ ▲ ▲ ▲ + │ │ │ │ │ + LysnrAI ChronoMind NomGap MindLyst Dashboards + (all 6 (web+iOS (Expo (iOS+ (admin+ + surfaces) +Android) RN) Android) tracker) +``` + +--- + +## 6. Recommended Fix Order + +1. **G2 — Email delivery** (unblocks G1, G4) — Wire SendGrid/SES into platform-service delivery module +2. **G1 — Forgot Password UI** — Add to all apps (once email works) +3. **G3 — MindLyst web auth** — Add auth context + login form +4. **G5 — ChronoMind web token refresh** — Add refresh logic +5. **G6 — NomGap token refresh** — Add refresh timer +6. **G4 — Email verification UI** — Add verification prompt post-register +7. **G7 — Change Password** — Add endpoint + UI in all settings screens +8. **G8 — Password validation** — Standardize client-side rules +9. **G12 — Delete Account** — Self-service endpoint + UI +10. **G9–G11** — Consistency cleanup + +--- + +## 7. Summary Answer + +> **Q: Can a ChronoMind user sign in directly to NomGap?** +> **A: No.** They must register separately. Each product has its own user namespace (`productId`). Same email = different accounts on different products. This is **by design** — each product has independent plans, subscriptions, and licenses. Cross-product account linking is a future P3 feature. + +> **Q: Do all apps use the same backend?** +> **A: Yes.** All products call the same platform-service `/auth/*` endpoints, storing users in the same Cosmos DB `users` container, isolated by `productId`. + +> **Q: What's the biggest gap?** +> **A: Password reset doesn't work end-to-end.** The backend endpoints exist but (a) no email delivery is wired, and (b) zero client apps have forgot-password UI. This is the #1 gap to fix. diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_ai_common_plat/PLATFORM_COMPONENTS_ROADMAP.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_ai_common_plat/PLATFORM_COMPONENTS_ROADMAP.md index cf55cd12..5fa5a7ce 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_ai_common_plat/PLATFORM_COMPONENTS_ROADMAP.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_ai_common_plat/PLATFORM_COMPONENTS_ROADMAP.md @@ -1,7 +1,7 @@ # Platform Components Roadmap — What's Built, What's Missing, What's Next > **Status:** Living document — brainstorm + gap analysis -> **Last updated:** 2026-02-17 +> **Last updated:** 2026-03-15 > **Scope:** All infrastructure components relevant to admin, DevOps, and product operations across the ByteLyst platform. > **Repos:** `learning_ai_common_plat` (platform-service, packages) · `learning_voice_ai_agent` (dashboards, clients) @@ -28,7 +28,7 @@ ## 1. Current Inventory -### 1.1 Platform-Service Modules (25 modules) +### 1.1 Platform-Service Modules (30 modules) | Category | Module | Endpoints | Description | | ------------ | --------------- | --------- | --------------------------------------------------------------------------------------------------- | @@ -57,6 +57,11 @@ | **Ops** | `themes` | 7 routes | Platform theming (iOS, Android, Desktop) | | **Ops** | `blob` | 5 routes | Azure Blob Storage SAS tokens, list, delete, info | | **Registry** | `products` | 4 routes | Multi-product registry with full lifecycle (draft → pre_launch → beta → active → sunset → disabled) | +| **Ops** | `jobs` | 5 routes | Scheduled jobs: cron parser, registry, runner, 6 built-in jobs, manual trigger | +| **Ops** | `status` | 6 routes | Public status page: health checker, incidents CRUD, history | +| **Ops** | `delivery` | 6 routes | Transactional email: 8 templates, renderer, SendGrid/Postmark/console adapters, delivery log | +| **Identity** | `auth` (reset) | 4 routes | Password reset (forgot/reset) + email verification (verify/resend) — added to auth module | +| **Infra** | `event-bus` | Singleton | In-memory typed pub/sub via @bytelyst/events — emits on register, password reset, email verified | ### 1.2 Shared Packages (13 packages) @@ -75,12 +80,13 @@ | `@bytelyst/extraction` | Extraction client + shared types | | `@bytelyst/monitoring` | Health-check utilities | | `@bytelyst/design-tokens` | Cross-platform token generator (JSON → CSS/TS/Kotlin/Swift) | +| `@bytelyst/events` | Typed in-memory event bus with error isolation (14 tests) | ### 1.3 Services | Service | Port | Description | | ---------------------- | ---- | ---------------------------------------------------- | -| **platform-service** | 4003 | Consolidated Fastify service (25 modules, 621 tests) | +| **platform-service** | 4003 | Consolidated Fastify service (30 modules, 988 tests) | | **extraction-service** | 4005 | LangExtract text extraction + Python sidecar | | **monitoring** | 4004 | Health-check aggregator (all services) | @@ -104,7 +110,13 @@ | **Feature flags** | ✅ | FNV-1a hash, percentage rollout, admin UI | | **Client telemetry** | ✅ | All platforms instrumented, admin Client Logs page | | **Rate limiting** | ✅ | In-memory sliding window + configurable rules per product | -| **Outbound webhooks** | ⚠️ Partial | Fire-and-forget POST for 3 events (`lib/webhooks.ts`); no subscription model, no retry, no HMAC signing | +| **Outbound webhooks** | ⚠️ Partial | Fire-and-forget POST for 3 events (`lib/webhooks.ts`); subscription model built in `modules/webhooks/` with HMAC signing + retry | +| **Event bus** | ✅ | `@bytelyst/events` package + singleton in platform-service; auth emits user.created, password_reset, email_verified | +| **Scheduled jobs** | ✅ | Cron parser, registry, in-process runner, 6 built-in jobs, admin API | +| **Email delivery** | ✅ | 8 templates, renderer, SendGrid/Postmark/console adapters, delivery log, event bus subscribers | +| **Password reset** | ✅ | forgot-password + reset-password endpoints, SHA-256 token hashing, anti-enumeration | +| **Email verification** | ✅ | verify-email + resend-verification endpoints, emailVerified field on UserDoc | +| **Status page** | ✅ | Health checker (3 services), incident management, public + admin endpoints | | **Kill switch** | ✅ | Per-product, checked by all clients via `/settings/kill-switch` | | **Audit logging** | ✅ | Records admin actions, queryable from admin dashboard | | **Blob storage** | ✅ | 6 containers (audio, transcripts, attachments, avatars, releases, backups), SAS tokens, admin endpoints | diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_ai_common_plat/SERVICE_CONSOLIDATION_ROADMAP.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_ai_common_plat/SERVICE_CONSOLIDATION_ROADMAP.md index c87f9a6f..df77a458 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_ai_common_plat/SERVICE_CONSOLIDATION_ROADMAP.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_ai_common_plat/SERVICE_CONSOLIDATION_ROADMAP.md @@ -11,13 +11,13 @@ ## Why Consolidate -| Problem | Impact | -| ---------------------------------------- | ---------------------------------------------- | -| 5 separate Node processes for 2 products | Unnecessary operational overhead | -| 5 ports to manage (4001–4005) | Complex docker-compose, run scripts, env files | -| 5 separate Cosmos connections | Wasted connection pool resources | -| 5 CI pipelines | Slow feedback, more config to maintain | -| 5 config schemas with duplicate env vars | Inconsistent config, easy to miss vars | +| Problem | Impact | +|---------|--------| +| 5 separate Node processes for 2 products | Unnecessary operational overhead | +| 5 ports to manage (4001–4005) | Complex docker-compose, run scripts, env files | +| 5 separate Cosmos connections | Wasted connection pool resources | +| 5 CI pipelines | Slow feedback, more config to maintain | +| 5 config schemas with duplicate env vars | Inconsistent config, easy to miss vars | **After consolidation:** 2 services — `platform-service` (port 4003) + `extraction-service` (port 4005) @@ -31,12 +31,12 @@ Services export product ID differently — modules reference different names: -| Service | Export Name | Source | -| -------------------- | -------------------- | ---------------------------------------------------------------------------- | -| **platform-service** | `PRODUCT_ID` | `loadProductIdentity().productId` from `@bytelyst/config` | -| **growth-service** | `PRODUCT_ID` | same as platform ✅ | -| **billing-service** | `PRODUCT_ID` | same as platform ✅ | -| **tracker-service** | `DEFAULT_PRODUCT_ID` | `process.env.DEFAULT_PRODUCT_ID \|\| getProductId()` — **different name** ⚠️ | +| Service | Export Name | Source | +|---------|-----------|--------| +| **platform-service** | `PRODUCT_ID` | `loadProductIdentity().productId` from `@bytelyst/config` | +| **growth-service** | `PRODUCT_ID` | same as platform ✅ | +| **billing-service** | `PRODUCT_ID` | same as platform ✅ | +| **tracker-service** | `DEFAULT_PRODUCT_ID` | `process.env.DEFAULT_PRODUCT_ID \|\| getProductId()` — **different name** ⚠️ | **Fix:** When merging tracker modules, change all `DEFAULT_PRODUCT_ID` imports to `PRODUCT_ID` in the copied module files, and add `DEFAULT_PRODUCT_ID` env var support to platform-service's `product-config.ts` for backward compat. @@ -44,16 +44,15 @@ Services export product ID differently — modules reference different names: Platform-service `package.json` is **missing** these deps needed by merged modules: -| Dep | Needed By | Currently In | -| ------------------------------- | ------------------------------------------- | ------------------------------- | -| `stripe` (^17.5.0) | billing modules (stripe webhooks, checkout) | billing-service, growth-service | -| `@bytelyst/auth` (workspace:\*) | tracker modules (`extractAuth`) | tracker-service | -| `@fastify/rate-limit` (^10.3.0) | tracker rate limiting | tracker-service | +| Dep | Needed By | Currently In | +|-----|-----------|-------------| +| `stripe` (^17.5.0) | billing modules (stripe webhooks, checkout) | billing-service, growth-service | +| `@bytelyst/auth` (workspace:*) | tracker modules (`extractAuth`) | tracker-service | +| `@fastify/rate-limit` (^10.3.0) | tracker rate limiting | tracker-service | ### Gap 3: Billing Internal Key Auth (Global Hook) `billing-service/src/server.ts` has a **global** `onRequest` hook: - ```typescript app.addHook('onRequest', async (req, reply) => { if (path === '/health' || path.includes('/stripe/webhook')) return; @@ -61,7 +60,6 @@ app.addHook('onRequest', async (req, reply) => { if (key !== INTERNAL_KEY) reply.code(401).send(...) }); ``` - This **cannot** be a global hook after merge — it would block auth, audit, tracker, etc. routes. **Fix:** Convert to a Fastify plugin registered only on billing route prefixes, or add `x-internal-key` check inside each billing route handler. @@ -69,7 +67,6 @@ This **cannot** be a global hook after merge — it would block auth, audit, tra ### Gap 4: Growth Webhooks Library `growth-service/src/lib/webhooks.ts` dispatches fire-and-forget HTTP callbacks on invitation redeem. References env vars: - - `WEBHOOK_INVITATION_REDEEMED_URL` - `WEBHOOK_REFERRAL_STATUS_URL` @@ -85,26 +82,26 @@ Growth-service config requires `STRIPE_SECRET_KEY` as **required** (not optional **Dashboard API clients (TypeScript):** -| File | Current Env Var | Current Default | -| -------------------------------------------------------------- | --------------------- | ---------------------------------- | -| `admin-dashboard-web/src/lib/billing-client.ts` | `BILLING_SERVICE_URL` | `http://localhost:4002` | -| `admin-dashboard-web/src/lib/growth-client.ts` | `GROWTH_SERVICE_URL` | `http://localhost:4001` | -| `user-dashboard-web/src/lib/billing-client.ts` | `BILLING_SERVICE_URL` | `http://localhost:4002` | -| `user-dashboard-web/src/lib/growth-client.ts` | `GROWTH_SERVICE_URL` | `http://localhost:4001` | -| `user-dashboard-web/src/app/api/stripe/webhook/route.ts` | `BILLING_SERVICE_URL` | `http://localhost:4002` | -| `admin-dashboard-web/src/app/api/stripe/config/route.ts` | — | `http://localhost:4002` inline | -| `admin-dashboard-web/src/lib/stripe-context.tsx` | — | `http://localhost:4002` (3 places) | -| `tracker-dashboard-web/src/app/api/tracker/[...path]/route.ts` | `TRACKER_API_URL` | `http://localhost:4004` | -| `tracker-dashboard-web/src/app/api/auth/login/route.ts` | `PLATFORM_API_URL` | `http://localhost:4003` ✅ | -| `tracker-dashboard-web/src/app/api/auth/me/route.ts` | `PLATFORM_API_URL` | `http://localhost:4003` ✅ | +| File | Current Env Var | Current Default | +|------|----------------|-----------------| +| `admin-dashboard-web/src/lib/billing-client.ts` | `BILLING_SERVICE_URL` | `http://localhost:4002` | +| `admin-dashboard-web/src/lib/growth-client.ts` | `GROWTH_SERVICE_URL` | `http://localhost:4001` | +| `user-dashboard-web/src/lib/billing-client.ts` | `BILLING_SERVICE_URL` | `http://localhost:4002` | +| `user-dashboard-web/src/lib/growth-client.ts` | `GROWTH_SERVICE_URL` | `http://localhost:4001` | +| `user-dashboard-web/src/app/api/stripe/webhook/route.ts` | `BILLING_SERVICE_URL` | `http://localhost:4002` | +| `admin-dashboard-web/src/app/api/stripe/config/route.ts` | — | `http://localhost:4002` inline | +| `admin-dashboard-web/src/lib/stripe-context.tsx` | — | `http://localhost:4002` (3 places) | +| `tracker-dashboard-web/src/app/api/tracker/[...path]/route.ts` | `TRACKER_API_URL` | `http://localhost:4004` | +| `tracker-dashboard-web/src/app/api/auth/login/route.ts` | `PLATFORM_API_URL` | `http://localhost:4003` ✅ | +| `tracker-dashboard-web/src/app/api/auth/me/route.ts` | `PLATFORM_API_URL` | `http://localhost:4003` ✅ | **Python clients (desktop + backend):** -| File | Current Env Var | Current Default | -| --------------------------------------- | --------------------- | ----------------------- | +| File | Current Env Var | Current Default | +|------|----------------|-----------------| | `backend/src/clients/billing_client.py` | `BILLING_SERVICE_URL` | `http://localhost:4002` | -| `src/cloud/api_sync.py` | `BILLING_SERVICE_URL` | `http://localhost:4002` | -| `src/cloud/plan_resolver.py` | `BILLING_SERVICE_URL` | `http://localhost:4002` | +| `src/cloud/api_sync.py` | `BILLING_SERVICE_URL` | `http://localhost:4002` | +| `src/cloud/plan_resolver.py` | `BILLING_SERVICE_URL` | `http://localhost:4002` | All these must change to `PLATFORM_SERVICE_URL` / `http://localhost:4003`. @@ -115,12 +112,10 @@ All these must change to `PLATFORM_SERVICE_URL` / `http://localhost:4003`. ### Gap 8: Stripe Webhook Test Hardcodes Port `user-dashboard-web/src/__tests__/stripe-webhook.test.ts` sets: - ```typescript process.env.BILLING_SERVICE_URL = 'http://localhost:4002'; expect(url).toBe('http://localhost:4002/api/stripe/webhook'); ``` - Must update to port 4003. ### Gap 9: Load Test Scripts @@ -138,7 +133,6 @@ Must update defaults to port 4003. ### Gap 11: LysnrAI Services Stubs `learning_voice_ai_agent/services/` contains `.env.example` stubs for each service: - - `services/billing-service/.env.example` - `services/growth-service/.env.example` - `services/tracker-service/.env.example` @@ -160,7 +154,6 @@ Mobile apps call the Python backend (`localhost:8000`), which calls billing-serv ### Gap 14: Docker Compose `depends_on` for Tracker Dashboard `learning_voice_ai_agent/docker-compose.yml` has: - ```yaml tracker-dashboard: depends_on: @@ -169,23 +162,17 @@ tracker-dashboard: platform-service: condition: service_started ``` - After merge, `tracker-service` container no longer exists. Must change `depends_on` to only `platform-service`. ### Gap 15: Admin Dashboard `docs.ts` Service Directory List `admin-dashboard-web/src/lib/docs.ts` has a hardcoded list of service directories: - ```typescript const serviceDirs = [ - 'admin-dashboard-web', - 'user-dashboard-web', - 'mobile_app', - 'services/billing-service', - 'services/growth-service', + 'admin-dashboard-web', 'user-dashboard-web', 'mobile_app', + 'services/billing-service', 'services/growth-service', ]; ``` - Must update to remove old service names or replace with `services/platform-service`. ### Gap 16: MindLyst Docs Reference Old Services @@ -208,7 +195,6 @@ Platform-service's Dockerfile only copies `services/platform-service/` — it do ### Route Path Collision Check ✅ All services use unique route prefixes — **no collisions**: - - platform: `/auth/*`, `/audit/*`, `/notifications/*`, `/flags/*`, `/ratelimit/*`, `/blob/*`, `/devices/*` - billing: `/subscriptions/*`, `/usage/*`, `/plans/*`, `/licenses/*`, `/payments/*`, `/stripe/*` - growth: `/invitations/*`, `/referrals/*`, `/promos/*` @@ -258,12 +244,12 @@ services/ All containers served by one Cosmos client in platform-service: -| Origin | Containers | -| ----------------------- | ----------------------------------------------------------------------------------- | +| Origin | Containers | +|--------|-----------| | **platform** (existing) | `users`, `audit_log`, `feature_flags`, `notification_devices`, `notification_prefs` | -| **billing** → platform | `subscriptions`, `payments`, `plans`, `licenses`, `usage_daily` | -| **growth** → platform | `invitation_codes`, `referrals`, `promo_codes` | -| **tracker** → platform | `tracker_items`, `tracker_comments`, `tracker_votes` | +| **billing** → platform | `subscriptions`, `payments`, `plans`, `licenses`, `usage_daily` | +| **growth** → platform | `invitation_codes`, `referrals`, `promo_codes` | +| **tracker** → platform | `tracker_items`, `tracker_comments`, `tracker_votes` | --- @@ -404,7 +390,7 @@ All containers served by one Cosmos client in platform-service: - [x] **3.3.1** Created `platform-service/src/lib/auth.ts` re-exporting from `@bytelyst/auth` - [x] **3.3.2** Copied from tracker-service (identical content) -- [x] **3.3.3** Added `@bytelyst/auth` (workspace:\*) to package.json +- [x] **3.3.3** Added `@bytelyst/auth` (workspace:*) to package.json - [x] **3.3.4** Added `@fastify/rate-limit` (^10.3.0) to package.json - [x] **3.3.5** `jose` already in platform ✅ @@ -574,30 +560,29 @@ Also fixed: monitoring/health.ts, AI.dev/SKILLS docs, MIGRATION_GUIDE.md [`81609 ## Summary -| Phase | What | Effort | Tests Moved | Critical Gaps Addressed | -| --------- | ------------------------------------------- | ------------- | --------------- | ------------------------------------ | -| **0** | Preparation & backup | 30 min | — | — | -| **1** | Merge growth-service (3 modules) | 2–3 hrs | ~14 | Gap 4 (webhooks), Gap 5 (Stripe key) | -| **2** | Merge billing-service (5 modules) | 4–5 hrs | ~11 | Gap 3 (internal key auth) | -| **3** | Merge tracker-service (4 modules) | 3–4 hrs | ~45 | Gap 1 (product ID), Gap 2 (deps) | -| **4** | Update consumers (20+ files across 3 repos) | 4–5 hrs | — | Gaps 6–11, 13–17 | -| **5** | Documentation & final verification | 2–3 hrs | — | — | -| **Total** | **5 services → 2** | **~4–5 days** | **~125+ tests** | **17 gaps addressed** | +| Phase | What | Effort | Tests Moved | Critical Gaps Addressed | +|-------|------|--------|-------------|------------------------| +| **0** | Preparation & backup | 30 min | — | — | +| **1** | Merge growth-service (3 modules) | 2–3 hrs | ~14 | Gap 4 (webhooks), Gap 5 (Stripe key) | +| **2** | Merge billing-service (5 modules) | 4–5 hrs | ~11 | Gap 3 (internal key auth) | +| **3** | Merge tracker-service (4 modules) | 3–4 hrs | ~45 | Gap 1 (product ID), Gap 2 (deps) | +| **4** | Update consumers (20+ files across 3 repos) | 4–5 hrs | — | Gaps 6–11, 13–17 | +| **5** | Documentation & final verification | 2–3 hrs | — | — | +| **Total** | **5 services → 2** | **~4–5 days** | **~125+ tests** | **17 gaps addressed** | ## Port Allocation (After) -| Service | Port | -| -------------------------------------------- | -------- | -| **platform-service** | **4003** | -| **extraction-service** | **4005** | -| extraction-service python sidecar (internal) | 4006 | +| Service | Port | +|---------|------| +| **platform-service** | **4003** | +| **extraction-service** | **4005** | +| extraction-service python sidecar (internal) | 4006 | Ports 4001, 4002, 4004 freed up. ## Rollback Strategy Each phase has its own commit. If a phase breaks something: - 1. `git revert ` to undo that phase 2. The old service code is in git history 3. Backup branches created in Phase 0 @@ -605,13 +590,13 @@ Each phase has its own commit. If a phase breaks something: ## Risks & Mitigations -| Risk | Mitigation | -| ---------------------------------------- | ----------------------------------------------------------------------------- | -| Route path collisions | Verified ✅ — all services use unique prefixes | -| Config schema gets large | Group env vars by domain with clear section comments | -| Stripe webhook raw body | Fastify handles this — verify after move | +| Risk | Mitigation | +|------|-----------| +| Route path collisions | Verified ✅ — all services use unique prefixes | +| Config schema gets large | Group env vars by domain with clear section comments | +| Stripe webhook raw body | Fastify handles this — verify after move | | Billing internal key blocks other routes | Scoped Fastify plugin (Phase 2.2) isolates key check to billing prefixes only | -| Public tracker routes skip auth | Register outside scoped plugins — verify in Phase 3.5.3 | -| Python billing client breaks | Change env var name, keep same API paths — transparent to Python code | -| Stripe webhook test fails | Explicit port update in Phase 4.4 | -| Product ID mismatch | Alias `DEFAULT_PRODUCT_ID = PRODUCT_ID` in Phase 3.2.4 | +| Public tracker routes skip auth | Register outside scoped plugins — verify in Phase 3.5.3 | +| Python billing client breaks | Change env var name, keep same API paths — transparent to Python code | +| Stripe webhook test fails | Explicit port update in Phase 4.4 | +| Product ID mismatch | Alias `DEFAULT_PRODUCT_ID = PRODUCT_ID` in Phase 3.2.4 | diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_multimodal_memory_agents/ENV_AUDIT_LYSNRAI.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_multimodal_memory_agents/ENV_AUDIT_LYSNRAI.md index acdb5691..1c3f8265 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_multimodal_memory_agents/ENV_AUDIT_LYSNRAI.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_multimodal_memory_agents/ENV_AUDIT_LYSNRAI.md @@ -9,17 +9,17 @@ ## 1. Project → Env File Map -| # | Project | Env File | Port | -| --- | ----------------------------------------------- | ---------------------------------- | ---- | -| 1 | Desktop app (`src/`) | `.env` (root) | — | -| 2 | Backend API (`backend/`) | `backend/.env` | 8000 | -| 3 | Admin Dashboard (`admin-dashboard-web/`) | `admin-dashboard-web/.env.local` | 3001 | -| 4 | User Dashboard (`user-dashboard-web/`) | `user-dashboard-web/.env.local` | 3002 | -| 5 | Tracker Dashboard (`tracker-dashboard-web/`) | `tracker-dashboard-web/.env.local` | 3003 | -| 6 | Billing Service (`services/billing-service/`) | `services/billing-service/.env` | 4002 | -| 7 | Growth Service (`services/growth-service/`) | `services/growth-service/.env` | 4001 | -| 8 | Platform Service (`services/platform-service/`) | `services/platform-service/.env` | 4003 | -| 9 | Tracker Service (`services/tracker-service/`) | `services/tracker-service/.env` | 4004 | +| # | Project | Env File | Port | +|---|---------|----------|------| +| 1 | Desktop app (`src/`) | `.env` (root) | — | +| 2 | Backend API (`backend/`) | `backend/.env` | 8000 | +| 3 | Admin Dashboard (`admin-dashboard-web/`) | `admin-dashboard-web/.env.local` | 3001 | +| 4 | User Dashboard (`user-dashboard-web/`) | `user-dashboard-web/.env.local` | 3002 | +| 5 | Tracker Dashboard (`tracker-dashboard-web/`) | `tracker-dashboard-web/.env.local` | 3003 | +| 6 | Billing Service (`services/billing-service/`) | `services/billing-service/.env` | 4002 | +| 7 | Growth Service (`services/growth-service/`) | `services/growth-service/.env` | 4001 | +| 8 | Platform Service (`services/platform-service/`) | `services/platform-service/.env` | 4003 | +| 9 | Tracker Service (`services/tracker-service/`) | `services/tracker-service/.env` | 4004 | --- @@ -36,63 +36,63 @@ grep -rn 'MISSING_ENV_VALUE' --include='.env*' --include='*.env' . | grep -v nod ### 3.1 Root `.env` (Desktop App) -| Variable | Status | Action | -| --------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------- | -| `APPLICATIONINSIGHTS_CONNECTION_STRING` | ❌ Empty | Get from Azure Portal → Application Insights → Overview | -| `ANH_CONNECTION_STRING` | ⚠️ Has `YOUR_KEY_HERE` placeholder | Replace with real SharedAccessKey from Azure Portal → Notification Hubs | +| Variable | Status | Action | +|----------|--------|--------| +| `APPLICATIONINSIGHTS_CONNECTION_STRING` | ❌ Empty | Get from Azure Portal → Application Insights → Overview | +| `ANH_CONNECTION_STRING` | ⚠️ Has `YOUR_KEY_HERE` placeholder | Replace with real SharedAccessKey from Azure Portal → Notification Hubs | ### 3.2 `backend/.env` -| Variable | Status | Action | -| ------------------------------- | -------- | ------------------------------------------------------------------------ | -| `AZURE_EMAIL_CONNECTION_STRING` | ❌ Empty | Get from Azure Portal → Communication Services → Keys | -| `SMTP_HOST` | ❌ Empty | Configure if using SMTP fallback instead of Azure Communication Services | -| `SMTP_USER` | ❌ Empty | Configure if using SMTP fallback | -| `SMTP_PASS` | ❌ Empty | Configure if using SMTP fallback | +| Variable | Status | Action | +|----------|--------|--------| +| `AZURE_EMAIL_CONNECTION_STRING` | ❌ Empty | Get from Azure Portal → Communication Services → Keys | +| `SMTP_HOST` | ❌ Empty | Configure if using SMTP fallback instead of Azure Communication Services | +| `SMTP_USER` | ❌ Empty | Configure if using SMTP fallback | +| `SMTP_PASS` | ❌ Empty | Configure if using SMTP fallback | ### 3.3 `admin-dashboard-web/.env.local` -| Variable | Status | Action | -| -------------------------- | -------- | ---------------------------------------------------------- | -| `NEXT_PUBLIC_POSTHOG_KEY` | ❌ Empty | Get from PostHog → Project Settings (optional — analytics) | +| Variable | Status | Action | +|----------|--------|--------| +| `NEXT_PUBLIC_POSTHOG_KEY` | ❌ Empty | Get from PostHog → Project Settings (optional — analytics) | | `NEXT_PUBLIC_POSTHOG_HOST` | ❌ Empty | Get from PostHog → Project Settings (optional — analytics) | ### 3.4 `user-dashboard-web/.env.local` -| Variable | Status | Action | -| -------------------------- | -------- | ------------------------------------------------------------------- | +| Variable | Status | Action | +|----------|--------|--------| | `ENTERPRISE_EMAIL_DOMAINS` | ❌ Empty | Set comma-separated list of domains that qualify for Enterprise SSO | -| `MICROSOFT_CLIENT_ID` | ❌ Empty | Register app in Azure Portal → Entra ID → App registrations | -| `MICROSOFT_CLIENT_SECRET` | ❌ Empty | Same as above | -| `GOOGLE_CLIENT_ID` | ❌ Empty | Register app in Google Cloud Console → Credentials | -| `GOOGLE_CLIENT_SECRET` | ❌ Empty | Same as above | -| `NEXT_PUBLIC_POSTHOG_KEY` | ❌ Empty | PostHog analytics (optional) | -| `NEXT_PUBLIC_POSTHOG_HOST` | ❌ Empty | PostHog analytics (optional) | +| `MICROSOFT_CLIENT_ID` | ❌ Empty | Register app in Azure Portal → Entra ID → App registrations | +| `MICROSOFT_CLIENT_SECRET` | ❌ Empty | Same as above | +| `GOOGLE_CLIENT_ID` | ❌ Empty | Register app in Google Cloud Console → Credentials | +| `GOOGLE_CLIENT_SECRET` | ❌ Empty | Same as above | +| `NEXT_PUBLIC_POSTHOG_KEY` | ❌ Empty | PostHog analytics (optional) | +| `NEXT_PUBLIC_POSTHOG_HOST` | ❌ Empty | PostHog analytics (optional) | ### 3.5 `tracker-dashboard-web/.env.local` -| Variable | Status | Action | -| -------------------------- | -------- | ---------------------------- | -| `NEXT_PUBLIC_POSTHOG_KEY` | ❌ Empty | PostHog analytics (optional) | +| Variable | Status | Action | +|----------|--------|--------| +| `NEXT_PUBLIC_POSTHOG_KEY` | ❌ Empty | PostHog analytics (optional) | | `NEXT_PUBLIC_POSTHOG_HOST` | ❌ Empty | PostHog analytics (optional) | ### 3.6 `services/growth-service/.env` -| Variable | Status | Action | -| --------------------------------- | -------- | ------------------------------------------------------------ | +| Variable | Status | Action | +|----------|--------|--------| | `WEBHOOK_INVITATION_REDEEMED_URL` | ❌ Empty | Set to backend or platform-service webhook callback endpoint | -| `WEBHOOK_REFERRAL_STATUS_URL` | ❌ Empty | Set to backend or platform-service webhook callback endpoint | +| `WEBHOOK_REFERRAL_STATUS_URL` | ❌ Empty | Set to backend or platform-service webhook callback endpoint | ### 3.7 `services/billing-service/.env` -| Variable | Status | Action | -| ------------------ | -------- | --------------------------------------------------------------- | +| Variable | Status | Action | +|----------|--------|--------| | `PLAN_LIMITS_JSON` | ❌ Empty | Optional — set JSON with per-plan limits if overriding defaults | ### 3.8 `services/platform-service/.env` -| Variable | Status | Action | -| ------------------------ | -------- | ------------------------------------------------------------------------ | +| Variable | Status | Action | +|----------|--------|--------| | `RATE_LIMIT_CONFIG_JSON` | ❌ Empty | Optional — set JSON with per-endpoint rate limits if overriding defaults | ### 3.9 `services/tracker-service/.env` @@ -105,21 +105,21 @@ grep -rn 'MISSING_ENV_VALUE' --include='.env*' --include='*.env' . | grep -v nod These were missing from `.env` files but had known values, so they were filled in: -| Project | Variable | Value Added | -| -------------------------------- | -------------------------- | --------------------------------------- | -| Root `.env` | `PLATFORM_SERVICE_URL` | `http://localhost:4003` | -| Root `.env` | `LYSNR_API_URL` | `http://localhost:8000` | -| Root `.env` | `LYSNR_ADMIN_URL` | `http://localhost:3001` | -| Root `.env` | `LYSNR_DASHBOARD_URL` | `http://localhost:3002` | -| `backend/.env` | `BILLING_SERVICE_URL` | `http://localhost:4002` | -| `backend/.env` | `PLATFORM_SERVICE_URL` | `http://localhost:4003` | -| `backend/.env` | `CORS_ORIGINS` | Expanded to include all dashboard ports | -| `admin-dashboard-web/.env.local` | `STRIPE_PUBLISHABLE_KEY` | Test key (was missing) | -| `admin-dashboard-web/.env.local` | `STRIPE_WEBHOOK_SECRET` | Test key (was missing) | -| `admin-dashboard-web/.env.local` | `STRIPE_PRICE_PRO` | `price_1Szl2z...` | -| `admin-dashboard-web/.env.local` | `STRIPE_PRICE_ENTERPRISE` | `price_1Szl3D...` | -| `user-dashboard-web/.env.local` | `ENTERPRISE_EMAIL_DOMAINS` | Empty (needs config) | -| `services/billing-service/.env` | `USAGE_WARN_THRESHOLD` | `80` | +| Project | Variable | Value Added | +|---------|----------|-------------| +| Root `.env` | `PLATFORM_SERVICE_URL` | `http://localhost:4003` | +| Root `.env` | `LYSNR_API_URL` | `http://localhost:8000` | +| Root `.env` | `LYSNR_ADMIN_URL` | `http://localhost:3001` | +| Root `.env` | `LYSNR_DASHBOARD_URL` | `http://localhost:3002` | +| `backend/.env` | `BILLING_SERVICE_URL` | `http://localhost:4002` | +| `backend/.env` | `PLATFORM_SERVICE_URL` | `http://localhost:4003` | +| `backend/.env` | `CORS_ORIGINS` | Expanded to include all dashboard ports | +| `admin-dashboard-web/.env.local` | `STRIPE_PUBLISHABLE_KEY` | Test key (was missing) | +| `admin-dashboard-web/.env.local` | `STRIPE_WEBHOOK_SECRET` | Test key (was missing) | +| `admin-dashboard-web/.env.local` | `STRIPE_PRICE_PRO` | `price_1Szl2z...` | +| `admin-dashboard-web/.env.local` | `STRIPE_PRICE_ENTERPRISE` | `price_1Szl3D...` | +| `user-dashboard-web/.env.local` | `ENTERPRISE_EMAIL_DOMAINS` | Empty (needs config) | +| `services/billing-service/.env` | `USAGE_WARN_THRESHOLD` | `80` | --- @@ -134,14 +134,14 @@ These were missing from `.env` files but had known values, so they were filled i These values **must be identical** across all services that use them: -| Secret | Used By | -| ------------------- | -------------------------------------------------------------------- | -| `JWT_SECRET` | All 4 Fastify services + all 3 dashboards + backend | -| `COSMOS_ENDPOINT` | All 4 Fastify services + admin + user dashboards + backend + desktop | -| `COSMOS_KEY` | Same as above | -| `COSMOS_DATABASE` | Same as above (must be `lysnrai`) | -| `STRIPE_SECRET_KEY` | billing-service, growth-service, admin-dashboard, user-dashboard | -| `AZURE_BLOB_*` | platform-service, admin-dashboard, user-dashboard, desktop | +| Secret | Used By | +|--------|---------| +| `JWT_SECRET` | All 4 Fastify services + all 3 dashboards + backend | +| `COSMOS_ENDPOINT` | All 4 Fastify services + admin + user dashboards + backend + desktop | +| `COSMOS_KEY` | Same as above | +| `COSMOS_DATABASE` | Same as above (must be `lysnrai`) | +| `STRIPE_SECRET_KEY` | billing-service, growth-service, admin-dashboard, user-dashboard | +| `AZURE_BLOB_*` | platform-service, admin-dashboard, user-dashboard, desktop | --- diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_multimodal_memory_agents/USAGE_REVIEW.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_multimodal_memory_agents/USAGE_REVIEW.md index 8edfb4df..4534a38a 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_multimodal_memory_agents/USAGE_REVIEW.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_multimodal_memory_agents/USAGE_REVIEW.md @@ -8,60 +8,54 @@ ## Key Metrics at a Glance -| Metric | Value | Assessment | -| ---------------------------- | --------------------------------- | ---------------------------------- | -| **Lines written by Cascade** | 227,885 (year) / 143,815 (period) | Extremely high output | -| **% new code by Windsurf** | 99% | Near-total AI-assisted development | -| **Cascade conversations** | 21 | ~0.7/day — focused, long sessions | -| **Cascade messages sent** | 1,470 (Write), 71 (Chat) | 95% Write mode — action-oriented | -| **Credits used** | 8,069 | Heavy but productive usage | -| **Terminal messages sent** | 5,893 | Heavy command execution | -| **Workflows used** | 35 | Good use of custom workflows | -| **Memories used** | 20 | Context retention across sessions | -| **Web searches** | 9 | Minimal external lookup needed | -| **Previews** | 45 | Regular visual verification | -| **App deploys** | 1 | | -| **Commands used** | 0 | Slash commands not utilized | -| **Tab acceptances** | 1 (Markdown) | Almost zero autocomplete usage | +| Metric | Value | Assessment | +|--------|-------|------------| +| **Lines written by Cascade** | 227,885 (year) / 143,815 (period) | Extremely high output | +| **% new code by Windsurf** | 99% | Near-total AI-assisted development | +| **Cascade conversations** | 21 | ~0.7/day — focused, long sessions | +| **Cascade messages sent** | 1,470 (Write), 71 (Chat) | 95% Write mode — action-oriented | +| **Credits used** | 8,069 | Heavy but productive usage | +| **Terminal messages sent** | 5,893 | Heavy command execution | +| **Workflows used** | 35 | Good use of custom workflows | +| **Memories used** | 20 | Context retention across sessions | +| **Web searches** | 9 | Minimal external lookup needed | +| **Previews** | 45 | Regular visual verification | +| **App deploys** | 1 | | +| **Commands used** | 0 | Slash commands not utilized | +| **Tab acceptances** | 1 (Markdown) | Almost zero autocomplete usage | --- ## Model Distribution -| Model | Usage % | Role | -| ----------------------------------- | ------- | ---------------------------------- | -| **GPT-5.2 Low Reasoning** | 50.52% | Bulk code generation, simple edits | -| **Claude Opus 4.5 (Thinking)** | 22.19% | Complex reasoning, architecture | -| **GPT-5.2 Low Reasoning** (variant) | 16.97% | Additional generation | -| **Claude Opus 4.5** | 8.22% | Targeted complex tasks | -| **SWE-1.5 (Promo)** | 2.09% | Trial/evaluation | +| Model | Usage % | Role | +|-------|---------|------| +| **GPT-5.2 Low Reasoning** | 50.52% | Bulk code generation, simple edits | +| **Claude Opus 4.5 (Thinking)** | 22.19% | Complex reasoning, architecture | +| **GPT-5.2 Low Reasoning** (variant) | 16.97% | Additional generation | +| **Claude Opus 4.5** | 8.22% | Targeted complex tasks | +| **SWE-1.5 (Promo)** | 2.09% | Trial/evaluation | --- ## Strengths ### 1. Extremely productive output - 143,815 lines in 30 days across 21 conversations is exceptional. That's ~6,848 lines per conversation and ~4,794 lines per day. This built out the entire LysnrAI monorepo (6 client apps, 5 backend services, 600+ tests) and the MindLyst KMP foundation. ### 2. Write-heavy workflow (95% Write vs 5% Chat) - 1,470 Write messages vs 71 Chat messages shows a highly action-oriented approach — using Cascade primarily for implementation rather than Q&A. This is the most productive usage pattern. ### 3. Heavy terminal integration (5,893 messages) - ~4 terminal commands per Cascade message indicates extensive build/test/verify cycles. This is a sign of rigorous development — not just generating code, but continuously validating it. ### 4. Good workflow adoption (35 uses) - Custom workflows for starting services, running tests, building releases, etc. are being used regularly. This reduces repetitive work and ensures consistency. ### 5. Memory utilization (20 memories) - Using persistent memory for project context (architecture decisions, rebranding mappings, service configs) avoids re-explaining context across the 21 conversations. ### 6. Focused activity pattern - The heatmap shows concentrated bursts of activity (Dec–Feb) rather than scattered usage. This aligns with the LysnrAI monorepo buildout and MindLyst kickoff — deep focused sprints. --- @@ -69,50 +63,40 @@ The heatmap shows concentrated bursts of activity (Dec–Feb) rather than scatte ## Areas for Improvement ### 1. Tab completions nearly unused (1 acceptance) - **Current:** Only 1 Markdown tab acceptance in the entire period. **Recommendation:** Enable and use Windsurf's inline tab completions for: - - Boilerplate code (imports, function signatures, test setup) - Repetitive patterns (Cosmos CRUD, Fastify route scaffolds) - Variable/method name completion - This alone could save significant keystroke overhead for the ~1% of code you write manually. +This alone could save significant keystroke overhead for the ~1% of code you write manually. ### 2. Zero slash commands used - **Current:** 0 commands used despite having 10+ custom workflows defined. **Recommendation:** Use slash commands (e.g., `/start-all-services`, `/debug-service`, `/test-ios-app`) directly in chat to trigger workflows. Currently, workflows are used (35 times) but commands are at 0 — this suggests workflows are being triggered via other means or the slash command integration isn't configured. ### 3. Chat mode underutilized (5%) - **Current:** 71 Chat messages vs 1,470 Write messages. **Recommendation:** Use Chat mode for: - - **Architecture discussions** before implementing (e.g., "should I use KMP or native for mobile?") - **Code review** — paste code and ask for review before committing - **Debugging strategy** — discuss approach before diving into Write mode - A healthy ratio might be 80/20 Write/Chat rather than 95/5. +A healthy ratio might be 80/20 Write/Chat rather than 95/5. ### 4. Web search barely used (9 searches) - **Current:** Only 9 web searches in 30 days. **Recommendation:** Use web search for: - - Checking latest API docs (Azure Speech SDK, Stripe, Fastify 5) - Verifying deprecation notices before adopting patterns - Finding community solutions for edge cases - This would reduce reliance on training data which may be outdated. +This would reduce reliance on training data which may be outdated. ### 5. MCP integrations not used (0 invocations) - **Current:** No MCP (Model Context Protocol) tool invocations. **Recommendation:** If you have MCP servers configured (e.g., for Azure, GitHub, Cosmos DB), using them could provide real-time data access during development sessions. ### 6. Model selection could be more strategic - **Current:** 50% GPT-5.2 Low Reasoning + 22% Claude Opus 4.5 Thinking. **Recommendation:** - - Use **Claude Opus 4.5 (Thinking)** for: architecture decisions, complex debugging, multi-file refactors, security reviews - Use **GPT-5.2** for: bulk code generation, simple CRUD, test writing, documentation - The current 50/22 split seems reasonable, but consider bumping Claude usage for critical paths (auth, billing, data integrity) where reasoning depth matters more @@ -121,16 +105,16 @@ The heatmap shows concentrated bursts of activity (Dec–Feb) rather than scatte ## Usage Efficiency Score -| Category | Score | Notes | -| ------------------------ | -------- | ------------------------------------------------ | -| **Output volume** | 10/10 | 143K lines in 30 days is extraordinary | -| **Write/Chat balance** | 7/10 | Could use more Chat for planning | -| **Terminal integration** | 10/10 | 5,893 commands shows rigorous verification | -| **Workflow adoption** | 8/10 | 35 uses is good; slash commands at 0 | -| **Tab completions** | 1/10 | Nearly unused — biggest improvement area | -| **Memory usage** | 7/10 | 20 memories is adequate; could store more | -| **Web search** | 4/10 | 9 searches is very low for this volume | -| **Overall** | **7/10** | Highly productive; optimize completions + search | +| Category | Score | Notes | +|----------|-------|-------| +| **Output volume** | 10/10 | 143K lines in 30 days is extraordinary | +| **Write/Chat balance** | 7/10 | Could use more Chat for planning | +| **Terminal integration** | 10/10 | 5,893 commands shows rigorous verification | +| **Workflow adoption** | 8/10 | 35 uses is good; slash commands at 0 | +| **Tab completions** | 1/10 | Nearly unused — biggest improvement area | +| **Memory usage** | 7/10 | 20 memories is adequate; could store more | +| **Web search** | 4/10 | 9 searches is very low for this volume | +| **Overall** | **7/10** | Highly productive; optimize completions + search | --- @@ -147,7 +131,6 @@ The heatmap shows concentrated bursts of activity (Dec–Feb) rather than scatte ## What You Built in This Period With these 21 conversations and 143K lines, you shipped: - - Complete LysnrAI monorepo (347 commits, 6 apps, 5 services, 600+ tests) - MindLyst KMP foundation (shared module, 3 platform UIs, design system) - Full microservices extraction (6 phases) diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_multimodal_memory_agents/WEB_ABUSE_CONTROLS.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_multimodal_memory_agents/WEB_ABUSE_CONTROLS.md index e6aef9ad..5b480b4a 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_multimodal_memory_agents/WEB_ABUSE_CONTROLS.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_multimodal_memory_agents/WEB_ABUSE_CONTROLS.md @@ -76,3 +76,4 @@ Global toggles: - Add per-user quotas (requires auth). - Add SSRF protection to `/api/triage` URL enrichment (block localhost/private IPs, limit redirects, cap response bytes). - Add structured request validation (zod) and centralize API middleware. + diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_voice_ai_agent/CODEX_SESSION_SUMMARY_AND_PLAYBOOK.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_voice_ai_agent/CODEX_SESSION_SUMMARY_AND_PLAYBOOK.md index 5d7400b6..938f3877 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_voice_ai_agent/CODEX_SESSION_SUMMARY_AND_PLAYBOOK.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_docs/learning_voice_ai_agent/CODEX_SESSION_SUMMARY_AND_PLAYBOOK.md @@ -104,3 +104,4 @@ bash scripts/secret-scan-repo.sh bash scripts/check.sh make check ``` + diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_agent_monitoring_fx/build-agent.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_agent_monitoring_fx/build-agent.md index 96b871e7..050ee70b 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_agent_monitoring_fx/build-agent.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_agent_monitoring_fx/build-agent.md @@ -23,7 +23,6 @@ npm run test ### 2. Build the TypeScript // turbo - ```bash cd /Users/sd9235/code/mygh/learning_agent_monitoring_fx/apps/agent npx tsc @@ -32,7 +31,6 @@ npx tsc ### 3. Bundle into single JS file (for SEA) // turbo - ```bash cd /Users/sd9235/code/mygh/learning_agent_monitoring_fx/apps/agent npx esbuild dist/main.js --bundle --platform=node --outfile=dist/agent-bundle.js --external:sharp --external:better-sqlite3 @@ -152,7 +150,6 @@ jobs: ### 8. Verify the binary // turbo - ```bash cd /Users/sd9235/code/mygh/learning_agent_monitoring_fx/apps/agent ./dist/agentlens-macos --server http://localhost:3001 --help 2>&1 || echo "(expected: agent starts or shows usage)" diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_clock/repo_backup-and-push.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_clock/repo_backup-and-push.md new file mode 100644 index 00000000..e6f031ab --- /dev/null +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_clock/repo_backup-and-push.md @@ -0,0 +1,53 @@ +--- +description: Backup main branches then push all repos to origin in sequence +--- + +# Backup & Push All Repos + +Combines `/repo_backup-main-branch` and `/repo_push-repos` into a single sequential workflow. Ideal for end-of-session save-all. + +## Step 1: Backup main branches + +Creates timestamped backup branches with smart duplicate detection. + +// turbo +Run `bash ~/code/mygh/learning_ai_common_plat/scripts/backup-main.sh` from any directory + +## Step 2: Push all repos to origin + +// turbo + +```bash +for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do + echo "━━━ Pushing $repo ━━━" + (cd ~/code/mygh/$repo && git push origin main 2>&1) +done +echo "" +echo "✨ All repos pushed!" +``` + +## What it does: + +1. **Backup** — creates timestamped backup branches, cleans up old ones (7 days), skips duplicates +2. **Push** — pushes `main` to `origin/main` for all 5 repos + +## Repositories: + +- learning_ai_common_plat +- learning_voice_ai_agent +- learning_multimodal_memory_agents +- learning_ai_clock +- learning_ai_fastgap + +## When to use: + +- End of a work session +- Before switching machines +- After a batch of commits across repos +- Anytime you want a safe checkpoint + sync to remote + +## Notes: + +- Backup runs first so the backup branch includes the latest local commits +- Push only pushes `main` — backup branches are pushed by the backup script itself +- If push fails (diverged remote), run `/repo_sync-repos` first to pull diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_clock/repo_backup-main-branch.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_clock/repo_backup-main-branch.md new file mode 100644 index 00000000..3760dc6e --- /dev/null +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_clock/repo_backup-main-branch.md @@ -0,0 +1,34 @@ +--- +description: Smart backup of main branches with duplicate detection +--- + +# Backup Main Branch + +Creates smart backups of main branches across all repositories. + +// turbo +Run `bash ~/code/mygh/learning_ai_common_plat/scripts/backup-main.sh` from any directory + +## What it does: + +1. Checks each repository for changes +2. Skips backup if main hasn't changed since last backup +3. Creates timestamped backup branch +4. Cleans up old backups (keeps 7 days) +5. Returns to main branch + +## Repositories covered: + +- learning_ai_common_plat +- learning_voice_ai_agent +- learning_multimodal_memory_agents +- learning_ai_clock +- learning_ai_fastgap + +## Features: + +- Smart duplicate detection +- Automatic cleanup of old backups +- Multi-repo support +- Safe operations (always returns to main) +- Color-coded output for clarity diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_clock/repo_commit-workspace.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_clock/repo_commit-workspace.md new file mode 100644 index 00000000..19616020 --- /dev/null +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_clock/repo_commit-workspace.md @@ -0,0 +1,50 @@ +--- +description: Commit all workspace changes in logical order with intelligent messages +--- + +# Commit Workspace + +Commits all pending changes across all 5 workspace repos in logical order. + +## Step 1: Check status of all repos + +// turbo + +```bash +for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do + echo "━━━ $repo ━━━" + (cd ~/code/mygh/$repo && git status --short) + echo "" +done +``` + +## Step 2: Stage and commit each repo + +For each repo with changes: +1. Review the diff +2. Stage all changes with `git add -A` +3. Write a conventional commit message: `type(scope): description` +4. Commit + +## Commit message conventions: + +- `feat(scope):` — new feature +- `fix(scope):` — bug fix +- `refactor(scope):` — code restructuring +- `docs(scope):` — documentation only +- `test(scope):` — adding/updating tests +- `chore(scope):` — maintenance tasks + +## Order: + +1. **learning_ai_common_plat** — shared packages first (others may depend on it) +2. **learning_voice_ai_agent** — LysnrAI product repo +3. **learning_multimodal_memory_agents** — MindLyst product repo +4. **learning_ai_clock** — ChronoMind product repo +5. **learning_ai_fastgap** — NomGap product repo + +## Notes: + +- Always commit common_plat first since other repos may reference its packages +- Use `--no-verify` only if pre-commit hooks are blocking on non-code issues +- After committing, run `/repo_push-repos` to push all to origin diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_clock/repo_push-repos.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_clock/repo_push-repos.md new file mode 100644 index 00000000..3238edf7 --- /dev/null +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_clock/repo_push-repos.md @@ -0,0 +1,42 @@ +--- +description: Push local main branch to origin for all 5 workspace repos +--- + +# Push Repos + +Pushes local `main` to `origin/main` for all workspace repositories. + +// turbo + +```bash +for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do + echo "━━━ $repo ━━━" + (cd ~/code/mygh/$repo && git push origin main) +done +``` + +## What it does: + +1. Iterates over all 5 workspace repos +2. Runs `git push origin main` in each +3. Fails fast if a repo has diverged from remote (resolve with rebase manually) + +## Repositories: + +- learning_ai_common_plat +- learning_voice_ai_agent +- learning_multimodal_memory_agents +- learning_ai_clock +- learning_ai_fastgap + +## When to use: + +- After committing a batch of changes locally +- After running `/repo_commit-workspace` +- To sync local work to GitHub before switching machines + +## Notes: + +- Only pushes `main` — does not push other branches +- Will fail safely if remote has diverged — run `/repo_sync-repos` first then rebase +- Use `/repo_sync-repos` to pull before pushing if you've been working on another machine diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_clock/repo_sync-repos.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_clock/repo_sync-repos.md new file mode 100644 index 00000000..8fd52aed --- /dev/null +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_clock/repo_sync-repos.md @@ -0,0 +1,42 @@ +--- +description: Pull latest from origin main across all 5 workspace repos +--- + +# Sync Repos + +Pulls the latest changes from `origin/main` for all workspace repositories. + +// turbo + +```bash +for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do + echo "━━━ $repo ━━━" + (cd ~/code/mygh/$repo && git pull --ff-only origin main) +done +``` + +## What it does: + +1. Iterates over all 5 workspace repos +2. Runs `git pull --ff-only origin main` in each +3. Fails fast if there are local divergent commits (use `git pull --rebase` manually in that case) + +## Repositories: + +- learning_ai_common_plat +- learning_voice_ai_agent +- learning_multimodal_memory_agents +- learning_ai_clock +- learning_ai_fastgap + +## When to use: + +- Starting a new work session +- After pushing changes from another machine +- Before running `/repo_backup-main-branch` + +## Notes: + +- Uses `--ff-only` to prevent accidental merge commits +- If a repo has uncommitted changes, `git pull` will still work (fast-forward only) +- If a repo has diverged from origin, the pull will fail safely — resolve manually diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_common_plat/repo_backup-and-push.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_common_plat/repo_backup-and-push.md index f448a6c1..33d47a1e 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_common_plat/repo_backup-and-push.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_common_plat/repo_backup-and-push.md @@ -18,7 +18,7 @@ Run `bash scripts/backup-main.sh` from any repository root // turbo ```bash -for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock; do +for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do echo "━━━ Pushing $repo ━━━" (cd ~/code/mygh/$repo && git push origin main 2>&1) done @@ -29,7 +29,7 @@ echo "✨ All repos pushed!" ## What it does: 1. **Backup** — creates timestamped backup branches, cleans up old ones (7 days), skips duplicates -2. **Push** — pushes `main` to `origin/main` for all 3 repos +2. **Push** — pushes `main` to `origin/main` for all 5 repos ## Repositories: @@ -37,6 +37,7 @@ echo "✨ All repos pushed!" - learning_voice_ai_agent - learning_multimodal_memory_agents - learning_ai_clock +- learning_ai_fastgap ## When to use: diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_common_plat/repo_backup-main-branch.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_common_plat/repo_backup-main-branch.md index 85c80e91..8e81418d 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_common_plat/repo_backup-main-branch.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_common_plat/repo_backup-main-branch.md @@ -23,6 +23,7 @@ Run `bash scripts/backup-main.sh` from any repository root - learning_voice_ai_agent - learning_multimodal_memory_agents - learning_ai_clock +- learning_ai_fastgap ## Features: diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_common_plat/repo_push-repos.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_common_plat/repo_push-repos.md index 97f18e05..3238edf7 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_common_plat/repo_push-repos.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_common_plat/repo_push-repos.md @@ -1,5 +1,5 @@ --- -description: Push local main branch to origin for all 4 workspace repos +description: Push local main branch to origin for all 5 workspace repos --- # Push Repos @@ -9,7 +9,7 @@ Pushes local `main` to `origin/main` for all workspace repositories. // turbo ```bash -for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock; do +for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do echo "━━━ $repo ━━━" (cd ~/code/mygh/$repo && git push origin main) done @@ -17,7 +17,7 @@ done ## What it does: -1. Iterates over all 4 workspace repos +1. Iterates over all 5 workspace repos 2. Runs `git push origin main` in each 3. Fails fast if a repo has diverged from remote (resolve with rebase manually) @@ -27,6 +27,7 @@ done - learning_voice_ai_agent - learning_multimodal_memory_agents - learning_ai_clock +- learning_ai_fastgap ## When to use: diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_common_plat/repo_sync-repos.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_common_plat/repo_sync-repos.md index dd3fca34..8fd52aed 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_common_plat/repo_sync-repos.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_ai_common_plat/repo_sync-repos.md @@ -1,5 +1,5 @@ --- -description: Pull latest from origin main across all 4 workspace repos +description: Pull latest from origin main across all 5 workspace repos --- # Sync Repos @@ -9,7 +9,7 @@ Pulls the latest changes from `origin/main` for all workspace repositories. // turbo ```bash -for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock; do +for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do echo "━━━ $repo ━━━" (cd ~/code/mygh/$repo && git pull --ff-only origin main) done @@ -17,7 +17,7 @@ done ## What it does: -1. Iterates over all 4 workspace repos +1. Iterates over all 5 workspace repos 2. Runs `git pull --ff-only origin main` in each 3. Fails fast if there are local divergent commits (use `git pull --rebase` manually in that case) @@ -27,6 +27,7 @@ done - learning_voice_ai_agent - learning_multimodal_memory_agents - learning_ai_clock +- learning_ai_fastgap ## When to use: diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_multimodal_memory_agents/mobile-code-quality.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_multimodal_memory_agents/mobile-code-quality.md index 537b19f3..784e99a4 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_multimodal_memory_agents/mobile-code-quality.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_multimodal_memory_agents/mobile-code-quality.md @@ -14,7 +14,6 @@ description: Verify iOS/mobile code compiles, all files are in Xcode targets, an ### Step 1.1 — Verify git clean // turbo - ```bash cd $HOME/code/mygh/learning_multimodal_memory_agents && git diff --quiet && git diff --cached --quiet && echo "Clean" || echo "WARNING: uncommitted changes" ``` @@ -39,7 +38,6 @@ cd $HOME/code/mygh/learning_multimodal_memory_agents/mindlyst-native && ./gradle ### Step 1.4 — Web build check // turbo - ```bash cd $HOME/code/mygh/learning_multimodal_memory_agents/mindlyst-native/web && npx next build 2>&1 | tail -10 ``` @@ -51,7 +49,6 @@ cd $HOME/code/mygh/learning_multimodal_memory_agents/mindlyst-native/web && npx ### Step 2.1 — Verify git clean // turbo - ```bash cd $HOME/code/mygh/learning_voice_ai_agent && git diff --quiet && git diff --cached --quiet && echo "Clean" || echo "WARNING: uncommitted changes" ``` @@ -59,7 +56,6 @@ cd $HOME/code/mygh/learning_voice_ai_agent && git diff --quiet && git diff --cac ### Step 2.2 — Install CocoaPods // turbo - ```bash cd $HOME/code/mygh/learning_voice_ai_agent/mobile_app/ios && pod install ``` @@ -85,7 +81,6 @@ cd $HOME/code/mygh/learning_voice_ai_agent && xcodebuild build \ ### Step 2.4 — Verify pbxproj ↔ filesystem consistency // turbo - ```bash cd $HOME/code/mygh/learning_voice_ai_agent && \ echo "=== Checking LysnrKeyboard ===" && \ @@ -111,7 +106,6 @@ else echo "$missing file(s) may be missing"; fi ### Step 2.5 — Check for common Swift issues // turbo - ```bash cd $HOME/code/mygh/learning_voice_ai_agent && \ echo "=== Hardcoded secrets ===" && \ @@ -126,7 +120,6 @@ echo "Keyboard extension: $count print() calls (should be 0)" ### Step 2.6 — Verify Info.plist + entitlements // turbo - ```bash cd $HOME/code/mygh/learning_voice_ai_agent && \ for key in NSMicrophoneUsageDescription NSSpeechRecognitionUsageDescription NSExtensionPointIdentifier RequestsOpenAccess; do @@ -143,32 +136,32 @@ else echo "ERROR: App Group entitlement MISSING"; fi ### What This Catches -| Check | Prevents | -| --------------------- | -------------------------------------------------- | -| KMP compile | Shared business logic errors | -| Build all iOS targets | Missing source files in Xcode project | -| pbxproj consistency | New .swift files on disk but not in target | -| Secret scan | Hardcoded API keys in Swift source | -| print() check | Debug logging in production keyboard extension | -| Info.plist check | Missing privacy descriptions → App Store rejection | -| Entitlements check | Missing App Group → keyboard can't share data | +| Check | Prevents | +|-------|----------| +| KMP compile | Shared business logic errors | +| Build all iOS targets | Missing source files in Xcode project | +| pbxproj consistency | New .swift files on disk but not in target | +| Secret scan | Hardcoded API keys in Swift source | +| print() check | Debug logging in production keyboard extension | +| Info.plist check | Missing privacy descriptions → App Store rejection | +| Entitlements check | Missing App Group → keyboard can't share data | ### Key Files -| File | Purpose | -| ----------------------------------------------------------------------------- | -------------------------- | -| `mindlyst-native/shared/` | KMP shared business logic | -| `mindlyst-native/web/` | Next.js web dashboard | -| `../learning_voice_ai_agent/mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj` | Xcode project config | -| `../learning_voice_ai_agent/mobile_app/ios/LysnrKeyboard/` | Keyboard extension sources | -| `../learning_voice_ai_agent/mobile_app/ios/Podfile` | CocoaPods deps | +| File | Purpose | +|------|---------| +| `mindlyst-native/shared/` | KMP shared business logic | +| `mindlyst-native/web/` | Next.js web dashboard | +| `../learning_voice_ai_agent/mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj` | Xcode project config | +| `../learning_voice_ai_agent/mobile_app/ios/LysnrKeyboard/` | Keyboard extension sources | +| `../learning_voice_ai_agent/mobile_app/ios/Podfile` | CocoaPods deps | ### Troubleshooting -| Problem | Fix | -| ------------------------- | ------------------------------------------------------------------------ | -| KMP compile fails | Check `gradle/libs.versions.toml` for version conflicts | -| Xcode build fails | Use `.xcworkspace`, run `pod install` first | +| Problem | Fix | +|---------|-----| +| KMP compile fails | Check `gradle/libs.versions.toml` for version conflicts | +| Xcode build fails | Use `.xcworkspace`, run `pod install` first | | File missing from pbxproj | Add to PBXFileReference + PBXGroup + PBXBuildFile + PBXSourcesBuildPhase | -| Android SDK missing | Set `sdk.dir` in `local.properties` or skip Android steps | -| Simulator not found | Run `xcrun simctl list devices` to see available simulators | +| Android SDK missing | Set `sdk.dir` in `local.properties` or skip Android steps | +| Simulator not found | Run `xcrun simctl list devices` to see available simulators | diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_multimodal_memory_agents/release-testflight-mindlyst.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_multimodal_memory_agents/release-testflight-mindlyst.md index 24ca6094..265f53ca 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_multimodal_memory_agents/release-testflight-mindlyst.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_multimodal_memory_agents/release-testflight-mindlyst.md @@ -49,7 +49,6 @@ See `iosApp/README_SETUP.md` for more details. ### 4. Verify Gradle/KMP builds // turbo - ```bash cd mindlyst-native && ./gradlew :shared:compileKotlinIosSimulatorArm64 ``` @@ -67,7 +66,6 @@ cd mindlyst-native && ./gradlew :shared:compileKotlinIosArm64 ``` For simulator testing: - ```bash cd mindlyst-native && ./gradlew :shared:compileKotlinIosSimulatorArm64 ``` @@ -75,7 +73,6 @@ cd mindlyst-native && ./gradlew :shared:compileKotlinIosSimulatorArm64 ### 2. Embed the shared framework in Xcode If not already linked: - ```bash cd mindlyst-native && ./gradlew :shared:embedAndSignAppleFrameworkForXcode ``` @@ -90,7 +87,6 @@ Or manually: open Xcode → Target → General → Build. ### 4. Clean build folder // turbo - ```bash xcodebuild clean -project mindlyst-native/MindLyst.xcodeproj -scheme MindLyst -configuration Release 2>&1 | tail -3 ``` @@ -144,24 +140,24 @@ The `app-store-connect` export method auto-uploads the IPA. ### Key Paths -| Path | Purpose | -| ------------------------------------------- | ---------------------------------------- | -| `mindlyst-native/iosApp/` | Swift UI source files | -| `mindlyst-native/shared/` | KMP shared module (business logic) | -| `mindlyst-native/shared/build.gradle.kts` | KMP build config (iOS targets) | -| `mindlyst-native/gradle/libs.versions.toml` | Version catalog | -| `mindlyst-native/MindLyst.xcodeproj/` | Xcode project (must be created manually) | -| `scripts/MindLystExportOptions.plist` | Export options for TestFlight upload | +| Path | Purpose | +|------|---------| +| `mindlyst-native/iosApp/` | Swift UI source files | +| `mindlyst-native/shared/` | KMP shared module (business logic) | +| `mindlyst-native/shared/build.gradle.kts` | KMP build config (iOS targets) | +| `mindlyst-native/gradle/libs.versions.toml` | Version catalog | +| `mindlyst-native/MindLyst.xcodeproj/` | Xcode project (must be created manually) | +| `scripts/MindLystExportOptions.plist` | Export options for TestFlight upload | ### Build Identity -| Field | Value | -| ------------- | ----------------------- | -| Team ID | `748N7QPX7J` | -| Bundle ID | `com.mindlyst.MindLyst` | -| Signing | Automatic | -| KMP Framework | `shared` (static) | -| Min iOS | 16.0 | +| Field | Value | +|-------|-------| +| Team ID | `748N7QPX7J` | +| Bundle ID | `com.mindlyst.MindLyst` | +| Signing | Automatic | +| KMP Framework | `shared` (static) | +| Min iOS | 16.0 | ### KMP Architecture @@ -177,12 +173,12 @@ All business logic lives in `shared/src/commonMain/`. iOS code in `iosApp/` is a ### Troubleshooting -| Problem | Fix | -| ----------------------------- | ------------------------------------------------------------------------ | -| "No signing certificate" | Xcode → Settings → Accounts → Manage Certificates → + Apple Distribution | -| "Provisioning profile" error | Xcode → Target → Signing → Enable "Automatically manage signing" | -| "Build number already exists" | Increment build number in step 3 | -| KMP build fails | Check Java 17: `java -version`. Install: `brew install openjdk@17` | -| "shared.framework not found" | Run `./gradlew :shared:embedAndSignAppleFrameworkForXcode` | -| Gradle SSL proxy error | Build on home network (corporate proxy blocks Gradle repos) | -| Processing >30 min | Check [Apple system status](https://developer.apple.com/system-status/) | +| Problem | Fix | +|---------|-----| +| "No signing certificate" | Xcode → Settings → Accounts → Manage Certificates → + Apple Distribution | +| "Provisioning profile" error | Xcode → Target → Signing → Enable "Automatically manage signing" | +| "Build number already exists" | Increment build number in step 3 | +| KMP build fails | Check Java 17: `java -version`. Install: `brew install openjdk@17` | +| "shared.framework not found" | Run `./gradlew :shared:embedAndSignAppleFrameworkForXcode` | +| Gradle SSL proxy error | Build on home network (corporate proxy blocks Gradle repos) | +| Processing >30 min | Check [Apple system status](https://developer.apple.com/system-status/) | diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_multimodal_memory_agents/repo_scan-repo-and-update-windsurf-context.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_multimodal_memory_agents/repo_scan-repo-and-update-windsurf-context.md index 17548e02..a7c6f00f 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_multimodal_memory_agents/repo_scan-repo-and-update-windsurf-context.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_multimodal_memory_agents/repo_scan-repo-and-update-windsurf-context.md @@ -52,69 +52,52 @@ This workflow scans the entire MindLyst repository, builds a comprehensive under 7. **Run verification commands** (non-destructive, read-only): // turbo - `cd mindlyst-native && ./gradlew projects` — verify module structure - // turbo + // turbo - `cd mindlyst-native/web && cat package.json | head -20` — verify web deps 8. **Generate WINDSURF_CONTEXT.md** at the repo root with the following sections: ```markdown # WINDSURF_CONTEXT.md - > Auto-generated by /scan-repo workflow. Last updated: > Re-run with: /scan-repo-and-update-windsurf-context ## Project Summary - <1-paragraph summary of MindLyst, current state, and what phase we're in> ## Architecture - ## Module Map - ## Shared Logic (KMP commonMain) - ## Platform UI State - ### Android - - ### iOS - - ### Web - ## Design Tokens - ## Dependencies - ## Build Status - ## Implementation Progress - ## Open Issues / TODOs - ## Key Files Quick Reference - ``` @@ -123,13 +106,11 @@ This workflow scans the entire MindLyst repository, builds a comprehensive under 10. **Verify** the generated file is complete and accurate by re-reading it. 11. **Commit and push**: - -- `git add WINDSURF_CONTEXT.md` -- `git commit -m "docs: update WINDSURF_CONTEXT.md via /scan-repo-and-update-windsurf-context"` -- `git push` + - `git add WINDSURF_CONTEXT.md` + - `git commit -m "docs: update WINDSURF_CONTEXT.md via /scan-repo-and-update-windsurf-context"` + - `git push` ## Notes - - This workflow creates `WINDSURF_CONTEXT.md` if it doesn't exist, or fully replaces it if it does - It commits and pushes the updated file automatically - Run this periodically to keep context fresh, especially after major changes diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/debug-service.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/debug-service.md index 062f93ef..12ce4bb0 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/debug-service.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/debug-service.md @@ -9,13 +9,12 @@ Follow these steps to diagnose and fix a failing service or endpoint. ### 1. Identify the failing service Check which service is affected: - - Backend API (Python/FastAPI) → `backend/src/` - Platform Service (Fastify) → `../learning_ai_common_plat/services/platform-service/src/` - Extraction Service (Fastify) → `../learning_ai_common_plat/services/extraction-service/src/` -- Admin Dashboard (Next.js) → `admin-dashboard-web/src/` +- Admin Dashboard (Next.js) → `../learning_ai_common_plat/dashboards/admin-web/src/` - User Dashboard (Next.js) → `user-dashboard-web/src/` -- Tracker Dashboard (Next.js) → `tracker-dashboard-web/src/` +- Tracker Dashboard (Next.js) → `../learning_ai_common_plat/dashboards/tracker-web/src/` ### 2. Check health @@ -29,7 +28,6 @@ For local dev: Run `tail -50 .logs/backend.log .logs/platform-service.log 2>/dev/null | head -100` For Docker: - ```bash docker compose logs --tail=50 ``` @@ -54,7 +52,6 @@ docker compose logs --tail=50 Run `python -m pytest tests/ backend/tests/ -v --tb=short -x` For TypeScript services: - ```bash cd ../learning_ai_common_plat && pnpm --filter @lysnrai/ test ``` diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/docker-compose.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/docker-compose.md index 166b0b25..9dde240e 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/docker-compose.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/docker-compose.md @@ -10,9 +10,8 @@ Use this on a home network or CI where there is no corporate proxy. - Docker Desktop running - `.env` at repo root (Cosmos DB, JWT, Stripe, Azure credentials) -- `admin-dashboard-web/.env.local` (Cosmos DB, JWT) - `user-dashboard-web/.env.local` (Cosmos DB, JWT) -- `tracker-dashboard-web/.env.local` (Cosmos DB, JWT) +- Admin + Tracker dashboards now live in `../learning_ai_common_plat/dashboards/` and use shared `.env` ### Start @@ -24,29 +23,25 @@ docker compose logs -f # tail all logs ### Services -| Service | Container | Port | Image / Dockerfile | -| ----------------- | ------------------- | -------- | ----------------------------------------------------------------- | -| Loki | `loki` | 3100 | `grafana/loki:3.3.2` | -| Grafana | `grafana` | 3000 | `grafana/grafana:11.4.0` | -| Traefik Gateway | `gateway` | 80, 8080 | `traefik:v3.3` | -| Backend API | `backend` | 8000 | `backend/Dockerfile` | -| Admin Dashboard | `admin-dashboard` | 3001 | `admin-dashboard-web/Dockerfile` | -| User Dashboard | `user-dashboard` | 3002 | `user-dashboard-web/Dockerfile` | -| Tracker Dashboard | `tracker-dashboard` | 3003 | `tracker-dashboard-web/Dockerfile` | -| Growth Service | `growth-service` | 4001 | `../learning_ai_common_plat/services/growth-service/Dockerfile` | -| Billing Service | `billing-service` | 4002 | `../learning_ai_common_plat/services/billing-service/Dockerfile` | -| Platform Service | `platform-service` | 4003 | `../learning_ai_common_plat/services/platform-service/Dockerfile` | -| Tracker Service | `tracker-service` | 4004 | `../learning_ai_common_plat/services/tracker-service/Dockerfile` | +| Service | Container | Port | Image / Dockerfile | +|---------|-----------|------|--------------------| +| Loki | `loki` | 3100 | `grafana/loki:3.3.2` | +| Grafana | `grafana` | 3000 | `grafana/grafana:11.4.0` | +| Traefik Gateway | `gateway` | 80, 8080 | `traefik:v3.3` | +| Backend API | `backend` | 8000 | `backend/Dockerfile` | +| Admin Dashboard | `admin-dashboard` | 3001 | `../learning_ai_common_plat/dashboards/admin-web/` | +| User Dashboard | `user-dashboard` | 3002 | `user-dashboard-web/Dockerfile` | +| Tracker Dashboard | `tracker-dashboard` | 3003 | `../learning_ai_common_plat/dashboards/tracker-web/` | +| Platform Service | `platform-service` | 4003 | `../learning_ai_common_plat/services/platform-service/Dockerfile` | +| Extraction Service | `extraction-service` | 4005 | `../learning_ai_common_plat/services/extraction-service/Dockerfile` | ### Traefik Routing (port 80) -| PathPrefix | Routes to | -| ------------------------------------------------------------------------------------------------- | ---------------- | -| `/api` (catch-all), `/health` | Backend API | -| `/api/invitations`, `/api/referrals`, `/api/promos` | Growth Service | -| `/api/subscriptions`, `/api/payments`, `/api/usage`, `/api/plans`, `/api/licenses`, `/api/stripe` | Billing Service | -| `/api/auth`, `/api/audit`, `/api/notifications`, `/api/flags`, `/api/ratelimit` | Platform Service | -| `/api/items`, `/api/tracker` | Tracker Service | +| PathPrefix | Routes to | +|------------|-----------| +| `/api` (catch-all), `/health` | Backend API | +| `/api/auth`, `/api/audit`, `/api/notifications`, `/api/flags`, `/api/ratelimit`, `/api/subscriptions`, `/api/invitations`, `/api/stripe`, `/public` | Platform Service (consolidated) | +| `/api/extract`, `/api/tasks` | Extraction Service | ### Stop diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/generate-store-assets.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/generate-store-assets.md index cc13c990..f747a65d 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/generate-store-assets.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/generate-store-assets.md @@ -7,37 +7,35 @@ description: Regenerate all app store artwork (icons, screenshots, feature graph All 73 store assets are generated programmatically from a single Python script. // turbo - 1. Run `python3 assets/generate-store-assets.py` from the repo root ### Output (73 PNGs) -| Category | Count | Directory | -| --------------- | ----- | ---------------------------------------------------------------------------------- | -| App Icons | 36 | `assets/store/icons/` — iOS (13), Android (6), macOS (7), Windows (5), Favicon (5) | -| Screenshots | 32 | `assets/store/screenshots/{ios,android,mac,windows}/` — 4 screens × dark+light | -| Feature Graphic | 1 | `assets/store/feature/feature-graphic-1024x500.png` | -| Splash Screens | 4 | `assets/store/splash/` | +| Category | Count | Directory | +|----------|-------|-----------| +| App Icons | 36 | `assets/store/icons/` — iOS (13), Android (6), macOS (7), Windows (5), Favicon (5) | +| Screenshots | 32 | `assets/store/screenshots/{ios,android,mac,windows}/` — 4 screens × dark+light | +| Feature Graphic | 1 | `assets/store/feature/feature-graphic-1024x500.png` | +| Splash Screens | 4 | `assets/store/splash/` | ### Customizing Design Edit the color palette at the top of `assets/generate-store-assets.py`: -| Variable | Default | Purpose | -| --------------- | --------- | ------------------------------------- | -| `GREEN_PRIMARY` | `#2E7D32` | Icon circle, badges, section headers | -| `GREEN_ACCENT` | `#50FA7B` | Glowing text, active tabs, highlights | -| `DARK_BG` | `#1E1E2E` | Dark mode background | -| `DARK_SURFACE` | `#282A36` | Cards, inputs, tab bar | -| `MUTED` | `#6272A4` | Secondary text, timestamps | -| `CYAN` | `#8BE9FD` | Transcript card accents | +| Variable | Default | Purpose | +|----------|---------|---------| +| `GREEN_PRIMARY` | `#2E7D32` | Icon circle, badges, section headers | +| `GREEN_ACCENT` | `#50FA7B` | Glowing text, active tabs, highlights | +| `DARK_BG` | `#1E1E2E` | Dark mode background | +| `DARK_SURFACE` | `#282A36` | Cards, inputs, tab bar | +| `MUTED` | `#6272A4` | Secondary text, timestamps | +| `CYAN` | `#8BE9FD` | Transcript card accents | After changing colors, re-run step 1 to regenerate all assets. ### Icon Design The app icon is a **waveform-in-circle** with a green glow on dark background. Customize in `generate_app_icon()`: - - Circle radius: `size * 0.28` - Waveform bars: `num_bars=7`, heights pattern `[0.3, 0.5, 0.75, 1.0, 0.75, 0.5, 0.3]` - Corner radius: `size * 0.22` @@ -46,7 +44,6 @@ The app icon is a **waveform-in-circle** with a green glow on dark background. C ### Screenshot Content Each screen function has hardcoded sample data (user name, transcript titles, word counts). Edit: - - `generate_screenshot_home()` — greeting, stats, activity list - `generate_screenshot_record()` — timer, live text preview - `generate_screenshot_history()` — transcript cards with dates diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/mobile-code-quality.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/mobile-code-quality.md index 738f98c6..7f79d3f9 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/mobile-code-quality.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/mobile-code-quality.md @@ -12,7 +12,6 @@ or after adding/renaming Swift files. ### Step 1 — Verify git working tree is clean // turbo - ```bash git diff --quiet && git diff --cached --quiet && echo "Working tree clean" || echo "WARNING: uncommitted changes" ``` @@ -20,7 +19,6 @@ git diff --quiet && git diff --cached --quiet && echo "Working tree clean" || ec ### Step 2 — Install CocoaPods (if needed) // turbo - ```bash cd mobile_app/ios && pod install --repo-update && cd ../.. ``` @@ -49,7 +47,6 @@ xcodebuild build \ Check that every `.swift` file on disk in the keyboard extension directory is listed in the Xcode project. // turbo - ```bash echo "=== Checking LysnrKeyboard target source consistency ===" for f in mobile_app/ios/LysnrKeyboard/*.swift; do @@ -80,7 +77,6 @@ fi ### Step 5 — Check for common Swift issues // turbo - ```bash echo "=== Checking for hardcoded secrets ===" grep -rn --include='*.swift' -E '(AZURE_SPEECH_KEY|api_key|secret|password)\s*=' mobile_app/ios/ \ @@ -104,7 +100,6 @@ grep -rn --include='*.swift' '[^?]![^=]' mobile_app/ios/LysnrKeyboard/*.swift \ ### Step 6 — Verify Info.plist keys // turbo - ```bash echo "=== Checking required Info.plist keys ===" for key in NSMicrophoneUsageDescription NSSpeechRecognitionUsageDescription NSExtensionPointIdentifier RequestsOpenAccess; do @@ -129,22 +124,22 @@ fi ### What This Catches -| Check | Prevents | -| ------------------- | ----------------------------------------------------------------- | -| Build all targets | Missing source files in Xcode project (e.g. LysnrTelemetry.swift) | -| pbxproj consistency | New .swift files added to disk but not to Xcode target | -| Secret scan | Hardcoded API keys in Swift source | -| print() check | Debug logging in production keyboard extension | -| Force-unwrap check | Runtime crashes from unsafe unwraps | -| Info.plist check | Missing privacy descriptions → App Store rejection | -| Entitlements check | Missing App Group → keyboard can't share data with main app | +| Check | Prevents | +|-------|----------| +| Build all targets | Missing source files in Xcode project (e.g. LysnrTelemetry.swift) | +| pbxproj consistency | New .swift files added to disk but not to Xcode target | +| Secret scan | Hardcoded API keys in Swift source | +| print() check | Debug logging in production keyboard extension | +| Force-unwrap check | Runtime crashes from unsafe unwraps | +| Info.plist check | Missing privacy descriptions → App Store rejection | +| Entitlements check | Missing App Group → keyboard can't share data with main app | ### Key Files -| File | Purpose | -| --------------------------------------------------------- | ---------------------------------------------- | -| `mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj` | Xcode project (targets, sources, build phases) | -| `mobile_app/ios/LysnrKeyboard/` | Keyboard extension sources | -| `mobile_app/ios/LysnrKeyboard/Info.plist` | Extension config + privacy descriptions | -| `mobile_app/ios/LysnrKeyboard/LysnrKeyboard.entitlements` | App Group entitlement | -| `mobile_app/ios/Podfile` | CocoaPods dependencies | +| File | Purpose | +|------|---------| +| `mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj` | Xcode project (targets, sources, build phases) | +| `mobile_app/ios/LysnrKeyboard/` | Keyboard extension sources | +| `mobile_app/ios/LysnrKeyboard/Info.plist` | Extension config + privacy descriptions | +| `mobile_app/ios/LysnrKeyboard/LysnrKeyboard.entitlements` | App Group entitlement | +| `mobile_app/ios/Podfile` | CocoaPods dependencies | diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/production-readiness.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/production-readiness.md index efabaecb..eb9abff2 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/production-readiness.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/production-readiness.md @@ -8,7 +8,7 @@ date: 2025-02-12 > Runs comprehensive checks across all 3 repos, fixes failures as they occur, commits + pushes incrementally. > **NOTE**: GitHub Actions are temporarily disabled due to billing issues. Please run these checks manually. > Order: common_plat → voice_agent → mindlyst (dependency-safe). -> +> > See MANUAL_CI.md in each repo for quick pre-push checks. ## Phase 1: learning_ai_common_plat (shared packages + services) @@ -56,9 +56,8 @@ for pkg in packages/*/dist; do node -e "require('./$pkg/index.js')" 2>/dev/null ```bash # 1. Install dashboards (requires common_plat built) -cd $HOME/code/mygh/learning_voice_ai_agent/admin-dashboard-web && npm install -cd ../user-dashboard-web && npm install -cd ../tracker-dashboard-web && npm install +# NOTE: admin-dashboard-web and tracker-dashboard-web moved to learning_ai_common_plat/dashboards/ +cd $HOME/code/mygh/learning_voice_ai_agent/user-dashboard-web && npm install # 2. Type-check all dashboards npx tsc --noEmit @@ -124,11 +123,10 @@ cd .. bash scripts/build.sh # If fails: fix, then git add . && git commit -m "fix(desktop): build fixes" && git push -# 16. E2E tests for all dashboards -cd admin-dashboard-web && npm run test:e2e -cd ../user-dashboard-web && npm run test:e2e -cd ../tracker-dashboard-web && npm run test:e2e -# If fails: fix, commit push per dashboard +# 16. E2E tests for user dashboard +cd user-dashboard-web && npm run test:e2e +# If fails: fix, commit push +# NOTE: admin + tracker E2E tests now run from learning_ai_common_plat/dashboards/ ``` ## Phase 3: learning_multimodal_memory_agents (MindLyst) @@ -197,24 +195,23 @@ npx tsx services/monitoring/health-check.ts ## Enhanced Coverage Summary (Post-Quick Wins) -| Component | Type-Check | Lint | Format | Unit Tests | Coverage | Build | Bundle | E2E | Security | -| ------------------------ | ---------- | ---- | ------ | ---------- | -------- | ----- | ------ | --- | -------- | -| **common_plat packages** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | -| **common_plat services** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | -| **admin-dashboard** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **user-dashboard** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **tracker-dashboard** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| **desktop app** | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | -| **backend API** | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | -| **mindlyst shared** | ✅ | 📱 | 📱 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | -| **mindlyst web** | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | -| **mindlyst android** | ✅ | 📱 | 📱 | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | -| **mindlyst ios** | ❌ | 📱 | 📱 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Component | Type-Check | Lint | Format | Unit Tests | Coverage | Build | Bundle | E2E | Security | +|-----------|------------|------|--------|------------|----------|-------|--------|-----|----------| +| **common_plat packages** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | +| **common_plat services** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | +| **admin-dashboard** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **user-dashboard** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **tracker-dashboard** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| **desktop app** | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | +| **backend API** | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | +| **mindlyst shared** | ✅ | 📱 | 📱 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | +| **mindlyst web** | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | +| **mindlyst android** | ✅ | 📱 | 📱 | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | +| **mindlyst ios** | ❌ | 📱 | 📱 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 📱 = Available via dedicated mobile workflow (.windsurf/workflows/mobile-code-quality.md) **All Quick Wins Implemented:** - - ✅ Prettier formatting (consistent across all repos) - ✅ ESLint for common_plat (12 projects now linted) - ✅ Test coverage with 80% threshold enforcement @@ -227,7 +224,7 @@ npx tsx services/monitoring/health-check.ts ## Notes -- **Commit message pattern**: +- **Commit message pattern**: - `fix(scope): type-check fixes` - `test(scope): fix failing tests` - `fix(python): lint/format fixes` diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/release-desktop.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/release-desktop.md index 5432e742..19572871 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/release-desktop.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/release-desktop.md @@ -38,20 +38,19 @@ cp .env ~/.lysnrai/.env # create config dir + env file Edit `~/.lysnrai/.env` and fill in real values for: -| Variable | Where to get it | -| ------------------------- | ------------------------------------ | -| `AZURE_SPEECH_KEY` | Azure Portal → Speech Service → Keys | -| `AZURE_SPEECH_REGION` | e.g. `eastus` | -| `AZURE_OPENAI_ENDPOINT` | Azure Portal → OpenAI → Endpoint | -| `AZURE_OPENAI_KEY` | Azure Portal → OpenAI → Keys | -| `AZURE_OPENAI_DEPLOYMENT` | e.g. `gpt-4o-mini` | +| Variable | Where to get it | +|----------|-----------------| +| `AZURE_SPEECH_KEY` | Azure Portal → Speech Service → Keys | +| `AZURE_SPEECH_REGION` | e.g. `eastus` | +| `AZURE_OPENAI_ENDPOINT` | Azure Portal → OpenAI → Endpoint | +| `AZURE_OPENAI_KEY` | Azure Portal → OpenAI → Keys | +| `AZURE_OPENAI_DEPLOYMENT` | e.g. `gpt-4o-mini` | ### 4. (Optional) Set up Apple code signing Only needed if you want to distribute the app to others (notarized). **a) Create a "Developer ID Application" certificate:** - 1. Go to [developer.apple.com/account/resources/certificates](https://developer.apple.com/account/resources/certificates/list) 2. Click **+** → select **Developer ID Application** 3. Create a CSR: open **Keychain Access** → Certificate Assistant → Request a Certificate From a Certificate Authority → Save to Disk @@ -59,7 +58,6 @@ Only needed if you want to distribute the app to others (notarized). 5. Verify: `security find-identity -v -p codesigning | grep "Developer ID"` **b) Generate an app-specific password** (for notarization): - 1. Go to [appleid.apple.com](https://appleid.apple.com) → Sign-In and Security → App-Specific Passwords 2. Generate a new password, label it "LysnrAI Notarization" 3. Keep it handy — the codesign script will prompt for it securely (never stored) @@ -73,7 +71,6 @@ Run these checks before building any platform to catch issues early. ### 0a. Verify clean working tree // turbo - ```bash git diff --quiet && git diff --cached --quiet && echo "Clean" || (echo "ERROR: Uncommitted changes — commit or stash first" && exit 1) ``` @@ -81,7 +78,6 @@ git diff --quiet && git diff --cached --quiet && echo "Clean" || (echo "ERROR: U ### 0b. Activate venv + verify deps // turbo - ```bash source .venv/bin/activate && pip install -e ".[dev]" --quiet ``` @@ -89,7 +85,6 @@ source .venv/bin/activate && pip install -e ".[dev]" --quiet ### 0c. Lint with ruff // turbo - ```bash source .venv/bin/activate && python -m ruff check src/ --select E,F,W --no-fix 2>&1 | tail -20 ``` @@ -103,7 +98,6 @@ source .venv/bin/activate && python -m pytest tests/ -v --tb=short -q 2>&1 | tai ### 0e. Verify imports (quick syntax check) // turbo - ```bash source .venv/bin/activate && python -c "import src.main; print('OK: src.main imports cleanly')" ``` @@ -115,7 +109,6 @@ source .venv/bin/activate && python -c "import src.main; print('OK: src.main imp ### Step 1 — Build the .app bundle // turbo - ```bash bash scripts/build.sh ``` @@ -130,7 +123,6 @@ bash scripts/codesign_macos.sh dist/LysnrAI.app ``` The script interactively prompts for: - - **Apple ID** (default: `saravanakumardb@gmail.com`) - **Team ID** (default: `748N7QPX7J`) - **App-specific password** (secure input, not echoed or stored) @@ -224,37 +216,37 @@ Share these instructions with anyone receiving the app. ### Keyboard Shortcuts -| Action | Windows / Linux | macOS | -| -------------- | ------------------ | ------------------------- | +| Action | Windows / Linux | macOS | +|--------|----------------|-------| | Dictate (hold) | `Ctrl+Shift+Space` | `Cmd+Shift+Space` or `Fn` | -| History | `Ctrl+Shift+H` | `Cmd+Shift+H` | -| Undo paste | `Ctrl+Shift+Z` | `Cmd+Shift+Z` | -| Stats | `Ctrl+Shift+S` | `Cmd+Shift+S` | -| Shortcuts help | `Ctrl+Shift+K` | `Cmd+Shift+K` | +| History | `Ctrl+Shift+H` | `Cmd+Shift+H` | +| Undo paste | `Ctrl+Shift+Z` | `Cmd+Shift+Z` | +| Stats | `Ctrl+Shift+S` | `Cmd+Shift+S` | +| Shortcuts help | `Ctrl+Shift+K` | `Cmd+Shift+K` | ### Key Files -| File | Purpose | -| ------------------------------ | ------------------------------------------------------ | -| `scripts/build.sh` | macOS build (PyInstaller + dylibs + Info.plist) | -| `scripts/build_windows.ps1` | Windows build (PyInstaller + ZIP + optional installer) | -| `scripts/build_linux.sh` | Linux build (PyInstaller + tar.gz + optional AppImage) | -| `scripts/codesign_macos.sh` | macOS code signing + interactive notarization | -| `scripts/codesign_windows.ps1` | Windows Authenticode signing | -| `scripts/install_macos.sh` | Full macOS install (build + /Applications + launcher) | -| `.env` | Template for Azure credentials | +| File | Purpose | +|------|---------| +| `scripts/build.sh` | macOS build (PyInstaller + dylibs + Info.plist) | +| `scripts/build_windows.ps1` | Windows build (PyInstaller + ZIP + optional installer) | +| `scripts/build_linux.sh` | Linux build (PyInstaller + tar.gz + optional AppImage) | +| `scripts/codesign_macos.sh` | macOS code signing + interactive notarization | +| `scripts/codesign_windows.ps1` | Windows Authenticode signing | +| `scripts/install_macos.sh` | Full macOS install (build + /Applications + launcher) | +| `.env` | Template for Azure credentials | ### Troubleshooting -| Problem | Fix | -| -------------------------------- | ------------------------------------------------------------------------------ | -| macOS "App is damaged" | Not notarized. Run `xattr -cr /Applications/LysnrAI.app` | -| macOS no Accessibility | Must launch via `.command` file (Terminal inherits Accessibility) | -| macOS no Developer ID cert | Create at developer.apple.com → Certificates → + → Developer ID Application | -| Windows SmartScreen blocks | Code sign the .exe, or click "More info" → "Run anyway" | +| Problem | Fix | +|---------|-----| +| macOS "App is damaged" | Not notarized. Run `xattr -cr /Applications/LysnrAI.app` | +| macOS no Accessibility | Must launch via `.command` file (Terminal inherits Accessibility) | +| macOS no Developer ID cert | Create at developer.apple.com → Certificates → + → Developer ID Application | +| Windows SmartScreen blocks | Code sign the .exe, or click "More info" → "Run anyway" | | Windows VCRUNTIME140.dll missing | Install [VC++ Redistributable](https://aka.ms/vs/17/release/vc_redist.x64.exe) | -| Linux no system tray | Install `gnome-shell-extension-appindicator` (GNOME) | -| Linux no audio | Check PulseAudio/PipeWire is running, mic not muted | -| PyInstaller build fails | Activate `.venv` first: `source .venv/bin/activate && pip install -e ".[dev]"` | -| Ruff lint errors | Run `ruff check src/ --fix` to auto-fix, then review | -| Tests fail before build | Fix failing tests first — never ship with red tests | +| Linux no system tray | Install `gnome-shell-extension-appindicator` (GNOME) | +| Linux no audio | Check PulseAudio/PipeWire is running, mic not muted | +| PyInstaller build fails | Activate `.venv` first: `source .venv/bin/activate && pip install -e ".[dev]"` | +| Ruff lint errors | Run `ruff check src/ --fix` to auto-fix, then review | +| Tests fail before build | Fix failing tests first — never ship with red tests | diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/release-testflight.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/release-testflight.md index be9bd045..70b1697b 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/release-testflight.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/release-testflight.md @@ -45,7 +45,6 @@ cd learning_voice_ai_agent/mobile_app/ios && pod install ### 0a. Read current build state // turbo - ```bash cat mobile_app/ios/BUILD_STATE.md ``` @@ -55,7 +54,6 @@ Note the **Current Build** number. The next release will be `current + 1`. ### 0b. Verify clean working tree // turbo - ```bash git status && git diff --quiet && git diff --cached --quiet && echo "✅ Clean" || (echo "❌ ERROR: Uncommitted changes — commit or stash first" && exit 1) ``` @@ -80,7 +78,6 @@ Must end with `BUILD SUCCEEDED`. If it fails, fix errors before continuing. ### 0d. Verify pbxproj ↔ filesystem consistency // turbo - ```bash for f in mobile_app/ios/LysnrKeyboard/*.swift; do fname=$(basename "$f") @@ -93,7 +90,6 @@ done && echo "✅ All keyboard .swift files are in the Xcode project" ### 0e. Verify BUILD_STATE.md matches project.pbxproj // turbo - ```bash state_build=$(grep -Eo 'CURRENT_PROJECT_VERSION = [0-9]+' mobile_app/ios/BUILD_STATE.md | head -1 | awk '{print $3}') pbx_builds=$(grep "CURRENT_PROJECT_VERSION" mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj | awk '{print $3}' | tr -d ';' | sort -u) @@ -113,7 +109,6 @@ If `BUILD_STATE.md` marks the current build as **Pending Upload Retry**, do NOT Retry **step 5 only** with the existing archive path `/tmp/LysnrAI_.xcarchive`. // turbo - ```bash if grep -q "Current / Pending Upload Retry" mobile_app/ios/BUILD_STATE.md; then current_build=$(grep -Eo 'CURRENT_PROJECT_VERSION = [0-9]+' mobile_app/ios/BUILD_STATE.md | head -1 | awk '{print $3}') @@ -135,7 +130,6 @@ occurrences of `CURRENT_PROJECT_VERSION = N;` with `CURRENT_PROJECT_VERSION = N+ Verify the bump took effect on all 6: // turbo - ```bash grep "CURRENT_PROJECT_VERSION" mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj ``` @@ -155,7 +149,6 @@ Commit BEFORE archiving so the build number in git always matches what was uploa ### 3. Install CocoaPods dependencies // turbo - ```bash cd mobile_app/ios && pod install && cd ../.. ``` @@ -189,7 +182,6 @@ xcodebuild -exportArchive \ The `app-store-connect` export method auto-uploads the IPA. **Expected output:** - ``` Uploaded LysnrAI ** EXPORT SUCCEEDED ** @@ -205,7 +197,6 @@ Go back to step 1 and bump again. ### 6. Update BUILD_STATE.md After a successful upload, Cascade updates `mobile_app/ios/BUILD_STATE.md`: - - Set **Current Build** to the new number - Add a row to the Build History table with the build number, status `Released`, and key changes - Update Active Issues table if any were fixed or newly found @@ -240,38 +231,38 @@ Internal testing group has auto-distribute enabled. After Apple finishes process ### Key Files -| File | Purpose | -| ----------------------------------------------------------- | ------------------------------------------------------------------ | -| `mobile_app/ios/BUILD_STATE.md` | **Source of truth** — current build number, history, active issues | -| `mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj` | Build settings (6× CURRENT_PROJECT_VERSION) | -| `scripts/ExportOptions.plist` | Export options (method: app-store-connect, team: 748N7QPX7J) | -| `mobile_app/ios/Podfile` | CocoaPods deps (Azure Speech SDK) | -| `mobile_app/ios/LysnrKeyboard/KeyboardViewController.swift` | Keyboard extension main controller | -| `mobile_app/ios/LysnrKeyboard/LysnrTelemetry.swift` | Keyboard telemetry client | -| `mobile_app/ios/LysnrKeyboard/Info.plist` | Keyboard extension config + privacy keys | +| File | Purpose | +|------|---------| +| `mobile_app/ios/BUILD_STATE.md` | **Source of truth** — current build number, history, active issues | +| `mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj` | Build settings (6× CURRENT_PROJECT_VERSION) | +| `scripts/ExportOptions.plist` | Export options (method: app-store-connect, team: 748N7QPX7J) | +| `mobile_app/ios/Podfile` | CocoaPods deps (Azure Speech SDK) | +| `mobile_app/ios/LysnrKeyboard/KeyboardViewController.swift` | Keyboard extension main controller | +| `mobile_app/ios/LysnrKeyboard/LysnrTelemetry.swift` | Keyboard telemetry client | +| `mobile_app/ios/LysnrKeyboard/Info.plist` | Keyboard extension config + privacy keys | ### Build Identity -| Field | Value | -| -------------------- | ------------------------------- | -| Team ID | `748N7QPX7J` | -| Bundle ID | `com.bytelyst.LysnrAI` | -| Keyboard Bundle ID | `com.bytelyst.LysnrAI.keyboard` | -| App Group | `group.com.bytelyst.LysnrAI` | -| Signing | Automatic | -| Archive path pattern | `/tmp/LysnrAI_.xcarchive` | +| Field | Value | +|-------|-------| +| Team ID | `748N7QPX7J` | +| Bundle ID | `com.bytelyst.LysnrAI` | +| Keyboard Bundle ID | `com.bytelyst.LysnrAI.keyboard` | +| App Group | `group.com.bytelyst.LysnrAI` | +| Signing | Automatic | +| Archive path pattern | `/tmp/LysnrAI_.xcarchive` | ### Troubleshooting -| Problem | Fix | -| ----------------------------------- | --------------------------------------------------------------------------- | -| `BUILD FAILED` in step 0c | Fix compile errors; run `/mobile-code-quality` for full diagnostics | -| `.swift file not in pbxproj` | Add file to Xcode target Sources build phase | -| `LysnrTelemetry not found` | Verify `LysnrTelemetry.swift` is in LysnrKeyboard target Sources | -| `ARCHIVE FAILED` | Check full output: `2>&1 \| grep error:` | -| `bundle version must be higher` | Build number already uploaded — bump again from step 1 | -| `Upload limit reached` | Apple daily limit hit — wait ~24h, re-run step 5 only (archive is reusable) | -| `No signing certificate` | Xcode → Settings → Accounts → Manage Certificates → + Apple Distribution | -| `Provisioning profile` error | Xcode → Target → Signing → Enable "Automatically manage signing" | -| Processing >30 min | Check [Apple system status](https://developer.apple.com/system-status/) | -| Build number drift (git vs pbxproj) | Read `BUILD_STATE.md` — it is the source of truth | +| Problem | Fix | +|---------|-----| +| `BUILD FAILED` in step 0c | Fix compile errors; run `/mobile-code-quality` for full diagnostics | +| `.swift file not in pbxproj` | Add file to Xcode target Sources build phase | +| `LysnrTelemetry not found` | Verify `LysnrTelemetry.swift` is in LysnrKeyboard target Sources | +| `ARCHIVE FAILED` | Check full output: `2>&1 \| grep error:` | +| `bundle version must be higher` | Build number already uploaded — bump again from step 1 | +| `Upload limit reached` | Apple daily limit hit — wait ~24h, re-run step 5 only (archive is reusable) | +| `No signing certificate` | Xcode → Settings → Accounts → Manage Certificates → + Apple Distribution | +| `Provisioning profile` error | Xcode → Target → Signing → Enable "Automatically manage signing" | +| Processing >30 min | Check [Apple system status](https://developer.apple.com/system-status/) | +| Build number drift (git vs pbxproj) | Read `BUILD_STATE.md` — it is the source of truth | diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/repo_update-agent-docs.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/repo_update-agent-docs.md index e50e74dc..144e4690 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/repo_update-agent-docs.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/repo_update-agent-docs.md @@ -8,24 +8,24 @@ Scans all three workspace repos, builds a comprehensive understanding of the cur ## Repos Covered -| Repo | Path | Scope | -| ------------------------------------- | --------------------------------------------------- | ------------------------------------------------------- | -| **learning_voice_ai_agent** | `$HOME/code/mygh/learning_voice_ai_agent` | LysnrAI product code (desktop, backend, dashboards) | -| **learning_ai_common_plat** | `$HOME/code/mygh/learning_ai_common_plat` | Shared @bytelyst/_ packages + @lysnrai/_ microservices | +| Repo | Path | Scope | +|------|------|-------| +| **learning_voice_ai_agent** | `$HOME/code/mygh/learning_voice_ai_agent` | LysnrAI product code (desktop, backend, dashboards) | +| **learning_ai_common_plat** | `$HOME/code/mygh/learning_ai_common_plat` | Shared @bytelyst/* packages + @lysnrai/* microservices | | **learning_multimodal_memory_agents** | `$HOME/code/mygh/learning_multimodal_memory_agents` | MindLyst native app (KMP + SwiftUI + Compose + Next.js) | ## Files Updated Per Repo -| File | Tool | Format | -| --------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------- | -| `AGENTS.md` | Universal (OpenAI Codex, all agents) | Detailed markdown — full onboarding, structure, conventions, patterns, ownership | -| `CLAUDE.md` | Claude Code (Anthropic) | Short markdown (<50 lines) — compact quick-reference summary | -| `.cursorrules` | Cursor AI | Plain text — inline completion + chat rules | -| `.github/copilot-instructions.md` | GitHub Copilot | Markdown — code generation always/never lists | -| `.windsurfrules` | Windsurf / Codeium Cascade | Plain text — project rules for Windsurf memory system | -| `.clinerules` | Cline / Roo Code (VS Code) | Plain text — mandatory rules + key file locations | -| `.aider.conf.yml` | Aider | YAML — context files, conventions pointer, lint commands | -| `.editorconfig` | All editors / JetBrains AI | INI — indent, charset, line ending, trim rules | +| File | Tool | Format | +|------|------|--------| +| `AGENTS.md` | Universal (OpenAI Codex, all agents) | Detailed markdown — full onboarding, structure, conventions, patterns, ownership | +| `CLAUDE.md` | Claude Code (Anthropic) | Short markdown (<50 lines) — compact quick-reference summary | +| `.cursorrules` | Cursor AI | Plain text — inline completion + chat rules | +| `.github/copilot-instructions.md` | GitHub Copilot | Markdown — code generation always/never lists | +| `.windsurfrules` | Windsurf / Codeium Cascade | Plain text — project rules for Windsurf memory system | +| `.clinerules` | Cline / Roo Code (VS Code) | Plain text — mandatory rules + key file locations | +| `.aider.conf.yml` | Aider | YAML — context files, conventions pointer, lint commands | +| `.editorconfig` | All editors / JetBrains AI | INI — indent, charset, line ending, trim rules | --- @@ -36,21 +36,22 @@ Scans all three workspace repos, builds a comprehensive understanding of the cur 1. **Scan learning_voice_ai_agent structure:** // turbo - Run `find $HOME/code/mygh/learning_voice_ai_agent -maxdepth 2 -type f -name "package.json" -not -path "*/node_modules/*" | head -20` to find all JS projects - // turbo + // turbo - Run `find $HOME/code/mygh/learning_voice_ai_agent -maxdepth 1 -type f -name "*.py" -o -name "*.toml" -o -name "Makefile" | head -20` to find Python config - Read `AGENTS.md`, `README_MONO_REPO.md`, `docker-compose.yml`, `pyproject.toml` - Read each dashboard's `package.json` to check current `@bytelyst/*` dependencies - - List `admin-dashboard-web/src/lib/`, `user-dashboard-web/src/lib/`, `tracker-dashboard-web/src/lib/` to see which lib files exist - - Read `admin-dashboard-web/src/lib/cosmos.ts` and `auth-server.ts` to verify which @bytelyst/\* packages are wired + - List `user-dashboard-web/src/lib/` to see which lib files exist + - NOTE: admin-dashboard-web and tracker-dashboard-web moved to `../learning_ai_common_plat/dashboards/` + - Read `user-dashboard-web/src/lib/cosmos.ts` and `auth-server.ts` to verify which @bytelyst/* packages are wired - Count Python tests: `find tests/ -name "test_*.py" | wc -l` - - Count API routes: `find admin-dashboard-web/src/app/api -name "route.ts" | wc -l` + - Count API routes: `find user-dashboard-web/src/app/api -name "route.ts" | wc -l` - Read `.github/workflows/` to count CI workflows - Read `run-local-all-services.sh` header to understand service topology 2. **Scan learning_ai_common_plat structure:** // turbo - Run `find $HOME/code/mygh/learning_ai_common_plat/packages -maxdepth 2 -name "package.json" -not -path "*/node_modules/*"` to list all packages - // turbo + // turbo - Run `find $HOME/code/mygh/learning_ai_common_plat/services -maxdepth 2 -name "package.json" -not -path "*/node_modules/*"` to list all services - Read `AGENTS.md`, `README.md`, `pnpm-workspace.yaml`, `tsconfig.base.json` - Read each package's `src/index.ts` to catalog exports @@ -83,16 +84,16 @@ Scans all three workspace repos, builds a comprehensive understanding of the cur 6. **Update `AGENTS.md`** — the most comprehensive file. Ensure these sections are current: - **Project Identity** — product name, IDs, prefixes - **Monorepo Layout** — directory tree with descriptions, including sibling repo structure - - **Tech Stack Rules** — Python, TypeScript services, TypeScript dashboards (with @bytelyst/\* wiring) + - **Tech Stack Rules** — Python, TypeScript services, TypeScript dashboards (with @bytelyst/* wiring) - **Coding Conventions** — MUST/MUST NOT rules - - **File Ownership Map** — table mapping domains → services → key files (include all @bytelyst/\* mappings) + - **File Ownership Map** — table mapping domains → services → key files (include all @bytelyst/* mappings) - **How to Run Things** — start services, run tests, docker compose, seed - **Common Patterns** — adding modules, pages, service clients, backend routes, debugging - **Environment Variables** — required vars per service, env file locations - **Key Documents** — table of "when you need to..." → "read this" 7. **Update `CLAUDE.md`** — compact summary (<50 lines): - - Identity, key commands, critical rules, current @bytelyst/\* wiring state + - Identity, key commands, critical rules, current @bytelyst/* wiring state 8. **Update `.cursorrules`** — Cursor inline completion + chat rules: - Project context, code generation patterns, naming conventions, import patterns @@ -102,7 +103,7 @@ Scans all three workspace repos, builds a comprehensive understanding of the cur - Always/never lists, commit format, import patterns, type patterns 10. **Update `.windsurfrules`** — Windsurf/Cascade project rules: - - Architecture, key paths, conventions, @bytelyst/\* wiring, build verification commands + - Architecture, key paths, conventions, @bytelyst/* wiring, build verification commands 11. **Update `.clinerules`** — Cline/Roo Code mandatory rules: - Numbered mandatory rules, key file locations, verify command @@ -118,7 +119,7 @@ Scans all three workspace repos, builds a comprehensive understanding of the cur 14. **Update all 8 agent doc files** in common platform, ensuring: - `AGENTS.md`: Package exports, service modules, file ownership (27+ domains), dependency graph, consumer info, test count - `CLAUDE.md`: Compact summary - - `.cursorrules`: Completion rules for @bytelyst/_ and @lysnrai/_ + - `.cursorrules`: Completion rules for @bytelyst/* and @lysnrai/* - `.github/copilot-instructions.md`: Generation rules - `.windsurfrules`: pnpm workspace rules, ESM conventions - `.clinerules`: Mandatory rules for package/service development @@ -141,7 +142,7 @@ Scans all three workspace repos, builds a comprehensive understanding of the cur 16. **Verify consistency across repos:** - Shared conventions (commit format, productId rule, etc.) should match across all 3 AGENTS.md files - - @bytelyst/\* package descriptions should be consistent between common platform and voice agent docs + - @bytelyst/* package descriptions should be consistent between common platform and voice agent docs - Design token flow should be consistent between common platform and MindLyst docs 17. **Commit and push each repo** (stage all 8 files): diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/start-all-services.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/start-all-services.md index 77feebdf..4f2b535c 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/start-all-services.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/start-all-services.md @@ -7,15 +7,14 @@ description: Start all backend services locally (FastAPI + 2 microservices + Adm Run the local dev startup script to launch all 7 services: // turbo - 1. Run `./run-local-all-services.sh start` from the repo root 2. Verify all services are up: - // turbo +// turbo - Run `./run-local-all-services.sh status` 3. Quick health check: - // turbo +// turbo - Run `curl -s http://127.0.0.1:8000/health` — should return `{"status":"ok"}` - Run `curl -s http://127.0.0.1:4003/health` — should return `{"status":"ok"}` - Run `curl -s http://127.0.0.1:4005/health` — should return `{"status":"ok"}` @@ -26,15 +25,15 @@ Run the local dev startup script to launch all 7 services: ### Service URLs -| Service | URL | Log File | -| ---------------------------- | --------------------- | ------------------------------ | -| Backend API (FastAPI) | http://localhost:8000 | `.logs/backend.log` | -| Platform Service (Fastify) | http://localhost:4003 | `.logs/platform-service.log` | +| Service | URL | Log File | +|---------|-----|----------| +| Backend API (FastAPI) | http://localhost:8000 | `.logs/backend.log` | +| Platform Service (Fastify) | http://localhost:4003 | `.logs/platform-service.log` | | Extraction Service (Fastify) | http://localhost:4005 | `.logs/extraction-service.log` | -| Extraction Sidecar (Python) | http://localhost:4006 | `.logs/extraction-sidecar.log` | -| Admin Dashboard | http://localhost:3001 | `.logs/admin-dashboard.log` | -| User Dashboard | http://localhost:3002 | `.logs/user-dashboard.log` | -| Tracker Dashboard | http://localhost:3003 | `.logs/tracker-dashboard.log` | +| Extraction Sidecar (Python) | http://localhost:4006 | `.logs/extraction-sidecar.log` | +| Admin Dashboard | http://localhost:3001 | `.logs/admin-dashboard.log` | +| User Dashboard | http://localhost:3002 | `.logs/user-dashboard.log` | +| Tracker Dashboard | http://localhost:3003 | `.logs/tracker-dashboard.log` | ### Stop diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/test-coverage.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/test-coverage.md index f53ebe27..204544c3 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/test-coverage.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/test-coverage.md @@ -20,7 +20,6 @@ Generate detailed test coverage reports for all three workspace repos and produc ### Step 1: Python tests — LysnrAI desktop + backend // turbo - ```bash cd /Users/sd9235/code/mygh/learning_voice_ai_agent && python -m pytest tests/ backend/tests/ -v --tb=short --co -q 2>/dev/null | tail -5 ``` @@ -45,7 +44,6 @@ Cascade: Record the Python coverage summary (total lines, covered, missed, %). ### Step 2: Common Platform — all packages + services (Vitest) // turbo - ```bash cd /Users/sd9235/code/mygh/learning_ai_common_plat && pnpm build 2>&1 | tail -5 ``` @@ -70,20 +68,20 @@ Cascade: Record each package/service coverage summary (statements, branches, fun --- -### Step 3: LysnrAI Dashboards — Admin, User, Tracker (Vitest) +### Step 3: LysnrAI Dashboards (Vitest) -Run coverage for each dashboard: - -```bash -cd /Users/sd9235/code/mygh/learning_voice_ai_agent/admin-dashboard-web && npm run test:coverage 2>&1 | tail -60 -``` +Run coverage for user dashboard (admin + tracker moved to `learning_ai_common_plat/dashboards/`): ```bash cd /Users/sd9235/code/mygh/learning_voice_ai_agent/user-dashboard-web && npm run test:coverage 2>&1 | tail -60 ``` ```bash -cd /Users/sd9235/code/mygh/learning_voice_ai_agent/tracker-dashboard-web && npm run test:coverage 2>&1 | tail -60 +cd /Users/sd9235/code/mygh/learning_ai_common_plat/dashboards/admin-web && npm run test:coverage 2>&1 | tail -60 +``` + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat/dashboards/tracker-web && npm run test:coverage 2>&1 | tail -60 ``` Cascade: Record each dashboard's coverage summary. @@ -103,7 +101,6 @@ Cascade: Record MindLyst web coverage summary. ### Step 5: MindLyst KMP Shared (Kotlin) // turbo - ```bash cd /Users/sd9235/code/mygh/learning_multimodal_memory_agents/mindlyst-native && ./gradlew :shared:test 2>&1 | tail -30 ``` @@ -169,11 +166,11 @@ If there is nothing new to commit (same content/timestamp not created), report t ### Troubleshooting -| Problem | Fix | -| ------------------------------- | -------------------------------------------------- | -| `pytest-cov` not found | `pip install pytest-cov` | +| Problem | Fix | +|---------|-----| +| `pytest-cov` not found | `pip install pytest-cov` | | `@vitest/coverage-v8` not found | `npm install -D @vitest/coverage-v8` (per project) | -| Common-plat coverage fails | Run `pnpm build` first, then retry | -| Dashboard coverage fails | Run `npm install` first in that dashboard | -| KMP tests fail to compile | Run `./gradlew :shared:compileKotlinJvm` first | -| Timeout on large test suites | Add `--reporter=verbose --pool=forks` to vitest | +| Common-plat coverage fails | Run `pnpm build` first, then retry | +| Dashboard coverage fails | Run `npm install` first in that dashboard | +| KMP tests fail to compile | Run `./gradlew :shared:compileKotlinJvm` first | +| Timeout on large test suites | Add `--reporter=verbose --pool=forks` to vitest | diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/test-desktop-app.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/test-desktop-app.md index 8f051d53..fe516996 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/test-desktop-app.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/test-desktop-app.md @@ -5,7 +5,6 @@ description: Run and test the macOS desktop app locally ## Test Desktop App (macOS) ### Prerequisites - - Backend running on port 8000 (`./run-local-all-services.sh start`) - Python 3.12+ with venv - Azure Speech SDK credentials in `~/.LysnrAI/.env` @@ -23,7 +22,6 @@ pip install -e ".[dev]" --quiet ### Step 2 — Run automated quality checks // turbo - ```bash source .venv/bin/activate && python -m ruff check src/ --select E,F,W --no-fix 2>&1 | tail -10 ``` @@ -35,7 +33,6 @@ source .venv/bin/activate && python -m pytest tests/ -v --tb=short -q 2>&1 | tai ### Step 3 — Verify env file exists // turbo - ```bash ls ~/.LysnrAI/.env && echo "OK: env file found" || echo "ERROR: create ~/.LysnrAI/.env with Azure credentials" ``` @@ -57,17 +54,16 @@ source .venv/bin/activate && python3 -m src.main ### Key Files -| File | Purpose | -| --------------------------------- | --------------------------------------------- | -| `src/main.py` | App entry point, LysnrEngine, license restore | -| `src/licensing/license_client.py` | License activation via backend API | -| `src/hotkey/fn_listener.py` | macOS Fn/Globe key listener | -| `src/cloud/` | Azure Speech SDK integration | -| `src/audio/` | Audio recording + processing | -| `src/paste/` | Clipboard paste into active app | +| File | Purpose | +|------|---------| +| `src/main.py` | App entry point, LysnrEngine, license restore | +| `src/licensing/license_client.py` | License activation via backend API | +| `src/hotkey/fn_listener.py` | macOS Fn/Globe key listener | +| `src/cloud/` | Azure Speech SDK integration | +| `src/audio/` | Audio recording + processing | +| `src/paste/` | Clipboard paste into active app | ### Important Notes - - macOS: Set System Settings → Keyboard → Press Globe key → "Do Nothing" - Corporate proxy: Backend must be started with proxy bypass (run-local script handles this) - The app connects to `http://localhost:8000/api` for license activation diff --git a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/test-ios-app.md b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/test-ios-app.md index c9e6bc82..2a0c3fa1 100644 --- a/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/test-ios-app.md +++ b/__LOCAL_LLMs/AI_IDE_CHAT_HISTORY/WINDSURF/repo_workflows/learning_voice_ai_agent/test-ios-app.md @@ -5,7 +5,6 @@ description: Build and test the iOS app in Xcode Simulator ## Test iOS App ### Prerequisites - - All services running (`./run-local-all-services.sh start`) - Backend healthy: `curl http://127.0.0.1:8000/health` - CocoaPods installed: `pod install` in `mobile_app/ios/` @@ -55,14 +54,13 @@ Open **`mobile_app/ios/LysnrAI.xcworkspace`** (NOT `.xcodeproj` — CocoaPods re ### Key Files -| File | Purpose | -| ----------------------------------------------------------- | -------------------------------------------- | -| `mobile_app/ios/LysnrAI/Auth/AuthService.swift` | Auth state, credential sharing with keyboard | -| `mobile_app/ios/LysnrAI/ContentView.swift` | Tab navigation, passes authService to views | -| `mobile_app/ios/LysnrKeyboard/KeyboardViewController.swift` | Keyboard extension main controller | -| `mobile_app/ios/LysnrKeyboard/LysnrTelemetry.swift` | Keyboard telemetry client | -| `mobile_app/ios/LysnrKeyboard/Info.plist` | Extension config + privacy descriptions | +| File | Purpose | +|------|---------| +| `mobile_app/ios/LysnrAI/Auth/AuthService.swift` | Auth state, credential sharing with keyboard | +| `mobile_app/ios/LysnrAI/ContentView.swift` | Tab navigation, passes authService to views | +| `mobile_app/ios/LysnrKeyboard/KeyboardViewController.swift` | Keyboard extension main controller | +| `mobile_app/ios/LysnrKeyboard/LysnrTelemetry.swift` | Keyboard telemetry client | +| `mobile_app/ios/LysnrKeyboard/Info.plist` | Extension config + privacy descriptions | ### API Endpoint - iOS Simulator connects to `http://127.0.0.1:8000` (configured in AuthService.swift) diff --git a/dashboards/admin-web/.env.example b/dashboards/admin-web/.env.example index 8d438b3f..2c57452a 100644 --- a/dashboards/admin-web/.env.example +++ b/dashboards/admin-web/.env.example @@ -26,6 +26,9 @@ STRIPE_WEBHOOK_SECRET=whsec_... STRIPE_PRICE_PRO=price_... STRIPE_PRICE_ENTERPRISE=price_... +# ── Azure Key Vault (optional — resolves secrets at startup) ── +AZURE_KEYVAULT_URL= + # ── Seed (development only) ── SEED_SECRET=your-seed-secret diff --git a/dashboards/admin-web/src/lib/billing-client.ts b/dashboards/admin-web/src/lib/billing-client.ts index 9728f14c..832b7ecd 100644 --- a/dashboards/admin-web/src/lib/billing-client.ts +++ b/dashboards/admin-web/src/lib/billing-client.ts @@ -9,6 +9,9 @@ import { createApiClient } from '@bytelyst/api-client'; const billingApi = createApiClient({ baseUrl: `${process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003'}/api`, + defaultHeaders: { + 'x-product-id': process.env.PRODUCT_ID || 'lysnrai', + }, }); // ── Subscriptions (Admin) ──────────────────────────────────── diff --git a/dashboards/admin-web/src/lib/growth-client.ts b/dashboards/admin-web/src/lib/growth-client.ts index 4d3dff63..c468b23f 100644 --- a/dashboards/admin-web/src/lib/growth-client.ts +++ b/dashboards/admin-web/src/lib/growth-client.ts @@ -10,6 +10,9 @@ import { createApiClient } from '@bytelyst/api-client'; const growthApi = createApiClient({ baseUrl: `${process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003'}/api`, + defaultHeaders: { + 'x-product-id': process.env.PRODUCT_ID || 'lysnrai', + }, }); // ── Invitations ────────────────────────────────────────────────── diff --git a/dashboards/admin-web/src/lib/platform-client.ts b/dashboards/admin-web/src/lib/platform-client.ts index 1d2ea353..98f149d7 100644 --- a/dashboards/admin-web/src/lib/platform-client.ts +++ b/dashboards/admin-web/src/lib/platform-client.ts @@ -9,6 +9,9 @@ import { createApiClient } from '@bytelyst/api-client'; const platformApi = createApiClient({ baseUrl: `${process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003'}/api`, + defaultHeaders: { + 'x-product-id': process.env.PRODUCT_ID || 'lysnrai', + }, }); // ── Audit ─────────────────────────────────────────────────────── diff --git a/docs/CLOUD/CLOUD_AGNOSTIC_REFACTOR_ROADMAP.md b/docs/CLOUD/CLOUD_AGNOSTIC_REFACTOR_ROADMAP.md new file mode 100644 index 00000000..598a2065 --- /dev/null +++ b/docs/CLOUD/CLOUD_AGNOSTIC_REFACTOR_ROADMAP.md @@ -0,0 +1,1173 @@ +# Cloud-Agnostic Refactor Roadmap — ByteLyst Ecosystem + +> **Author:** AI Analysis (Cascade) +> **Date:** 2026-03-01 +> **Companion doc:** [`CLOUD_PROVIDER_MIGRATION_ANALYSIS.md`](./CLOUD_PROVIDER_MIGRATION_ANALYSIS.md) +> **Goal:** Refactor the codebase so it continues to work on Azure today, but switching to any other cloud provider requires **minimum effort** (days, not weeks). + +--- + +## Table of Contents + +1. [Philosophy](#1-philosophy) +2. [Current State vs Target State](#2-current-state-vs-target-state) +3. [Sprint Plan Overview](#3-sprint-plan-overview) +4. [Sprint 1: Database Abstraction Layer](#4-sprint-1-database-abstraction-layer) +5. [Sprint 2: Storage Abstraction Layer](#5-sprint-2-storage-abstraction-layer) +6. [Sprint 3: LLM Provider Abstraction](#6-sprint-3-llm-provider-abstraction) +7. [Sprint 4: Secrets Manager Abstraction](#7-sprint-4-secrets-manager-abstraction) +8. [Sprint 5: Speech Provider Abstraction](#8-sprint-5-speech-provider-abstraction) +9. [Sprint 6: Push Notification Abstraction](#9-sprint-6-push-notification-abstraction) +10. [Sprint 7: Monitoring & Telemetry Abstraction](#10-sprint-7-monitoring--telemetry-abstraction) +11. [Migration Effort After Refactor](#11-migration-effort-after-refactor) +12. [Testing Strategy](#12-testing-strategy) +13. [Env Var Naming Convention](#13-env-var-naming-convention) +14. [Risk Mitigation](#14-risk-mitigation) +15. [Appendix: Interface Specifications](#appendix-interface-specifications) + +--- + +## 1. Philosophy + +### Core Principle: Provider-Agnostic Interfaces, Provider-Specific Implementations + +``` +Application Code (routes, business logic) + │ + ▼ + @bytelyst/* interfaces ◄── Cloud-agnostic contracts + │ + ▼ + Provider implementations ◄── Azure today, swap tomorrow + ├── cosmos-provider/ (Azure Cosmos DB) + ├── mongo-provider/ (MongoDB Atlas — future) + ├── s3-provider/ (AWS S3 — future) + └── ... +``` + +### Design Rules + +1. **Application code NEVER imports cloud SDKs** — only `@bytelyst/*` interfaces +2. **Provider chosen at startup via env var** — `DB_PROVIDER=cosmos`, `STORAGE_PROVIDER=azure`, etc. +3. **All interfaces have an in-memory mock** — for testing without any cloud dependency +4. **Zero breaking changes** — every sprint keeps all existing tests passing +5. **Incremental adoption** — modules migrate one at a time, old and new patterns coexist + +### What This Is NOT + +- This is **not** a migration to another cloud — Azure continues to be the production provider +- This is **not** a rewrite — it's a series of refactors that insert interfaces between app code and cloud SDKs +- This is **not** over-engineering — each interface is thin (30–60 lines) and directly maps to patterns already in the codebase + +--- + +## 2. Current State vs Target State + +### Current: Direct Azure SDK Usage + +``` + 38 repository.ts files + ┌──────────────────────┐ +routes.ts ────────► │ container() │ + │ .items.query(SQL) │ ◄── @azure/cosmos types leak everywhere + │ .items.create(doc) │ + │ .item(id,pk).read() │ + └──────────────────────┘ + │ + ▼ + @bytelyst/cosmos (client.ts) + │ + ▼ + @azure/cosmos SDK +``` + +**Problems:** +- 38 platform-service repository files write raw Cosmos SQL queries +- 6 additional repository files in dashboards + MindLyst web +- Blob, Speech, OpenAI all have direct Azure SDK imports +- Switching DB means rewriting 44+ files + +### Target: Provider-Agnostic Interfaces + +``` + 38 repository.ts files + ┌──────────────────────────┐ +routes.ts ────────► │ collection.findMany({ │ + │ filter: {productId}, │ ◄── Cloud-agnostic API + │ sort: {createdAt: -1}, │ + │ limit: 20, │ + │ }) │ + └──────────────────────────┘ + │ + ▼ + @bytelyst/datastore (interface) + │ + ┌─────────┼─────────┐ + ▼ ▼ ▼ + CosmosAdapter MongoAdapter MemoryAdapter + (Azure) (MongoDB) (Testing) + │ + ▼ + @azure/cosmos SDK +``` + +**Benefits:** +- Repositories use a generic query API — no SQL strings, no Azure types +- Switching provider = implement a new adapter (~200 lines) + change env var +- In-memory adapter makes tests fast and cloud-free +- Azure continues to work exactly as before + +--- + +## 3. Sprint Plan Overview + +| Sprint | Package / Scope | Effort | Files Changed | Risk | +|--------|----------------|--------|---------------|------| +| **1** | `@bytelyst/datastore` — DB abstraction | 5–7 days | 44 repository files + 1 new package | Medium | +| **2** | `@bytelyst/storage` — Blob/Object abstraction | 2 days | 3 files + 1 new package | Low | +| **3** | `@bytelyst/llm` — LLM provider abstraction | 2 days | 4 files + 1 new package | Low | +| **4** | `@bytelyst/secrets` — Secrets manager abstraction | 1 day | 2 files (refactor existing) | Very Low | +| **5** | `@bytelyst/speech` — Speech STT abstraction | 3–4 days | 3 files + 1 new package | Medium | +| **6** | `@bytelyst/push` — Push notification abstraction | 1 day | 1 file + 1 new package | Very Low | +| **7** | Monitoring/Telemetry cleanup | 0.5 days | Already done (custom telemetry) | None | +| **Total** | | **~15–17 days** | ~55 files | | + +### Priority Order + +``` +Sprint 1 (DB) ──► Sprint 2 (Storage) ──► Sprint 3 (LLM) ──► Sprint 4 (Secrets) + ▲ HIGHEST ROI EASY EASY TRIVIAL + │ + └── 80% of migration effort lives here. Do this first. + +Sprint 5 (Speech) ──► Sprint 6 (Push) ──► Sprint 7 (Monitoring) + MEDIUM LOW PRIORITY ALREADY DONE +``` + +--- + +## 4. Sprint 1: Database Abstraction Layer + +**Package:** `@bytelyst/datastore` +**Effort:** 5–7 days +**This is the most important sprint — it eliminates 80% of cloud lock-in.** + +### 4.1 Interface Design + +```typescript +// packages/datastore/src/types.ts + +/** A cloud-agnostic document collection (like a Cosmos container or Mongo collection). */ +export interface DocumentCollection { + /** Find a single document by ID + partition key. */ + findById(id: string, partitionKey: string): Promise; + + /** Find multiple documents matching a filter. */ + findMany(opts: FindManyOptions): Promise; + + /** Find one document matching a filter. */ + findOne(opts: FindOneOptions): Promise; + + /** Count documents matching a filter. */ + count(filter: FilterMap): Promise; + + /** Insert a new document. */ + create(doc: T): Promise; + + /** Replace an entire document (full overwrite). */ + replace(id: string, partitionKey: string, doc: T): Promise; + + /** Upsert: create if not exists, replace if exists. */ + upsert(doc: T): Promise; + + /** Delete a document by ID + partition key. */ + delete(id: string, partitionKey: string): Promise; + + /** Run an aggregation (COUNT, SUM, GROUP BY). */ + aggregate(opts: AggregateOptions): Promise; +} + +export interface BaseDocument { + id: string; + [key: string]: unknown; +} + +export interface FindManyOptions { + filter: FilterMap; + sort?: SortMap; + limit?: number; + offset?: number; + partitionKey?: string; +} + +export interface FindOneOptions { + filter: FilterMap; + partitionKey?: string; +} + +export type FilterMap = Record; +export type SortMap = Record; // 1 = ASC, -1 = DESC + +export interface AggregateOptions { + filter: FilterMap; + groupBy?: string[]; + count?: string; // alias for COUNT(1) + sum?: string; // field to SUM +} + +/** Factory that creates collections — one per provider. */ +export interface DatastoreProvider { + collection(name: string): DocumentCollection; + initialize?(configs: Record): Promise; + close?(): Promise; +} + +export interface CollectionConfig { + partitionKeyPath: string; + defaultTtl?: number | null; +} +``` + +### 4.2 Cosmos Adapter (keeps everything working today) + +```typescript +// packages/datastore/src/providers/cosmos.ts + +import type { Container } from '@azure/cosmos'; +import type { BaseDocument, DocumentCollection, FindManyOptions, FilterMap, ... } from '../types.js'; + +export class CosmosCollection implements DocumentCollection { + constructor(private container: Container) {} + + async findById(id: string, partitionKey: string): Promise { + try { + const { resource } = await this.container.item(id, partitionKey).read(); + return resource ?? null; + } catch { return null; } + } + + async findMany(opts: FindManyOptions): Promise { + const { sql, params } = buildSqlQuery(opts); // ◄── converts FilterMap → Cosmos SQL + const { resources } = await this.container + .items.query({ query: sql, parameters: params }) + .fetchAll(); + return resources; + } + + async create(doc: T): Promise { + const { resource } = await this.container.items.create(doc); + return resource as T; + } + + async replace(id: string, partitionKey: string, doc: T): Promise { + const { resource } = await this.container.item(id, partitionKey).replace(doc); + return resource as T; + } + + async upsert(doc: T): Promise { + const { resource } = await this.container.items.upsert(doc); + return resource as T; + } + + async delete(id: string, partitionKey: string): Promise { + try { + await this.container.item(id, partitionKey).delete(); + return true; + } catch { return false; } + } + + // ... count(), findOne(), aggregate() +} + +/** Convert a FilterMap to Cosmos SQL. */ +function buildSqlQuery(opts: FindManyOptions): { sql: string; params: SqlParam[] } { + // { productId: 'x', userId: 'y' } + // → "SELECT * FROM c WHERE c.productId = @p0 AND c.userId = @p1 ORDER BY c.createdAt DESC OFFSET 0 LIMIT 20" + // This is a mechanical translation — no query language exposed to application code. +} +``` + +### 4.3 In-Memory Adapter (for testing) + +```typescript +// packages/datastore/src/providers/memory.ts + +export class MemoryCollection implements DocumentCollection { + private docs: Map = new Map(); + + async findById(id: string): Promise { + return this.docs.get(id) ?? null; + } + + async findMany(opts: FindManyOptions): Promise { + let results = [...this.docs.values()].filter(doc => matchesFilter(doc, opts.filter)); + if (opts.sort) results = sortDocs(results, opts.sort); + if (opts.offset) results = results.slice(opts.offset); + if (opts.limit) results = results.slice(0, opts.limit); + return results; + } + + async create(doc: T): Promise { + this.docs.set(doc.id, doc); + return doc; + } + // ... etc +} +``` + +### 4.4 MongoDB Adapter (future — ready to implement when needed) + +```typescript +// packages/datastore/src/providers/mongo.ts (STUB — implement when migrating) + +import type { Collection as MongoCollection } from 'mongodb'; +import type { BaseDocument, DocumentCollection, FindManyOptions } from '../types.js'; + +export class MongoDocumentCollection implements DocumentCollection { + constructor(private collection: MongoCollection) {} + + async findById(id: string): Promise { + return this.collection.findOne({ _id: id } as any) as Promise; + } + + async findMany(opts: FindManyOptions): Promise { + let cursor = this.collection.find(opts.filter); + if (opts.sort) cursor = cursor.sort(opts.sort); + if (opts.offset) cursor = cursor.skip(opts.offset); + if (opts.limit) cursor = cursor.limit(opts.limit); + return cursor.toArray() as Promise; + } + // ... etc +} +``` + +### 4.5 How Repository Files Change + +**Before (Cosmos SQL in every file):** + +```typescript +// services/platform-service/src/modules/flags/repository.ts +import { getContainer } from '../../lib/cosmos.js'; + +function container() { + return getContainer('feature_flags'); +} + +export async function list(productId: string): Promise { + const { resources } = await container() + .items.query({ + query: 'SELECT * FROM c WHERE c.productId = @productId ORDER BY c.key ASC', + parameters: [{ name: '@productId', value: productId }], + }) + .fetchAll(); + return resources; +} + +export async function getByKey(key: string, productId: string): Promise { + const { resources } = await container() + .items.query({ + query: 'SELECT * FROM c WHERE c.productId = @productId AND c.key = @key', + parameters: [ + { name: '@productId', value: productId }, + { name: '@key', value: key }, + ], + }) + .fetchAll(); + return resources[0] ?? null; +} + +export async function create(doc: FeatureFlagDoc): Promise { + const { resource } = await container().items.create(doc); + return resource as FeatureFlagDoc; +} +``` + +**After (cloud-agnostic):** + +```typescript +// services/platform-service/src/modules/flags/repository.ts +import { getCollection } from '../../lib/datastore.js'; +import type { FeatureFlagDoc } from './types.js'; + +function collection() { + return getCollection('feature_flags'); +} + +export async function list(productId: string): Promise { + return collection().findMany({ + filter: { productId }, + sort: { key: 1 }, + }); +} + +export async function getByKey(key: string, productId: string): Promise { + return collection().findOne({ + filter: { productId, key }, + }); +} + +export async function create(doc: FeatureFlagDoc): Promise { + return collection().create(doc); +} +``` + +**Key observations:** +- No SQL strings +- No `@azure/cosmos` types +- No `.items.query().fetchAll()` chaining +- The `getCollection()` function returns the right provider based on `DB_PROVIDER` env var +- **All existing behavior is preserved** — the Cosmos adapter generates the same SQL under the hood + +### 4.6 Service Wiring + +```typescript +// services/platform-service/src/lib/datastore.ts (replaces lib/cosmos.ts) +import { createDatastoreProvider } from '@bytelyst/datastore'; +import type { DocumentCollection, BaseDocument } from '@bytelyst/datastore'; + +let _provider: ReturnType | null = null; + +export function getProvider() { + if (!_provider) { + _provider = createDatastoreProvider(); // reads DB_PROVIDER env var + } + return _provider; +} + +export function getCollection(name: string): DocumentCollection { + return getProvider().collection(name); +} +``` + +```typescript +// packages/datastore/src/factory.ts +export function createDatastoreProvider(): DatastoreProvider { + const provider = process.env.DB_PROVIDER || 'cosmos'; + switch (provider) { + case 'cosmos': + return new CosmosDatastoreProvider(); // uses existing COSMOS_ENDPOINT, COSMOS_KEY + case 'mongo': + return new MongoDatastoreProvider(); // uses MONGO_URI + case 'memory': + return new MemoryDatastoreProvider(); // no config needed + default: + throw new Error(`Unknown DB_PROVIDER: ${provider}`); + } +} +``` + +### 4.7 Migration Plan for 38 Repository Files + +Migrate in batches, one module per commit. Each commit: +1. Update the repository file to use `getCollection()` instead of `getContainer()` +2. Replace SQL queries with `findMany()` / `findOne()` / `count()` / `aggregate()` +3. Run the module's test file — must pass +4. Commit: `refactor(module-name): migrate to datastore abstraction` + +**Batch order** (simplest first, complex last): + +| Batch | Modules | Complexity | Notes | +|-------|---------|-----------|-------| +| 1 | flags, plans, settings, changelog, products | Simple CRUD | 5 files, warmup | +| 2 | licenses, sessions, ip-rules, maintenance, feedback | Simple CRUD + filters | 5 files | +| 3 | items, comments, votes, brains, reflections | CRUD + filter combos | 5 files | +| 4 | audit, delivery, notifications, exports, jobs | CRUD + time queries | 5 files | +| 5 | tokens, usage, invitations, referrals, webhooks | More complex queries | 5 files | +| 6 | auth, subscriptions, telemetry, experiments | Complex (GROUP BY, aggregates) | 4 files | +| 7 | timers, shared-timers, routines, households | Sync logic, batch ops | 4 files | +| 8 | fasting-sessions, fasting-protocols, meal-log, social-fasting, daily-briefs, streaks, push-triggers, impersonation, status, memory, analytics, waitlist | Product-specific + remaining | 12 files | +| 9 | Dashboard cosmos clients (admin-web, MindLyst web) | Direct `@azure/cosmos` | 6 files | +| 10 | Python clients (desktop cosmos, backend cosmos) | `azure.cosmos` → abstracted | 2 files | + +### 4.8 Handling Complex Queries + +Some repository files use advanced Cosmos SQL features. Here's how the interface handles them: + +| Cosmos SQL Pattern | Datastore Interface Equivalent | +|--------------------|-------------------------------| +| `SELECT * FROM c WHERE c.x = @v` | `findMany({ filter: { x: v } })` | +| `SELECT * FROM c WHERE c.x = @v AND c.y = @w` | `findMany({ filter: { x: v, y: w } })` | +| `ORDER BY c.x ASC` | `findMany({ sort: { x: 1 } })` | +| `ORDER BY c.x DESC` | `findMany({ sort: { x: -1 } })` | +| `OFFSET @o LIMIT @l` | `findMany({ offset: o, limit: l })` | +| `SELECT VALUE COUNT(1) FROM c WHERE ...` | `count({ filter })` | +| `SELECT c.plan, COUNT(1) AS cnt ... GROUP BY c.plan` | `aggregate({ filter, groupBy: ['plan'], count: 'cnt' })` | +| `NOT IS_DEFINED(c.usedAt)` | `findMany({ filter: { usedAt: { $exists: false } } })` | +| `c.x >= @v` | `findMany({ filter: { x: { $gte: v } } })` | +| `ARRAY_CONTAINS(c.tags, @tag)` | `findMany({ filter: { tags: { $contains: tag } } })` | +| `container().item(id, pk).read()` | `findById(id, pk)` | +| `container().items.create(doc)` | `create(doc)` | +| `container().item(id, pk).replace(doc)` | `replace(id, pk, doc)` | +| `container().items.upsert(doc)` | `upsert(doc)` | +| `container().item(id, pk).delete()` | `delete(id, pk)` | + +For the filter operators, use a simple operator convention: + +```typescript +// Exact match +{ productId: 'lysnrai' } + +// Comparison operators +{ syncVersion: { $gte: 5 } } +{ createdAt: { $gte: '2026-01-01', $lt: '2026-02-01' } } + +// Exists check (replaces NOT IS_DEFINED) +{ usedAt: { $exists: false } } + +// Array contains +{ tags: { $contains: 'important' } } +``` + +The Cosmos adapter translates these to SQL. The MongoDB adapter passes them directly (native MQL). The memory adapter does in-memory filtering. + +--- + +## 5. Sprint 2: Storage Abstraction Layer + +**Package:** `@bytelyst/storage` +**Effort:** 2 days +**Files changed:** `packages/blob/src/blob.ts`, `src/cloud/blob_client.py`, `services/platform-service/src/modules/blob/` + +### 5.1 Interface Design + +```typescript +// packages/storage/src/types.ts + +export interface StorageProvider { + /** Get or create a bucket/container. */ + getBucket(name: string): StorageBucket; + + /** Check if storage is configured. */ + isConfigured(): boolean; +} + +export interface StorageBucket { + /** Upload a blob/object. */ + upload(path: string, data: Buffer | ReadableStream, contentType?: string): Promise; + + /** Download a blob/object. */ + download(path: string): Promise; + + /** Delete a blob/object. */ + delete(path: string): Promise; + + /** Check if a blob/object exists. */ + exists(path: string): Promise; + + /** List blobs/objects with optional prefix. */ + list(prefix?: string): Promise; + + /** Generate a time-limited signed URL for direct access. */ + getSignedUrl(path: string, opts: SignedUrlOptions): Promise; +} + +export interface SignedUrlOptions { + permissions: 'read' | 'write' | 'readwrite'; + expiresInMinutes?: number; // default: 60 +} + +export interface StorageObjectInfo { + name: string; + size: number; + lastModified: Date; + contentType?: string; +} +``` + +### 5.2 Provider Implementations + +```typescript +// packages/storage/src/providers/azure-blob.ts +// Wraps existing @bytelyst/blob code — nearly 1:1 mapping + +// packages/storage/src/providers/s3.ts (future) +// Uses @aws-sdk/client-s3 + @aws-sdk/s3-request-presigner + +// packages/storage/src/providers/r2.ts (future) +// S3-compatible — extends S3 provider with Cloudflare-specific config + +// packages/storage/src/providers/memory.ts +// In-memory Map for testing +``` + +### 5.3 Migration + +The existing `@bytelyst/blob` package (162 lines) becomes the Azure Blob provider inside `@bytelyst/storage`. Consumers switch from: + +```typescript +// Before +import { generateSasUrl, getContainerClient } from '@bytelyst/blob'; +``` + +```typescript +// After +import { getStorage } from '@bytelyst/storage'; +const bucket = getStorage().getBucket('audio'); +const url = await bucket.getSignedUrl('user123/recording.wav', { permissions: 'read' }); +``` + +**Python equivalent:** Refactor `src/cloud/blob_client.py` to use a `StorageProvider` ABC with `AzureBlobProvider` implementation. + +--- + +## 6. Sprint 3: LLM Provider Abstraction + +**Package:** `@bytelyst/llm` +**Effort:** 2 days +**Files changed:** `src/llm/text_cleaner.py`, `backend/src/clients/openai_client.py`, MindLyst `web/src/lib/llm.ts`, extraction-service config + +### 6.1 Interface Design + +```typescript +// packages/llm/src/types.ts + +export interface LLMProvider { + chatCompletion(req: ChatCompletionRequest): Promise; + chatCompletionStream?(req: ChatCompletionRequest): AsyncIterable; + isConfigured(): boolean; +} + +export interface ChatCompletionRequest { + messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>; + temperature?: number; + maxTokens?: number; + model?: string; // override default model +} + +export interface ChatCompletionResponse { + content: string; + usage?: { promptTokens: number; completionTokens: number }; +} +``` + +### 6.2 Key Insight: MindLyst Already Has This Pattern + +MindLyst `web/src/lib/llm.ts` already auto-detects Azure vs OpenAI based on env vars. This pattern should be promoted to a shared package. + +**Provider implementations:** +- `AzureOpenAIProvider` — uses `api-key` header + deployment-scoped URL +- `OpenAIProvider` — uses `Authorization: Bearer` header + model param +- `GeminiProvider` — uses Google Generative AI SDK (future) +- `OllamaProvider` — for local development (future) + +### 6.3 Python Migration + +```python +# Before (text_cleaner.py) +from openai import AzureOpenAI +self._client = AzureOpenAI(azure_endpoint=endpoint, api_key=api_key, api_version="2024-10-21") + +# After +from bytelyst.llm import create_llm_client +self._client = create_llm_client() # reads LLM_PROVIDER, OPENAI_API_KEY, etc. +# Returns OpenAI() or AzureOpenAI() based on config — same API surface +``` + +The `openai` Python SDK already has a common interface between `OpenAI` and `AzureOpenAI`. The abstraction is just a factory function that picks the right class. + +--- + +## 7. Sprint 4: Secrets Manager Abstraction + +**Package:** Refactor existing `@bytelyst/config` +**Effort:** 1 day +**Files changed:** `packages/config/src/keyvault.ts`, `src/secrets/keyvault.py` + +### 7.1 Key Insight: Already 90% Done + +The current `resolveKeyVaultSecrets()` already: +- Skips if `AZURE_KEYVAULT_URL` is not set +- Falls back to env vars for each secret +- Logs warnings but doesn't throw + +**Refactor:** Rename to `resolveSecrets()` with provider dispatch: + +```typescript +// packages/config/src/secrets.ts + +export interface SecretsProvider { + getSecret(name: string): Promise; +} + +export async function resolveSecrets( + secrets: SecretMapping[], + opts?: { provider?: string }, +): Promise { + const provider = opts?.provider || process.env.SECRETS_PROVIDER || 'env'; + + switch (provider) { + case 'azure-keyvault': + return resolveFromAzureKeyVault(secrets); // existing code + case 'aws-secrets-manager': + return resolveFromAWSSecretsManager(secrets); // future + case 'gcp-secret-manager': + return resolveFromGCPSecretManager(secrets); // future + case 'doppler': + return resolveFromDoppler(secrets); // future + case 'env': + default: + return; // All secrets already in env — nothing to resolve + } +} +``` + +### 7.2 Rename Azure-Prefixed Env Vars + +The current env vars have Azure-specific names. Add **generic aliases** that fall back to the Azure names: + +```typescript +// packages/config/src/env-aliases.ts + +export const ENV_ALIASES: Record = { + // Generic name → fallback names (checked in order) + 'BLOB_CONNECTION_STRING': ['AZURE_BLOB_CONNECTION_STRING'], + 'BLOB_ACCOUNT_NAME': ['AZURE_BLOB_ACCOUNT_NAME'], + 'BLOB_ACCOUNT_KEY': ['AZURE_BLOB_ACCOUNT_KEY'], + 'SPEECH_KEY': ['AZURE_SPEECH_KEY'], + 'SPEECH_REGION': ['AZURE_SPEECH_REGION'], + 'LLM_API_KEY': ['AZURE_OPENAI_KEY', 'OPENAI_API_KEY'], + 'LLM_ENDPOINT': ['AZURE_OPENAI_ENDPOINT', 'OPENAI_BASE_URL'], + 'LLM_MODEL': ['AZURE_OPENAI_DEPLOYMENT', 'OPENAI_MODEL'], +}; + +export function getEnv(name: string): string | undefined { + if (process.env[name]) return process.env[name]; + const aliases = ENV_ALIASES[name]; + if (aliases) { + for (const alias of aliases) { + if (process.env[alias]) return process.env[alias]; + } + } + return undefined; +} +``` + +This means existing `.env` files with `AZURE_*` names continue to work. New deployments can use generic names. + +--- + +## 8. Sprint 5: Speech Provider Abstraction + +**Package:** `@bytelyst/speech` +**Effort:** 3–4 days +**Files changed:** `src/audio/azure_stt.py`, `iosApp/Services/AzureSpeechTranscriber.swift` + +### 8.1 Interface Design (Python) + +```python +# bytelyst/speech/types.py + +from abc import ABC, abstractmethod +from typing import Callable, Optional + +class SpeechTranscriber(ABC): + """Cloud-agnostic streaming speech-to-text interface.""" + + @abstractmethod + def start(self, language: str = "en-US", languages: list[str] | None = None) -> None: + """Start continuous recognition.""" + + @abstractmethod + def stop(self) -> None: + """Stop recognition.""" + + @abstractmethod + def push_audio(self, data: bytes) -> None: + """Push raw audio data (PCM 16-bit, 16kHz, mono).""" + + @abstractmethod + def on_partial(self, callback: Callable[[str], None]) -> None: + """Register callback for partial (interim) results.""" + + @abstractmethod + def on_final(self, callback: Callable[[str], None]) -> None: + """Register callback for final (committed) results.""" + + @abstractmethod + def on_error(self, callback: Callable[[Exception], None]) -> None: + """Register callback for errors.""" + + @abstractmethod + def set_vocabulary(self, phrases: list[str]) -> None: + """Set custom vocabulary / phrase hints.""" +``` + +### 8.2 Provider Implementations + +```python +# bytelyst/speech/azure_provider.py +# Wraps existing azure_stt.py code — PushAudioInputStream, SpeechRecognizer, events + +# bytelyst/speech/google_provider.py (future) +# Uses google-cloud-speech streaming_recognize + +# bytelyst/speech/deepgram_provider.py (future) +# Uses Deepgram WebSocket API + +# bytelyst/speech/whisper_provider.py (future) +# Uses faster-whisper for local transcription (already in requirements.txt!) +``` + +### 8.3 Swift Protocol (iOS) + +```swift +// Shared/Speech/SpeechTranscriberProtocol.swift + +protocol SpeechTranscriber { + func start(language: String, languages: [String]?) async throws + func stop() async + func onPartial(_ handler: @escaping (String) -> Void) + func onFinal(_ handler: @escaping (String) -> Void) + func onError(_ handler: @escaping (Error) -> Void) + func setVocabulary(_ phrases: [String]) +} + +// Shared/Speech/AzureSpeechTranscriber.swift — existing code, implements protocol +// Shared/Speech/AppleSpeechTranscriber.swift — future, uses Apple's SFSpeechRecognizer +``` + +### 8.4 Note on Complexity + +Speech is the hardest abstraction because: +- Azure Speech SDK has a unique push-stream architecture +- Google Cloud Speech uses gRPC streaming +- Deepgram uses WebSockets +- Each has different audio format requirements and event models + +The abstraction hides these differences behind a unified push-audio + callback interface. The Azure implementation wraps existing code with zero behavior changes. + +--- + +## 9. Sprint 6: Push Notification Abstraction + +**Package:** `@bytelyst/push` +**Effort:** 1 day +**Files changed:** Platform-service push-triggers module + +### 9.1 Interface Design + +```typescript +export interface PushProvider { + send(notification: PushNotification): Promise; + sendBatch(notifications: PushNotification[]): Promise; +} + +export interface PushNotification { + deviceToken: string; + platform: 'ios' | 'android' | 'web'; + title: string; + body: string; + data?: Record; + badge?: number; +} +``` + +Implementations: `AzureNotificationHubProvider`, `FirebaseProvider` (future), `ExpoProvider` (for NomGap), `OneSignalProvider` (future). + +--- + +## 10. Sprint 7: Monitoring & Telemetry Abstraction + +**Effort:** 0.5 days (mostly done already) + +The ecosystem already has cloud-agnostic monitoring: +- **Custom telemetry** via `@bytelyst/telemetry-client` → platform-service → Cosmos +- **Loki + Grafana** in `services/monitoring/` +- **Health checks** via `/health` endpoints on all services + +**Remaining work:** +- Remove `opencensus-ext-azure` from Python requirements (optional, only used for App Insights) +- Ensure all structured logging uses `pino` (TS) or `structlog` (Python) — no Azure-specific loggers + +--- + +## 11. Migration Effort After Refactor + +Once all sprints are complete, here's how much work each cloud migration scenario requires: + +### Scenario: Switch DB from Cosmos to MongoDB Atlas + +| Step | Effort | Description | +|------|--------|-------------| +| Implement `MongoDatastoreProvider` | 1 day | ~200 lines — translate FilterMap to MongoDB find() | +| Set `DB_PROVIDER=mongo` + `MONGO_URI=...` | 5 minutes | Config change | +| Run data migration script | 2–4 hours | Export Cosmos JSON → import to MongoDB | +| Run full test suite | 30 minutes | Verify all 1,029+ tests pass | +| **Total** | **~1.5 days** | vs 3–5 weeks without abstraction | + +### Scenario: Switch Storage from Azure Blob to S3 + +| Step | Effort | Description | +|------|--------|-------------| +| Implement `S3StorageProvider` | 0.5 day | ~100 lines | +| Set `STORAGE_PROVIDER=s3` + `AWS_*` env vars | 5 minutes | Config change | +| Migrate blobs | 1–2 hours | azcopy or rclone | +| **Total** | **~0.5 days** | vs 2–3 days without abstraction | + +### Scenario: Switch LLM from Azure OpenAI to OpenAI Direct + +| Step | Effort | Description | +|------|--------|-------------| +| Set `LLM_PROVIDER=openai` + `OPENAI_API_KEY=...` | 5 minutes | Config change only | +| Remove `AZURE_OPENAI_*` env vars | 5 minutes | Cleanup | +| **Total** | **10 minutes** | Already near-zero today | + +### Scenario: Full Cloud Migration (Azure → AWS) + +| Step | Effort | Description | +|------|--------|-------------| +| Implement MongoDB/DynamoDB provider | 1–2 days | | +| Implement S3 storage provider | 0.5 days | | +| Implement AWS Secrets Manager provider | 0.5 days | | +| Switch LLM to OpenAI direct | 10 minutes | | +| Implement Google STT or AWS Transcribe | 2–3 days | Speech is still the hardest | +| Implement SNS push provider | 0.5 days | | +| Data migration + testing | 2–3 days | | +| **Total** | **~7–10 days** | vs 4–8 weeks without abstraction | + +--- + +## 12. Testing Strategy + +### 12.1 Provider-Agnostic Tests + +Every repository test should work against **any** provider. The test setup picks the provider: + +```typescript +// Test setup: use in-memory provider +import { setTestProvider } from '@bytelyst/datastore/testing'; + +beforeAll(() => { + setTestProvider('memory'); // Fast, no network, deterministic +}); +``` + +### 12.2 Provider Integration Tests + +Separate test suites verify each provider works correctly: + +``` +__tests__/ + datastore/ + cosmos.integration.test.ts # Runs against real Cosmos (CI only) + mongo.integration.test.ts # Runs against real MongoDB (CI only) + memory.test.ts # Always runs — verifies memory provider +``` + +### 12.3 Migration Verification Checklist + +For each sprint, before merging: +1. All existing tests pass (no regressions) +2. New interface tests pass with all implemented providers +3. Manual smoke test against Azure (dev environment) +4. No new `@azure/*` imports in application code (only in provider files) + +### 12.4 CI Gate + +Add a lint rule to prevent direct Azure SDK imports outside of provider directories: + +```bash +# scripts/check-cloud-agnostic.sh +# Fail if any file outside packages/*/providers/ imports @azure/* +rg '@azure/' services/ dashboards/ --include='*.ts' \ + --glob='!**/providers/**' --glob='!**/node_modules/**' \ + && echo "FAIL: Direct Azure SDK import found outside provider layer" && exit 1 \ + || echo "PASS: No direct Azure imports in application code" +``` + +--- + +## 13. Env Var Naming Convention + +### Current (Azure-specific) + +```bash +COSMOS_ENDPOINT=https://cosmos-mywisprai.documents.azure.com:443/ +COSMOS_KEY=... +COSMOS_DATABASE=lysnrai +AZURE_BLOB_CONNECTION_STRING=... +AZURE_BLOB_ACCOUNT_NAME=bytelystblobs +AZURE_BLOB_ACCOUNT_KEY=... +AZURE_OPENAI_ENDPOINT=... +AZURE_OPENAI_KEY=... +AZURE_OPENAI_DEPLOYMENT=gpt-4o-mini +AZURE_SPEECH_KEY=... +AZURE_SPEECH_REGION=eastus +AZURE_KEYVAULT_URL=... +``` + +### Target (generic with Azure fallbacks) + +```bash +# ── Provider Selection ──────────────────────────── +DB_PROVIDER=cosmos # cosmos | mongo | memory +STORAGE_PROVIDER=azure # azure | s3 | r2 | memory +LLM_PROVIDER=azure # azure | openai | gemini +SECRETS_PROVIDER=azure-keyvault # azure-keyvault | aws | doppler | env +SPEECH_PROVIDER=azure # azure | google | deepgram | whisper +PUSH_PROVIDER=azure-nh # azure-nh | firebase | expo + +# ── Database (provider-specific) ────────────────── +# Cosmos (when DB_PROVIDER=cosmos): +COSMOS_ENDPOINT=... +COSMOS_KEY=... +COSMOS_DATABASE=lysnrai +# MongoDB (when DB_PROVIDER=mongo): +# MONGO_URI=mongodb+srv://... + +# ── Storage (provider-specific) ─────────────────── +# Azure (when STORAGE_PROVIDER=azure): +AZURE_BLOB_CONNECTION_STRING=... +# S3 (when STORAGE_PROVIDER=s3): +# AWS_ACCESS_KEY_ID=... +# AWS_SECRET_ACCESS_KEY=... +# S3_BUCKET_PREFIX=bytelyst- + +# ── LLM (provider-specific) ────────────────────── +# Azure OpenAI: +AZURE_OPENAI_ENDPOINT=... +AZURE_OPENAI_KEY=... +AZURE_OPENAI_DEPLOYMENT=gpt-4o-mini +# OpenAI direct: +# OPENAI_API_KEY=... +# OPENAI_MODEL=gpt-4o-mini + +# ── Secrets (optional) ─────────────────────────── +AZURE_KEYVAULT_URL=... # only if SECRETS_PROVIDER=azure-keyvault + +# ── Speech ──────────────────────────────────────── +AZURE_SPEECH_KEY=... +AZURE_SPEECH_REGION=eastus +``` + +**Backward compatibility:** All existing `AZURE_*` env vars continue to work. The generic `*_PROVIDER` vars are additive. + +--- + +## 14. Risk Mitigation + +| Risk | Mitigation | +|------|-----------| +| **FilterMap can't express complex Cosmos SQL** | Add `rawQuery()` escape hatch for edge cases. Track usage — if >5% of queries need it, expand FilterMap operators | +| **Performance regression from abstraction layer** | Benchmark critical queries before/after. The abstraction adds one function call — negligible | +| **Team unfamiliar with new patterns** | Each sprint includes updating AGENTS.md with new conventions. Old pattern (direct Cosmos) still works during migration | +| **In-memory provider behaves differently** | Integration test suite runs against real Cosmos in CI. Memory provider is for unit tests only | +| **Stale data during DB migration** | Use dual-write pattern: write to both old and new provider during transition. Read from new, fall back to old | +| **Sprint 1 takes too long** | The 38 repository files can be migrated incrementally — even 5 files at a time is progress. Old and new patterns coexist | + +--- + +## Appendix: Interface Specifications + +### A.1 `@bytelyst/datastore` — Package Structure + +``` +packages/datastore/ +├── src/ +│ ├── index.ts # Public exports +│ ├── types.ts # All interfaces (DocumentCollection, DatastoreProvider, etc.) +│ ├── factory.ts # createDatastoreProvider() factory +│ ├── filter.ts # FilterMap → provider-specific query translation +│ ├── providers/ +│ │ ├── cosmos.ts # CosmosDatastoreProvider + CosmosCollection +│ │ ├── mongo.ts # MongoDatastoreProvider + MongoCollection (stub) +│ │ └── memory.ts # MemoryDatastoreProvider + MemoryCollection +│ └── testing.ts # Test helpers (setTestProvider, seedCollection, etc.) +├── package.json # peer deps: @azure/cosmos (optional), mongodb (optional) +├── tsconfig.json +└── vitest.config.ts +``` + +### A.2 `@bytelyst/storage` — Package Structure + +``` +packages/storage/ +├── src/ +│ ├── index.ts +│ ├── types.ts # StorageProvider, StorageBucket, SignedUrlOptions +│ ├── factory.ts # createStorageProvider() +│ ├── providers/ +│ │ ├── azure-blob.ts # Wraps existing @bytelyst/blob code +│ │ ├── s3.ts # AWS S3 (stub) +│ │ └── memory.ts # In-memory for testing +│ └── testing.ts +├── package.json +└── tsconfig.json +``` + +### A.3 `@bytelyst/llm` — Package Structure + +``` +packages/llm/ +├── src/ +│ ├── index.ts +│ ├── types.ts # LLMProvider, ChatCompletionRequest/Response +│ ├── factory.ts # createLLMProvider() +│ ├── providers/ +│ │ ├── azure-openai.ts # AzureOpenAI endpoint + api-key auth +│ │ ├── openai.ts # OpenAI direct + Bearer auth +│ │ └── gemini.ts # Google Generative AI (stub) +│ └── testing.ts # MockLLMProvider for tests +├── package.json +└── tsconfig.json +``` + +### A.4 Complete Interface: FilterMap Operators + +```typescript +// Exact match +{ field: value } + +// Comparison +{ field: { $gt: value } } // > +{ field: { $gte: value } } // >= +{ field: { $lt: value } } // < +{ field: { $lte: value } } // <= +{ field: { $ne: value } } // != + +// Existence +{ field: { $exists: true } } // IS_DEFINED(c.field) +{ field: { $exists: false } } // NOT IS_DEFINED(c.field) + +// String +{ field: { $startsWith: 'prefix' } } +{ field: { $contains: 'substr' } } + +// Array +{ field: { $contains: value } } // ARRAY_CONTAINS +{ field: { $in: [v1, v2, v3] } } // IN operator + +// Logical (for complex queries) +{ $or: [{ field1: v1 }, { field2: v2 }] } +``` + +**Cosmos adapter** translates each operator to SQL: +- `{ $gte: v }` → `c.field >= @pN` +- `{ $exists: false }` → `NOT IS_DEFINED(c.field)` +- `{ $contains: v }` on array → `ARRAY_CONTAINS(c.field, @pN)` +- `{ $in: [...] }` → `c.field IN (@pN, @pM, ...)` + +**MongoDB adapter** passes operators natively (MQL uses the same `$gte`, `$exists` syntax). + +**Memory adapter** evaluates operators with simple JS comparisons. + +--- + +## Summary + +| Sprint | What | Days | After This Sprint... | +|--------|------|------|---------------------| +| 1 | Database abstraction | 5–7 | DB swap = implement 1 adapter (~200 LOC) + config change | +| 2 | Storage abstraction | 2 | Blob swap = implement 1 adapter (~100 LOC) + config change | +| 3 | LLM abstraction | 2 | LLM swap = config change only (10 minutes) | +| 4 | Secrets abstraction | 1 | Secrets swap = config change only | +| 5 | Speech abstraction | 3–4 | Speech swap = implement 1 adapter (~300 LOC) | +| 6 | Push abstraction | 1 | Push swap = implement 1 adapter (~50 LOC) | +| 7 | Monitoring cleanup | 0.5 | Already cloud-agnostic | +| **Total** | | **~15–17 days** | **Full cloud migration = ~7–10 days instead of 4–8 weeks** | + +The key insight: **~80% of migration effort is in Sprint 1 (database)**. If you only do one sprint, do that one. Everything else is comparatively easy. + +--- + +*Document generated by automated codebase analysis. Companion to `CLOUD_PROVIDER_MIGRATION_ANALYSIS.md`. Review as the codebase evolves.* diff --git a/docs/CLOUD/CLOUD_PROVIDER_MIGRATION_ANALYSIS.md b/docs/CLOUD/CLOUD_PROVIDER_MIGRATION_ANALYSIS.md new file mode 100644 index 00000000..666b184e --- /dev/null +++ b/docs/CLOUD/CLOUD_PROVIDER_MIGRATION_ANALYSIS.md @@ -0,0 +1,726 @@ +# Cloud Provider Migration Analysis — ByteLyst Ecosystem + +> **Author:** AI Analysis (Cascade) +> **Date:** 2026-03-01 +> **Scope:** All 7 repos — LysnrAI, MindLyst, ChronoMind, NomGap, PeakPulse, Common Platform, JarvisJr +> **Purpose:** Evaluate current Azure investment, assess migration feasibility to AWS / GCP / MongoDB Atlas / multi-cloud, and provide actionable recommendations. + +--- + +## Table of Contents + +1. [Executive Summary](#1-executive-summary) +2. [Current Azure Investment Inventory](#2-current-azure-investment-inventory) +3. [Dependency Depth Analysis](#3-dependency-depth-analysis) +4. [Migration Target Comparison](#4-migration-target-comparison) +5. [Per-Service Migration Analysis](#5-per-service-migration-analysis) +6. [Migration Scenario Scoring](#6-migration-scenario-scoring) +7. [Cost Comparison](#7-cost-comparison) +8. [Abstraction Layer Assessment](#8-abstraction-layer-assessment) +9. [Risk Analysis](#9-risk-analysis) +10. [Recommendations](#10-recommendations) +11. [Migration Playbook (If Chosen)](#11-migration-playbook-if-chosen) +12. [Appendix A: File-Level Azure Dependency Map](#appendix-a-file-level-azure-dependency-map) +13. [Appendix B: SDK & Package Inventory](#appendix-b-sdk--package-inventory) + +--- + +## 1. Executive Summary + +The ByteLyst ecosystem is **moderately coupled** to Azure. The coupling is concentrated in **3 packages** (`@bytelyst/cosmos`, `@bytelyst/blob`, `@bytelyst/config`) and **2 Python modules** (`azure_stt.py`, `cosmos_client.py`). The architecture already uses an internal abstraction layer — most application code never imports Azure SDKs directly. + +### Key Findings + +| Dimension | Assessment | +|-----------|-----------| +| **Overall Azure lock-in** | **Medium** — concentrated in ~15 files, but those files are foundational | +| **Easiest to migrate** | Blob Storage, Key Vault, OpenAI, Application Insights | +| **Hardest to migrate** | Cosmos DB (SQL API queries in 56+ repository files), Azure Speech SDK | +| **Best alternative DB** | MongoDB Atlas (closest query model to Cosmos SQL API) | +| **Best alternative cloud** | AWS (broadest service parity, mature SDK ecosystem) | +| **Estimated migration effort** | 4–8 weeks for full cloud swap (Cosmos DB is the long pole) | +| **Recommendation** | **Stay on Azure** for now, but invest in abstraction layers to reduce future switching cost | + +### Azure Services Used (8 total) + +| # | Azure Service | Monthly Cost | Lock-in Risk | Files Affected | +|---|--------------|-------------|-------------|----------------| +| 1 | **Cosmos DB** (SQL/NoSQL API) | ~$4–10 | **HIGH** | 56+ repository files, 3 databases, ~45 containers | +| 2 | **Blob Storage** | ~$0.20 | LOW | 2 packages + 1 Python module | +| 3 | **Azure OpenAI** | ~$5–10 | LOW | 3 files (already supports OpenAI fallback) | +| 4 | **Speech Services** | $0 (F0) | **HIGH** | 2 files (deep SDK integration, streaming) | +| 5 | **Key Vault** | ~$0.06 | LOW | 2 files (1 TS, 1 Python) | +| 6 | **Notification Hubs** | $0 (Free) | MEDIUM | Planned, not yet deeply integrated | +| 7 | **Application Insights** | $0 (5GB free) | LOW | 1 file (custom telemetry already built) | +| 8 | **Azure Identity** (DefaultAzureCredential) | $0 | LOW | Used by Key Vault + Secrets Manager | + +--- + +## 2. Current Azure Investment Inventory + +### 2.1 Azure Resources (from Azure Portal) + +| Resource | Azure Name | Region | SKU | Status | +|----------|-----------|--------|-----|--------| +| Resource Group | `rg-mywisprai` | East US | — | Active | +| Cosmos DB | `cosmos-mywisprai` | West US 2 | Serverless | Active — 3 DBs, ~45 containers | +| Blob Storage | `bytelystblobs` | West US 2 | StorageV2, RAGRS | Active — 9+ containers | +| Azure OpenAI | `mywisprai-openai-sweden` | Sweden Central | S0 | Active — gpt-4o-mini deployment | +| Speech Service | `mywisprai-speech` | East US | F0 (Free) | Active | +| Key Vault | `kv-mywisprai` | East US | Standard | Active — ~25 secrets | +| Notification Hubs | `lysnnai` namespace | East US | Free | Active — 2 hubs | +| App Insights | `bytelyst-appinsights` | East US | Classic | Active | + +### 2.2 Cosmos DB Databases & Containers + +| Database | Containers | Products Using | +|----------|-----------|----------------| +| `lysnrai` | ~27 containers (users, subscriptions, feature_flags, audit_log, tracker_items, telemetry_events, etc.) | LysnrAI, platform-service (all products) | +| `mindlyst` | ~20 containers (brains, memory_items, streaks, reflections, etc.) | MindLyst | +| `mywisprai` | 10 containers (legacy, pre-rebrand) | Legacy / migration target | + +**Total: ~57 containers across 3 databases**, all using Cosmos SQL (NoSQL) API with SQL-like queries (`SELECT`, `WHERE`, `ORDER BY`, `OFFSET/LIMIT`, aggregate functions). + +### 2.3 Code Investment by Language + +| Language | Azure SDK Packages | Files Using Azure | Lines of Azure-Specific Code | +|----------|-------------------|-------------------|------------------------------| +| **TypeScript** | `@azure/cosmos`, `@azure/storage-blob`, `@azure/identity`, `@azure/keyvault-secrets` | ~65 files | ~500 lines | +| **Python** | `azure-cognitiveservices-speech`, `azure-cosmos`, `azure-storage-blob`, `azure-identity`, `azure-keyvault-secrets`, `openai` (AzureOpenAI) | ~8 files | ~400 lines | +| **Swift** | `MicrosoftCognitiveServicesSpeech` (SPX framework) | ~3 files | ~150 lines | +| **Kotlin** | None directly (uses platform-service REST API) | 0 files | 0 lines | + +--- + +## 3. Dependency Depth Analysis + +### 3.1 Cosmos DB — DEEP (56+ files) + +This is the **most deeply embedded** Azure dependency. Every repository module follows the pattern: + +``` +types.ts → repository.ts → routes.ts + ↑ + Uses @azure/cosmos SDK + SQL queries: SELECT c.id, c.name FROM c WHERE c.productId = @pid +``` + +**Touchpoints:** +- `packages/cosmos/` — shared client singleton (`@azure/cosmos` peer dep) +- `services/platform-service/src/modules/*/repository.ts` — **56 repository files** with Cosmos SQL queries +- `services/extraction-service/src/modules/*/repository.ts` — 2 repository files +- `dashboards/admin-web/src/lib/cosmos.ts` — direct `@azure/cosmos` import +- `dashboards/admin-web/src/lib/repositories/*.ts` — 4 repository files +- `mindlyst-native/web/src/lib/cosmos.ts` — direct `@azure/cosmos` import +- `learning_voice_ai_agent/src/cloud/cosmos_client.py` — Python Cosmos client +- `learning_voice_ai_agent/backend/src/cloud/cosmos.py` — Python backend Cosmos client + +**Query patterns used:** +- `container.items.query()` with parameterized SQL +- `container.items.create()`, `.replace()`, `.delete()`, `.read()` +- `container.items.upsert()` +- Partition key routing (`/userId`, `/productId`, `/id`) +- Cross-partition queries (admin/analytics) +- `SELECT VALUE COUNT(1)` aggregates +- `OFFSET ... LIMIT` pagination +- `ORDER BY` sorting +- `ARRAY_CONTAINS()` for array queries + +### 3.2 Azure Speech SDK — DEEP (3 files, streaming integration) + +The Speech SDK is used for **real-time streaming speech-to-text** with features that are tightly coupled to the Azure SDK's event-driven architecture: + +- `src/audio/azure_stt.py` — 248 lines. Uses `PushAudioInputStream`, `SpeechRecognizer`, continuous recognition with `recognizing`/`recognized`/`canceled`/`session_stopped` event callbacks, `PhraseListGrammar`, auto-language detection (10 languages), auto-reconnect +- `src/ui/settings.py` + `src/ui/unified_window.py` — connection testing +- `mindlyst-native/iosApp/Services/AzureSpeechTranscriber.swift` — iOS Swift SPX framework +- `mobile_app/ios/LysnrAI/` — iOS keyboard extension uses SPX framework + +### 3.3 Blob Storage — SHALLOW (3 files) + +- `packages/blob/src/blob.ts` — 162 lines, singleton client, SAS URL generation +- `src/cloud/blob_client.py` — 190 lines, Python equivalent +- `services/platform-service/src/modules/blob/` — REST API wrapper + +### 3.4 Azure OpenAI — SHALLOW (3 files, already abstracted) + +- `src/llm/text_cleaner.py` — uses `openai.AzureOpenAI` (OpenAI SDK with Azure endpoint) +- `backend/src/clients/openai_client.py` — uses `openai.AsyncAzureOpenAI` +- `mindlyst-native/web/src/lib/llm.ts` — **already has OpenAI fallback** (resolves provider dynamically) + +The `openai` Python/JS SDK supports both Azure and OpenAI endpoints with minimal config change. MindLyst web already handles this automatically. + +### 3.5 Key Vault — SHALLOW (2 files) + +- `packages/config/src/keyvault.ts` — 90 lines, `resolveKeyVaultSecrets()` with graceful fallback +- `src/secrets/keyvault.py` — 69 lines, `SecretResolver` class with env var fallback + +Both implementations already fall back to environment variables when Key Vault is unavailable. Migration = just stop using Key Vault and use the env var path. + +### 3.6 Notification Hubs — NOT YET INTEGRATED + +Planned but not deeply wired. Only namespace/hub exists in Azure. Mobile apps use `BLPlatformClient` (REST) to talk to platform-service, which would route push notifications. + +### 3.7 Application Insights — SHALLOW (1 file) + +- `opencensus-ext-azure` in Python requirements (optional telemetry) +- Custom telemetry system already built (`@bytelyst/telemetry-client`, platform-service telemetry module with Cosmos storage) + +The custom telemetry system means App Insights is supplementary, not critical. + +--- + +## 4. Migration Target Comparison + +### 4.1 Database: Cosmos DB → Alternatives + +| Feature | Azure Cosmos DB (current) | MongoDB Atlas | AWS DynamoDB | Google Firestore | PostgreSQL (Supabase/Neon) | +|---------|--------------------------|---------------|-------------|-----------------|---------------------------| +| **Data model** | Document (JSON) | Document (JSON) | Key-Value + Document | Document (JSON) | Relational + JSONB | +| **Query language** | SQL-like | MQL (MongoDB Query) | PartiQL / API | GQL-like API | SQL | +| **Partition keys** | Required | Shard keys (optional) | Required | Collection groups | Not applicable | +| **Serverless** | Yes | Yes (Atlas Serverless) | Yes | Yes | Yes (Neon) | +| **SQL queries** | `SELECT c.id FROM c WHERE c.x = @y` | `db.collection.find({x: y})` | `SELECT id FROM table WHERE x = ?` | Client SDK queries | Standard SQL | +| **Aggregates** | Basic (`COUNT`, `SUM`, `AVG`) | Full (`$group`, `$match`, `$lookup`) | Limited | Limited | Full SQL | +| **Cross-partition** | Yes (expensive) | Yes (scatter-gather) | Scan (expensive) | Yes | N/A | +| **Change feed** | Yes | Change Streams | DynamoDB Streams | Real-time listeners | Logical replication | +| **Global distribution** | Built-in multi-region | Atlas Global Clusters | Global Tables | Multi-region | Manual / Citus | +| **Max doc size** | 2 MB | 16 MB | 400 KB | 1 MB | Unlimited (JSONB) | +| **Free tier** | 1000 RU/s + 25 GB | 512 MB | 25 GB + 25 WCU/RCU | 1 GiB + 50K reads/day | 0.5 GB (Neon) | +| **Migration effort** | — | **Medium** (query rewrite) | **Hard** (paradigm shift) | **Hard** (no SQL) | **Hard** (schema design) | + +### 4.2 Object Storage: Blob → Alternatives + +| Feature | Azure Blob (current) | AWS S3 | GCP Cloud Storage | Cloudflare R2 | MinIO (self-hosted) | +|---------|---------------------|--------|-------------------|---------------|---------------------| +| **API compatibility** | Azure Blob API | S3 API | GCS API / S3-compat | S3-compatible | S3-compatible | +| **SAS tokens** | Yes (Azure SAS) | Pre-signed URLs | Signed URLs | Pre-signed URLs | Pre-signed URLs | +| **CDN integration** | Azure CDN | CloudFront | Cloud CDN | Built-in | Manual | +| **Cost (per GB)** | $0.018 (Cool) | $0.023 (Standard) | $0.020 | $0.015 (no egress) | Self-hosted | +| **Migration effort** | — | **Easy** | **Easy** | **Easy** | **Easy** | + +### 4.3 Speech-to-Text: Azure Speech → Alternatives + +| Feature | Azure Speech (current) | AWS Transcribe | Google Speech-to-Text | Deepgram | Whisper (local) | +|---------|----------------------|----------------|----------------------|----------|-----------------| +| **Streaming STT** | Yes (push stream) | Yes (WebSocket) | Yes (streaming) | Yes (WebSocket) | No (batch only) | +| **Languages** | 100+ | 100+ | 125+ | 36+ | 99+ | +| **Auto-detect lang** | Up to 10 at-once | Yes | Yes | Yes | Yes | +| **Custom vocabulary** | PhraseListGrammar | Custom vocabulary | Speech adaptation | Keywords | No | +| **Native SDK** | Python, Swift (SPX), JS | Python, no iOS SDK | Python, iOS, JS | REST/WebSocket | Python only | +| **iOS native SDK** | SPX framework (ObjC) | No native SDK | Yes (gRPC) | No native SDK | No | +| **Free tier** | 5 hrs/month (F0) | 60 min/month | 60 min/month | None | Free (local GPU) | +| **Latency** | ~200ms | ~300ms | ~200ms | ~100ms | ~500ms+ (local) | +| **Migration effort** | — | **Hard** (no iOS SDK) | **Medium** (has iOS SDK) | **Medium** (REST only) | **Hard** (no streaming) | + +### 4.4 LLM / AI: Azure OpenAI → Alternatives + +| Feature | Azure OpenAI (current) | OpenAI API (direct) | Google Gemini | AWS Bedrock | Anthropic Claude | +|---------|----------------------|--------------------|--------------|-----------| -----------------| +| **Models** | GPT-4o, GPT-4o-mini | Same models | Gemini 2.5 | Claude, Llama, Titan | Claude 3.5/4 | +| **API compatibility** | OpenAI SDK (azure mode) | OpenAI SDK (native) | Google SDK | AWS SDK | Anthropic SDK | +| **Data residency** | Azure regions | US only | Google regions | AWS regions | US/EU | +| **Cost (GPT-4o-mini)** | $0.15/$0.60 per M tokens | $0.15/$0.60 per M tokens | ~$0.10/$0.40 (Flash) | Varies | ~$0.25/$1.25 (Haiku) | +| **Migration effort** | — | **Trivial** (change endpoint) | **Easy** (SDK swap) | **Medium** | **Easy** (SDK swap) | + +### 4.5 Secrets Management: Key Vault → Alternatives + +| Feature | Azure Key Vault (current) | AWS Secrets Manager | GCP Secret Manager | HashiCorp Vault | Doppler / Infisical | +|---------|--------------------------|--------------------|--------------------|-----------------|---------------------| +| **Cost** | $0.03/10K ops | $0.40/secret/month | $0.06/10K ops | Free (OSS) | Free tier | +| **SDK** | `@azure/keyvault-secrets` | `@aws-sdk/client-secrets-manager` | `@google-cloud/secret-manager` | HTTP API | SDK / CLI | +| **Migration effort** | — | **Easy** | **Easy** | **Medium** | **Easy** | + +**Note:** The codebase already falls back to env vars when Key Vault is unavailable. This means Key Vault can be replaced by **any** secrets manager or simply .env files without code changes to application logic. + +### 4.6 Push Notifications: Notification Hubs → Alternatives + +| Feature | Azure NH (current) | AWS SNS | Firebase Cloud Messaging | OneSignal | Expo Push | +|---------|-------------------|---------|--------------------------|-----------|-----------| +| **APNs + FCM** | Yes | Yes | FCM only (APNs via FCM) | Yes | Yes | +| **Free tier** | 1M pushes/month | 1M publishes | Unlimited | 10K subscribers | Unlimited | +| **Migration effort** | — | **Easy** | **Easy** | **Easy** | **Easy** (NomGap uses Expo) | + +--- + +## 5. Per-Service Migration Analysis + +### 5.1 Cosmos DB → MongoDB Atlas + +**Difficulty: MEDIUM-HIGH** | **Effort: 3–5 weeks** | **Risk: MEDIUM** + +This is the **single largest migration task**. Here's why: + +#### What needs to change + +| Layer | Current (Cosmos SQL API) | Target (MongoDB) | Files | +|-------|--------------------------|-------------------|-------| +| Client package | `@azure/cosmos` → `CosmosClient` | `mongodb` → `MongoClient` | `packages/cosmos/src/client.ts` | +| Container registry | `getContainer(name)` | `db.collection(name)` | `packages/cosmos/src/containers.ts` | +| All repository files | `container.items.query('SELECT...')` | `collection.find({...})` | **56+ files** in platform-service | +| Dashboard Cosmos clients | `@azure/cosmos` direct | `mongodb` direct | 2 files (admin, MindLyst) | +| Python clients | `azure.cosmos.CosmosClient` | `pymongo.MongoClient` | 2 files | +| Query syntax | SQL-like (`SELECT c.id FROM c WHERE c.productId = @pid AND c.userId = @uid ORDER BY c.createdAt DESC OFFSET 0 LIMIT 20`) | MQL (`collection.find({productId: pid, userId: uid}).sort({createdAt: -1}).skip(0).limit(20)`) | All repository files | +| Partition keys | Explicit partition key in every query | Shard key (auto-routed) | All repository files | +| Upsert | `container.items.upsert(doc)` | `collection.updateOne({_id: id}, {$set: doc}, {upsert: true})` | ~20 files | +| Read by ID | `container.item(id, partitionKey).read()` | `collection.findOne({_id: id})` | All repository files | + +#### What stays the same +- Document structure (JSON documents with `id`, `productId`, partition keys) +- Data model (no schema changes needed — MongoDB is also schemaless) +- Partition key concept maps to shard key +- Serverless pricing model available on both + +#### Key migration steps +1. Update `@bytelyst/cosmos` package to export MongoDB-compatible API +2. Rewrite all SQL queries to MQL (56+ files) +3. Replace `container.items.query()` → `collection.find()` +4. Replace `container.item(id, pk).read()` → `collection.findOne({_id: id})` +5. Replace `container.items.create()` → `collection.insertOne()` +6. Replace `container.items.replace()` → `collection.replaceOne()` +7. Replace `container.items.upsert()` → `collection.updateOne({upsert: true})` +8. Update Python clients similarly +9. Migrate data (use Azure Data Factory or custom script) +10. Update all test mocks + +#### Why MongoDB Atlas is the best DB alternative +- **Closest query model** to Cosmos SQL API (both are document DBs) +- **MongoDB has a Cosmos DB compatibility mode** (but going native is better) +- Cosmos DB was originally inspired by MongoDB's document model +- MongoDB's `find()` queries map closely to Cosmos SQL `SELECT` queries +- Both support partition/shard keys, TTL indexes, change streams +- MongoDB Atlas Serverless pricing is competitive +- MongoDB has excellent TypeScript and Python SDKs + +### 5.2 Azure Speech → Google Cloud Speech-to-Text + +**Difficulty: HIGH** | **Effort: 2–3 weeks** | **Risk: HIGH** + +#### Why this is hard +- The Azure Speech SDK uses a **push-stream architecture** (`PushAudioInputStream`) that is deeply integrated into the audio pipeline +- The `SpeechRecognizer` has event-driven callbacks (`recognizing`, `recognized`, `canceled`, `session_stopped`) that the code relies on for real-time partial/final transcript delivery +- Custom vocabulary via `PhraseListGrammar` is Azure-specific +- Auto-language detection config is Azure-specific +- The **iOS SPX framework** (Objective-C) is used in LysnrAI keyboard extension and MindLyst — there's no direct equivalent for most alternatives + +#### Best alternative: Google Cloud Speech-to-Text +- Has streaming recognition with similar event model +- Has an iOS SDK (gRPC-based) +- Supports custom vocabulary (speech adaptation) +- Supports auto-language detection +- Similar pricing and free tier + +#### What needs to change +- `src/audio/azure_stt.py` — complete rewrite (~248 lines) +- `iosApp/Services/AzureSpeechTranscriber.swift` — complete rewrite +- `LysnrAI/LysnrKeyboard/` — keyboard extension STT integration +- Audio format handling (may differ between providers) +- Connection test code in settings UI + +### 5.3 Blob Storage → AWS S3 or Cloudflare R2 + +**Difficulty: LOW** | **Effort: 2–3 days** | **Risk: LOW** + +#### Why this is easy +- `@bytelyst/blob` package is a thin wrapper (162 lines) +- Only 3 files need changes +- S3 API is the de facto standard — R2, MinIO, GCS all support S3-compatible API +- SAS tokens → Pre-signed URLs (same concept, different implementation) + +#### What needs to change +- `packages/blob/src/blob.ts` — swap `@azure/storage-blob` → `@aws-sdk/client-s3` + `@aws-sdk/s3-request-presigner` +- `src/cloud/blob_client.py` — swap `azure.storage.blob` → `boto3` +- `services/platform-service/src/modules/blob/` — update routes for pre-signed URL format +- Environment variables: `AZURE_BLOB_*` → `AWS_S3_*` or `S3_*` + +### 5.4 Azure OpenAI → OpenAI API (direct) or Gemini + +**Difficulty: TRIVIAL** | **Effort: < 1 day** | **Risk: VERY LOW** + +#### Why this is trivial +- The `openai` Python SDK supports both Azure and OpenAI endpoints — just change config +- MindLyst web `llm.ts` **already auto-detects** Azure vs OpenAI and builds the correct URL +- LysnrAI desktop uses `AzureOpenAI` class from `openai` SDK — switch to `OpenAI` class +- Same models, same API shape, same pricing + +#### What needs to change +- Set `OPENAI_API_KEY` instead of `AZURE_OPENAI_*` env vars +- Change `AzureOpenAI(azure_endpoint=..., api_key=..., api_version=...)` → `OpenAI(api_key=...)` +- Change `AsyncAzureOpenAI(...)` → `AsyncOpenAI(...)` +- Remove `api_version` parameter +- That's it. The `openai` SDK handles the rest. + +### 5.5 Key Vault → Environment Variables / Any Secrets Manager + +**Difficulty: TRIVIAL** | **Effort: < 1 day** | **Risk: VERY LOW** + +Both `keyvault.ts` and `keyvault.py` already implement graceful fallback: +- If `AZURE_KEYVAULT_URL` is not set → uses env vars directly +- If Key Vault is unreachable → falls back to env vars + +**To migrate:** Simply stop setting `AZURE_KEYVAULT_URL`. Everything works via env vars. Then optionally adopt any other secrets manager (AWS Secrets Manager, Doppler, Infisical, etc.). + +### 5.6 Notification Hubs → Firebase Cloud Messaging + +**Difficulty: LOW** | **Effort: 1–2 days** | **Risk: LOW** + +Not yet deeply integrated. The platform-service notification module sends via REST API. Swap the push provider client. + +### 5.7 Application Insights → Self-hosted / Grafana + +**Difficulty: TRIVIAL** | **Effort: Already done** | **Risk: NONE** + +The ecosystem already has: +- Custom telemetry system (`@bytelyst/telemetry-client` → platform-service → Cosmos) +- Loki + Grafana in `services/monitoring/` +- App Insights is supplementary, can be dropped with zero code changes + +--- + +## 6. Migration Scenario Scoring + +### Scenario A: Stay on Azure (Status Quo) + +| Dimension | Score (1-5) | Notes | +|-----------|-------------|-------| +| Migration effort | **5** (none) | No work needed | +| Cost | **4** | ~$15/month at current scale, competitive | +| Vendor diversity | **1** | Single cloud vendor | +| Feature parity | **5** | Everything works today | +| **Total** | **15/20** | | + +### Scenario B: Full Migration to AWS + +| Dimension | Score (1-5) | Notes | +|-----------|-------------|-------| +| Migration effort | **2** | 6–8 weeks, Cosmos→DynamoDB is painful | +| Cost | **3** | Similar or slightly higher at small scale | +| Vendor diversity | **1** | Still single cloud, just different | +| Feature parity | **3** | No native iOS Speech SDK, DynamoDB query model is very different | +| **Total** | **9/20** | | + +### Scenario C: Multi-Cloud (MongoDB Atlas + OpenAI + R2 + Google STT) + +| Dimension | Score (1-5) | Notes | +|-----------|-------------|-------| +| Migration effort | **2** | 5–7 weeks, Cosmos→MongoDB is medium | +| Cost | **4** | MongoDB Atlas free tier, R2 no egress fees | +| Vendor diversity | **5** | No single-vendor dependency | +| Feature parity | **4** | MongoDB is a better document DB than Cosmos in many ways | +| **Total** | **15/20** | | + +### Scenario D: Stay Azure + Add Abstraction Layers + +| Dimension | Score (1-5) | Notes | +|-----------|-------------|-------| +| Migration effort | **4** | 1–2 weeks to add repository interface pattern | +| Cost | **4** | No change | +| Vendor diversity | **3** | Ready to switch, but still on Azure | +| Feature parity | **5** | Everything works today | +| **Total** | **16/20** | **Winner** | + +### Scenario E: Migrate DB Only (Cosmos → MongoDB Atlas, keep rest on Azure) + +| Dimension | Score (1-5) | Notes | +|-----------|-------------|-------| +| Migration effort | **3** | 3–5 weeks for DB migration | +| Cost | **4** | MongoDB Atlas Serverless may be cheaper | +| Vendor diversity | **3** | DB is independent, other services still Azure | +| Feature parity | **5** | MongoDB is very capable | +| **Total** | **15/20** | | + +--- + +## 7. Cost Comparison + +### Current Azure Costs (MVP / Low Usage) + +| Service | Monthly Cost | Notes | +|---------|-------------|-------| +| Cosmos DB (Serverless) | ~$4–10 | 3 databases, ~45 containers | +| Blob Storage (Cool, RAGRS) | ~$0.20 | 9+ containers | +| Azure OpenAI (GPT-4o-mini) | ~$5–10 | Pay per token | +| Speech (F0) | $0 | 5 hrs/month free | +| Key Vault | ~$0.06 | ~25 secrets | +| Notification Hubs (Free) | $0 | 1M pushes/month | +| App Insights | $0 | 5 GB/month free | +| **Total** | **~$10–20/month** | | + +### Equivalent AWS Costs + +| Service | AWS Equivalent | Monthly Cost | +|---------|---------------|-------------| +| Cosmos DB → DynamoDB (On-Demand) | DynamoDB | ~$5–15 | +| Blob → S3 Standard | S3 | ~$0.25 | +| Azure OpenAI → OpenAI API | Same pricing | ~$5–10 | +| Speech → Transcribe | Transcribe | ~$1–3 | +| Key Vault → Secrets Manager | Secrets Manager | ~$10 (per-secret pricing) | +| Notification Hubs → SNS | SNS | ~$0.50 | +| App Insights → CloudWatch | CloudWatch | ~$3 | +| **Total** | | **~$25–42/month** | + +### Equivalent Multi-Cloud Costs + +| Service | Provider | Monthly Cost | +|---------|---------|-------------| +| Cosmos DB → MongoDB Atlas Serverless | MongoDB | ~$3–8 | +| Blob → Cloudflare R2 | Cloudflare | ~$0.15 (no egress) | +| Azure OpenAI → OpenAI API (direct) | OpenAI | ~$5–10 | +| Speech → Google STT | Google Cloud | ~$1–3 | +| Key Vault → Doppler (free tier) | Doppler | $0 | +| Push → Firebase FCM | Google | $0 | +| Monitoring → Grafana Cloud (free) | Grafana | $0 | +| **Total** | | **~$10–22/month** | + +### Cost Summary + +| Scenario | Monthly Cost | vs Current | +|----------|-------------|-----------| +| **Azure (current)** | ~$10–20 | Baseline | +| **Full AWS** | ~$25–42 | +50–110% | +| **Multi-cloud** | ~$10–22 | ~Same | +| **MongoDB Atlas + Azure rest** | ~$10–18 | ~Same | + +**Verdict:** At current scale, cost is not a compelling reason to migrate. All options are under $50/month. Cost becomes more significant at scale (10K+ users), where MongoDB Atlas and R2 would likely be cheaper due to no egress fees and better serverless pricing. + +--- + +## 8. Abstraction Layer Assessment + +### Current State: Partially Abstracted + +The codebase already has meaningful abstraction: + +| Layer | Abstraction Level | Notes | +|-------|-------------------|-------| +| **Cosmos DB** | **Partial** — `@bytelyst/cosmos` package | Application code still writes raw SQL queries and uses `@azure/cosmos` types | +| **Blob Storage** | **Good** — `@bytelyst/blob` package | Thin wrapper, easy to swap internals | +| **OpenAI/LLM** | **Good** — MindLyst has provider auto-detection | LysnrAI desktop/backend hardcodes `AzureOpenAI` | +| **Key Vault** | **Excellent** — graceful fallback to env vars | Already cloud-agnostic in practice | +| **Speech** | **None** — raw SDK usage | Deep Azure SDK coupling in 3 files | +| **Auth (JWT)** | **Excellent** — uses `jose` library | No cloud dependency | +| **Push notifications** | **Good** — platform-service abstraction | Swap provider client only | + +### What's Missing: Repository Interface Pattern + +The biggest gap is that repository files directly use `@azure/cosmos` types and SQL query syntax. To make the DB layer swappable, you'd need: + +```typescript +// Proposed: packages/cosmos/src/repository.ts +export interface DocumentRepository { + findById(id: string, partitionKey: string): Promise; + findMany(filter: Record, opts?: QueryOptions): Promise; + create(doc: T): Promise; + replace(id: string, doc: T, partitionKey: string): Promise; + upsert(doc: T): Promise; + delete(id: string, partitionKey: string): Promise; + count(filter: Record): Promise; +} +``` + +This would allow swapping Cosmos → MongoDB → PostgreSQL behind the interface without touching 56+ repository files. + +**Effort to add:** 1–2 weeks. This is the **highest-ROI investment** regardless of migration decision. + +--- + +## 9. Risk Analysis + +### 9.1 Risks of Staying on Azure + +| Risk | Likelihood | Impact | Mitigation | +|------|-----------|--------|-----------| +| Azure pricing increases | Low | Medium | Add abstraction layer for future portability | +| Azure outage | Low | High | Multi-region already possible (Cosmos global distribution) | +| Feature stagnation | Very Low | Low | Azure is investing heavily in AI services | +| Vendor lock-in deepens over time | Medium | Medium | Add abstraction layers proactively | + +### 9.2 Risks of Migrating + +| Risk | Likelihood | Impact | Mitigation | +|------|-----------|--------|-----------| +| Data loss during migration | Low | Critical | Test migration on staging first, keep Azure as backup | +| Query performance differences | Medium | Medium | Benchmark before committing | +| Feature gaps in new provider | Medium | Medium | Prototype critical features first | +| Wasted engineering time | Medium | High | Only migrate if there's a clear business driver | +| Regression bugs in 56+ repository files | High | Medium | Comprehensive test suite (1,029 tests) catches most issues | +| Speech quality degradation | Medium | High | A/B test both providers before committing | + +### 9.3 Azure-Specific Lock-in Risks (ranked) + +| # | Component | Lock-in Level | Escape Hatch | +|---|-----------|--------------|-------------| +| 1 | **Cosmos DB SQL API** | High | Rewrite queries to MongoDB MQL or add repository interface | +| 2 | **Azure Speech SDK (streaming)** | High | Google STT has comparable streaming API | +| 3 | **Azure Identity (DefaultAzureCredential)** | Medium | Only used by Key Vault, which is already optional | +| 4 | **Blob Storage SAS tokens** | Low | Pre-signed URLs are equivalent across all providers | +| 5 | **Azure OpenAI** | Very Low | OpenAI SDK works with both — 1-line config change | +| 6 | **Key Vault** | Very Low | Already has env var fallback | +| 7 | **Notification Hubs** | Very Low | Not deeply integrated yet | +| 8 | **Application Insights** | None | Custom telemetry already built | + +--- + +## 10. Recommendations + +### Recommended Strategy: **Stay on Azure + Invest in Abstraction** (Scenario D) + +This is the highest-scoring approach. Here's the prioritized action plan: + +#### Phase 1: Add Repository Interface (1–2 weeks) +- Create `DocumentRepository` interface in `@bytelyst/cosmos` +- Implement `CosmosDocumentRepository` that wraps current `@azure/cosmos` calls +- Gradually migrate the 56 repository files to use the interface +- This makes future DB migration a matter of implementing `MongoDocumentRepository` — no application code changes needed + +#### Phase 2: Normalize LLM Abstraction (2–3 days) +- Move LysnrAI desktop/backend from `AzureOpenAI` → auto-detecting provider pattern (like MindLyst web already does) +- Support `OPENAI_PROVIDER=azure|openai|gemini` across all repos +- This makes LLM provider swappable via config + +#### Phase 3: Speech Abstraction Layer (1 week, optional) +- Create `SpeechTranscriber` protocol/interface +- Implement `AzureSpeechTranscriber` (current code, extracted) +- Prepare `GoogleSpeechTranscriber` stub for future use +- This is lower priority since Azure Speech F0 tier is free + +#### Phase 4: Document Decision Criteria for Future Migration +- Define triggers that would justify migration (e.g., cost > $X/month, Azure outage > Y hours, need for feature Z) +- Review annually + +### Why NOT Migrate Now + +1. **Cost is negligible** — ~$10–20/month doesn't justify weeks of engineering +2. **No business driver** — Azure isn't blocking any feature development +3. **Risk/reward is unfavorable** — 4–8 weeks of migration work for ~$0 cost savings +4. **Test coverage is good but not perfect** — 1,029 tests cover most paths, but query-level changes in 56 files still risk regressions +5. **Azure free tiers are generous** — Speech F0, Notification Hubs Free, App Insights free tier + +### When Migration WOULD Make Sense + +- **Cosmos DB costs exceed $100/month** → Consider MongoDB Atlas Serverless +- **Azure Speech quality is insufficient** → Evaluate Google STT or Deepgram +- **Enterprise customer requires specific cloud** → Build the repository interface, then implement their cloud backend +- **Azure has extended outage affecting your region** → Multi-region or multi-cloud +- **You want to go fully open-source** → PostgreSQL (Supabase) + Whisper + MinIO (significant rewrite) + +--- + +## 11. Migration Playbook (If Chosen) + +If you decide to migrate in the future, here's the execution order (shortest critical path): + +### Week 1–2: Database Abstraction +1. Create `DocumentRepository` interface +2. Implement `CosmosDocumentRepository` (wraps current code) +3. Migrate all 56 repository files to use interface +4. Verify all 1,029 tests pass + +### Week 3–4: Database Migration (Cosmos → MongoDB) +1. Implement `MongoDocumentRepository` +2. Set up MongoDB Atlas Serverless cluster +3. Write data migration script (Cosmos → MongoDB) +4. Run migration on staging, verify data integrity +5. Switch repository implementation via config flag +6. Run full test suite against MongoDB + +### Week 5: Storage + Secrets +1. Swap `@bytelyst/blob` internals to S3-compatible client +2. Migrate blobs (azcopy → aws s3 sync or similar) +3. Replace Key Vault with new secrets manager (or just env vars) +4. Update all environment variable names + +### Week 6: LLM + Speech (if needed) +1. Switch OpenAI from Azure endpoint to direct (config change only) +2. If migrating Speech: rewrite `azure_stt.py` and Swift `AzureSpeechTranscriber` +3. A/B test new speech provider against Azure + +### Week 7–8: Cleanup + Verification +1. Remove all `@azure/*` npm packages +2. Remove all `azure-*` pip packages +3. Update Docker configs, CI/CD +4. Update documentation +5. Monitor production for 2 weeks + +--- + +## Appendix A: File-Level Azure Dependency Map + +### TypeScript — `@azure/cosmos` (CRITICAL) + +| File | Repo | Direct Import | +|------|------|---------------| +| `packages/cosmos/src/client.ts` | common-plat | `@azure/cosmos` | +| `packages/cosmos/src/containers.ts` | common-plat | `@azure/cosmos` | +| `services/platform-service/src/modules/*/repository.ts` (56 files) | common-plat | Via `@bytelyst/cosmos` | +| `services/extraction-service/src/modules/*/repository.ts` (2 files) | common-plat | Via `@bytelyst/cosmos` | +| `dashboards/admin-web/src/lib/cosmos.ts` | common-plat | `@azure/cosmos` | +| `dashboards/admin-web/src/lib/repositories/*.ts` (4 files) | common-plat | Via cosmos.ts | +| `mindlyst-native/web/src/lib/cosmos.ts` | MindLyst | `@azure/cosmos` | + +### TypeScript — `@azure/storage-blob` + +| File | Repo | Direct Import | +|------|------|---------------| +| `packages/blob/src/blob.ts` | common-plat | `@azure/storage-blob` | + +### TypeScript — `@azure/identity` + `@azure/keyvault-secrets` + +| File | Repo | Direct Import | +|------|------|---------------| +| `packages/config/src/keyvault.ts` | common-plat | Dynamic import (both) | +| `dashboards/admin-web/src/app/api/ops/secrets/route.ts` | common-plat | Both (Secrets Manager UI) | + +### Python — Azure SDKs + +| File | Repo | SDK | +|------|------|-----| +| `src/audio/azure_stt.py` | LysnrAI | `azure.cognitiveservices.speech` | +| `src/cloud/cosmos_client.py` | LysnrAI | `azure.cosmos` | +| `src/cloud/blob_client.py` | LysnrAI | `azure.storage.blob` | +| `src/secrets/keyvault.py` | LysnrAI | `azure.identity`, `azure.keyvault.secrets` | +| `backend/src/secrets/keyvault.py` | LysnrAI | `azure.identity`, `azure.keyvault.secrets` | +| `backend/src/cloud/cosmos.py` | LysnrAI | `azure.cosmos` | +| `src/llm/text_cleaner.py` | LysnrAI | `openai.AzureOpenAI` | +| `backend/src/clients/openai_client.py` | LysnrAI | `openai.AsyncAzureOpenAI` | + +### Swift — Azure Speech SDK + +| File | Repo | SDK | +|------|------|-----| +| `iosApp/Services/AzureSpeechTranscriber.swift` | MindLyst | `MicrosoftCognitiveServicesSpeech` | +| `LysnrAI/LysnrKeyboard/KeyboardViewController.swift` | LysnrAI | SPX framework (via CocoaPods) | + +--- + +## Appendix B: SDK & Package Inventory + +### npm packages (TypeScript) + +| Package | Version | Used By | Swappable | +|---------|---------|---------|-----------| +| `@azure/cosmos` | ≥4.0.0 | `@bytelyst/cosmos`, admin-web, MindLyst web | Medium (query rewrite) | +| `@azure/storage-blob` | ≥12.0.0 | `@bytelyst/blob` | Easy (S3 compat) | +| `@azure/identity` | latest | `@bytelyst/config`, admin-web secrets | Easy (remove) | +| `@azure/keyvault-secrets` | latest | `@bytelyst/config`, admin-web secrets | Easy (remove) | + +### pip packages (Python) + +| Package | Version | Used By | Swappable | +|---------|---------|---------|-----------| +| `azure-cognitiveservices-speech` | ≥1.42.0 | Desktop STT | Hard (deep SDK integration) | +| `azure-cosmos` | latest | Desktop + backend Cosmos client | Medium (pymongo swap) | +| `azure-storage-blob` | ≥12.24.0 | Desktop blob client | Easy (boto3 swap) | +| `azure-identity` | ≥1.19.0 | Key Vault auth | Easy (remove) | +| `azure-keyvault-secrets` | ≥4.9.0 | Secrets resolver | Easy (remove) | +| `openai` | ≥1.60.0 | `AzureOpenAI` / `AsyncAzureOpenAI` | Trivial (change class name) | +| `opencensus-ext-azure` | ≥1.1.0 | Optional telemetry | Trivial (remove) | + +### Swift packages / CocoaPods + +| Package | Used By | Swappable | +|---------|---------|-----------| +| `MicrosoftCognitiveServicesSpeech` (SPX) | LysnrAI iOS, MindLyst iOS | Hard (need alternative streaming STT) | + +--- + +*Document generated by automated codebase analysis. Numbers are accurate as of 2026-03-01. Update as the codebase evolves.* diff --git a/docs/WINDSURF/AZURE_CONNECTION_AUDIT.md b/docs/WINDSURF/AZURE_CONNECTION_AUDIT.md new file mode 100644 index 00000000..a8cb24f7 --- /dev/null +++ b/docs/WINDSURF/AZURE_CONNECTION_AUDIT.md @@ -0,0 +1,181 @@ +# Azure Connection Audit — Full Workspace Report + +> **Date:** 2026-02-22 +> **Scope:** `learning_ai_common_plat`, `learning_voice_ai_agent`, `learning_multimodal_memory_agents`, `learning_ai_clock`, `learning_ai_fastgap` +> **Auditor:** Cascade (AI) + +--- + +## Executive Summary + +| Category | Issues Found | Fixed (session 1) | Fixed (session 2) | Remaining | +|----------|-------------|-------------------|-------------------|-----------| +| `x-request-id` missing | 12 clients | 2 (MindLyst) | **9** (root cause + feature-flags) | 0 ✅ | +| `x-product-id` missing | 6 clients | 0 | **6** (admin + user dashboards + Python) | 0 ✅ | +| Cosmos PK mismatch | 1 container | 0 (flagged) | 0 | 1 (needs migration) | +| `.env.example` gaps | 4 files | 1 (MindLyst) | **3** (ChronoMind, user-dash, admin-dash) | 0 ✅ | +| Hardcoded productId | 2 instances | 0 | **2** (telemetry.ts, platform_client.py) | 0 ✅ | +| Python client gaps | 1 file | 0 | **1** (headers + config) | 0 ✅ | + +--- + +## 1. `x-request-id` Header — Root Cause + +### Finding + +**`@bytelyst/api-client` does NOT auto-inject `x-request-id`.** + +The `createApiClient()` factory in `packages/api-client/src/client.ts` only sets `Content-Type`, auth token (via `getToken`), and caller-supplied `defaultHeaders`. No `x-request-id` is generated. This means **every consumer** that relies on `@bytelyst/api-client` without explicitly adding the header is missing request tracing. + +### Root Cause Fix + +Add `x-request-id: crypto.randomUUID()` to `buildHeaders()` in `packages/api-client/src/client.ts`. This single change propagates to all consumers automatically. + +### Affected Clients (missing `x-request-id`) + +| Repo | File | Client Pattern | +|------|------|---------------| +| `common_plat` | `dashboards/admin-web/src/lib/billing-client.ts` | `createApiClient` — no `x-request-id` | +| `common_plat` | `dashboards/admin-web/src/lib/growth-client.ts` | `createApiClient` — no `x-request-id` | +| `common_plat` | `dashboards/admin-web/src/lib/platform-client.ts` | `createApiClient` — no `x-request-id` | +| `common_plat` | `dashboards/tracker-web/src/lib/tracker-client.ts` | `createApiClient` — no `x-request-id` | +| `common_plat` | `packages/extraction/src/client.ts` | `createApiClient` — no `x-request-id` | +| `voice_ai_agent` | `user-dashboard-web/src/lib/billing-client.ts` | `createApiClient` — no `x-request-id` | +| `voice_ai_agent` | `user-dashboard-web/src/lib/growth-client.ts` | `createApiClient` — no `x-request-id` | +| `voice_ai_agent` | `user-dashboard-web/src/lib/platform-client.ts` | `createApiClient` — no `x-request-id` | +| `voice_ai_agent` | `user-dashboard-web/src/lib/feature-flags.ts` | Custom `fetch` — no `x-request-id` | +| `voice_ai_agent` | `backend/src/clients/platform_client.py` | `httpx` — no `x-request-id` | + +### Already Fixed (previous session) + +| Repo | File | Status | +|------|------|--------| +| `multimodal_memory` | `web/src/lib/billing-client.ts` | ✅ Added via `defaultHeaders` | +| `multimodal_memory` | `web/src/lib/feature-flags.ts` | ✅ Added manually | + +### Already Correct + +| Repo | File | Status | +|------|------|--------| +| `ai_fastgap` (NomGap) | `src/api/client.ts` | ✅ Custom client with `crypto.randomUUID()` | +| `ai_clock` (ChronoMind) | `web/src/lib/platform-sync.ts` | ✅ Custom client with `crypto.randomUUID()` | +| `voice_ai_agent` | `backend/src/main.py` | ✅ Middleware propagates/generates | +| `voice_ai_agent` | `backend/src/clients/extraction_client.py` | ✅ Passes `request_id` param | + +--- + +## 2. `x-product-id` Header Gaps + +### Clients Missing `x-product-id` + +| Repo | File | Impact | +|------|------|--------| +| `common_plat` | `admin-web/src/lib/billing-client.ts` | Server can't filter by product | +| `common_plat` | `admin-web/src/lib/growth-client.ts` | Server can't filter by product | +| `voice_ai_agent` | `user-dashboard-web/src/lib/billing-client.ts` | Server can't filter by product | +| `voice_ai_agent` | `user-dashboard-web/src/lib/growth-client.ts` | Server can't filter by product | +| `voice_ai_agent` | `user-dashboard-web/src/lib/platform-client.ts` | Passes in body, not header | +| `voice_ai_agent` | `backend/src/clients/platform_client.py` | Passes in body/params, not header | + +### Already Correct + +| Repo | File | +|------|------| +| `ai_fastgap` (NomGap) | `src/api/client.ts` — `x-product-id: API_CONFIG.productId` | +| `ai_clock` (ChronoMind) | `web/src/lib/platform-sync.ts` — `x-product-id` header | +| `multimodal_memory` (MindLyst) | `web/src/lib/billing-client.ts` — via `defaultHeaders` | +| `multimodal_memory` (MindLyst) | `web/src/lib/feature-flags.ts` — explicit header | +| `common_plat` | `tracker-web/src/lib/tracker-client.ts` — from `localStorage` | + +--- + +## 3. Cosmos DB Partition Key Mismatch + +### `referrals` Container — 3-way Mismatch + +| Location | Partition Key | +|----------|--------------| +| `platform-service/src/lib/cosmos-init.ts` | `/id` | +| MindLyst `web/src/lib/cosmos.ts` | `/userId` | +| Admin dashboard `admin-web/src/lib/cosmos.ts` | `/referrerId` | +| User dashboard `user-dashboard-web/src/lib/cosmos.ts` | `/referrerId` | + +**Status:** Flagged in previous session. Cannot be fixed without data migration. Comment added to `cosmos-init.ts`. + +**Risk:** Cross-partition queries will silently succeed but may return incomplete results or fail on point reads if the wrong partition key is specified. + +--- + +## 4. Missing Environment Variables in `.env.example` Files + +### ChronoMind `web/.env.example` + +Currently only has: +``` +NEXT_PUBLIC_PLATFORM_SERVICE_URL=http://localhost:4003/api +``` + +**Missing:** +- `NEXT_PUBLIC_PRODUCT_ID=chronomind` — used implicitly by `platform-sync.ts` (hardcoded there, but should be env-driven for consistency) + +### LysnrAI `user-dashboard-web/.env.example` + +**Missing:** +- `NEXT_PUBLIC_PRODUCT_ID=lysnrai` — referenced by `feature-flags.ts` line 10 +- `NEXT_PUBLIC_PLATFORM_SERVICE_URL=http://localhost:4003` — referenced by `feature-flags.ts` line 11 + +Has `PLATFORM_SERVICE_URL` (server-side) but not the `NEXT_PUBLIC_` variant (client-side). + +### LysnrAI root `.env.example` + +**Missing:** +- `NEXT_PUBLIC_PRODUCT_ID` — not needed at root level (desktop app), so this is informational only. + +### Admin dashboard `.env.example` + +**Missing:** +- `AZURE_KEYVAULT_URL` — referenced by `instrumentation.ts` but not in `.env.example` + +--- + +## 5. Hardcoded `productId` Values + +| Repo | File | Line | Value | Should Use | +|------|------|------|-------|-----------| +| `multimodal_memory` | `web/src/lib/telemetry.ts` | 19 | `productId: 'mindlyst'` | `process.env.NEXT_PUBLIC_PRODUCT_ID` | +| `voice_ai_agent` | `backend/src/clients/platform_client.py` | 86, 101 | `product_id: str = "lysnrai"` | `settings.PRODUCT_ID` or config | + +--- + +## 6. Python Backend Client Gaps (`platform_client.py`) + +The `PlatformClient` class in `backend/src/clients/platform_client.py` has several issues: + +1. **No `x-request-id` header** on any request +2. **No `x-product-id` header** on any request +3. **Creates new `httpx.AsyncClient` per request** — no connection pooling +4. **Hardcoded `product_id="lysnrai"` defaults** — should use config + +--- + +## 7. Previously Fixed (Session 1) + +| Fix | Repo | File | +|-----|------|------| +| Added `x-request-id` to billing client | `multimodal_memory` | `web/src/lib/billing-client.ts` | +| Added `x-request-id` to feature flags | `multimodal_memory` | `web/src/lib/feature-flags.ts` | +| Added 13 MindLyst containers to cosmos-init | `common_plat` | `services/platform-service/src/lib/cosmos-init.ts` | +| Added Blob Storage creds to Python config | `voice_ai_agent` | `backend/src/config.py` | +| Added missing env vars to MindLyst | `multimodal_memory` | `web/.env.example` | + +--- + +## 8. Recommended Fix Order + +1. **P0 — Root cause:** Add `x-request-id` auto-generation to `@bytelyst/api-client` `buildHeaders()` → fixes 9 TS clients at once +2. **P0 — LysnrAI feature-flags:** Add `x-request-id` to the custom `fetch` call in `user-dashboard-web/src/lib/feature-flags.ts` +3. **P1 — Python backend:** Add `x-request-id` and `x-product-id` headers to `platform_client.py` +4. **P1 — Env vars:** Add missing `NEXT_PUBLIC_*` vars to ChronoMind, LysnrAI user-dashboard, admin-dashboard `.env.example` files +5. **P2 — `x-product-id`:** Add to admin/user dashboard clients via `defaultHeaders` in `createApiClient` config +6. **P2 — Hardcoded productId:** Replace in `telemetry.ts` and `platform_client.py` +7. **P3 — Referrals PK mismatch:** Requires data migration strategy (separate task) diff --git a/packages/api-client/src/client.ts b/packages/api-client/src/client.ts index 8c1d3044..d55141e5 100644 --- a/packages/api-client/src/client.ts +++ b/packages/api-client/src/client.ts @@ -42,6 +42,9 @@ export function createApiClient(config: ApiClientConfig): ApiClient { function buildHeaders(options?: RequestInit): HeadersInit { const headers: Record = { 'Content-Type': 'application/json', + 'x-request-id': typeof globalThis.crypto?.randomUUID === 'function' + ? globalThis.crypto.randomUUID() + : `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`, ...defaultHeaders, }; diff --git a/services/platform-service/src/lib/cosmos-init.ts b/services/platform-service/src/lib/cosmos-init.ts index 180c0657..8a0bd63e 100644 --- a/services/platform-service/src/lib/cosmos-init.ts +++ b/services/platform-service/src/lib/cosmos-init.ts @@ -96,6 +96,10 @@ const CONTAINER_DEFS: Record = { changelog: { partitionKeyPath: '/productId' }, // Push notification triggers (NomGap) push_triggers: { partitionKeyPath: '/productId', defaultTtl: 30 * 86400 }, + // JarvisJr modules (agents, sessions, memory) + jarvis_agents: { partitionKeyPath: '/userId' }, + jarvis_sessions: { partitionKeyPath: '/userId' }, + jarvis_memory: { partitionKeyPath: '/agentId' }, }; export async function initCosmosIfNeeded(): Promise { diff --git a/services/platform-service/src/modules/jarvis-agents/jarvis-agents.test.ts b/services/platform-service/src/modules/jarvis-agents/jarvis-agents.test.ts new file mode 100644 index 00000000..524d4212 --- /dev/null +++ b/services/platform-service/src/modules/jarvis-agents/jarvis-agents.test.ts @@ -0,0 +1,223 @@ +/** + * JarvisJr agents module unit tests — validates schema parsing, constants, and type guards. + */ + +import { describe, it, expect } from 'vitest'; +import { + CreateAgentSchema, + UpdateAgentSchema, + ListAgentsQuerySchema, + DIFFICULTY_LEVELS, + PRIVACY_LEVELS, + COACHING_FRAMEWORKS, +} from './types.js'; + +// ── Constants ── + +describe('JarvisJr agent constants', () => { + it('has expected difficulty levels', () => { + expect(DIFFICULTY_LEVELS).toEqual(['beginner', 'intermediate', 'advanced', 'adaptive']); + }); + + it('has expected privacy levels', () => { + expect(PRIVACY_LEVELS).toEqual(['standard', 'local_only']); + }); + + it('has expected coaching frameworks', () => { + expect(COACHING_FRAMEWORKS).toContain('socratic'); + expect(COACHING_FRAMEWORKS).toContain('star'); + expect(COACHING_FRAMEWORKS).toContain('scamper'); + expect(COACHING_FRAMEWORKS).toContain('immersive'); + expect(COACHING_FRAMEWORKS).toContain('freeform'); + expect(COACHING_FRAMEWORKS.length).toBe(7); + }); +}); + +// ── CreateAgentSchema ── + +describe('CreateAgentSchema', () => { + const validMinimal = { + name: 'Coach', + role: 'Communication coach', + systemPrompt: 'You are a communication coach who uses the Socratic method.', + voiceId: 'alloy', + }; + + it('accepts minimal valid input with defaults', () => { + const result = CreateAgentSchema.safeParse(validMinimal); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.name).toBe('Coach'); + expect(result.data.coachingFramework).toBe('freeform'); + expect(result.data.accentColor).toBe('#7C6BFF'); + expect(result.data.sessionLength).toBe(15); + expect(result.data.difficultyLevel).toBe('adaptive'); + expect(result.data.language).toBe('en'); + expect(result.data.privacyLevel).toBe('standard'); + expect(result.data.checkInSchedule).toBeNull(); + expect(result.data.isTemplate).toBe(false); + expect(result.data.templateSource).toBeNull(); + } + }); + + it('accepts full input with all fields', () => { + const result = CreateAgentSchema.safeParse({ + ...validMinimal, + coachingFramework: 'socratic', + accentColor: '#5A8CFF', + welcomeMessage: 'Ready to practice?', + sessionLength: 30, + difficultyLevel: 'intermediate', + language: 'es', + privacyLevel: 'local_only', + checkInSchedule: '0 9 * * 1-5', + isTemplate: true, + templateSource: 'agent_abc123', + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.coachingFramework).toBe('socratic'); + expect(result.data.sessionLength).toBe(30); + expect(result.data.language).toBe('es'); + expect(result.data.checkInSchedule).toBe('0 9 * * 1-5'); + } + }); + + it('rejects missing name', () => { + const result = CreateAgentSchema.safeParse({ + role: 'Coach', + systemPrompt: 'You are a coach.', + voiceId: 'alloy', + }); + expect(result.success).toBe(false); + }); + + it('rejects missing role', () => { + const result = CreateAgentSchema.safeParse({ + name: 'Coach', + systemPrompt: 'You are a coach.', + voiceId: 'alloy', + }); + expect(result.success).toBe(false); + }); + + it('rejects missing systemPrompt', () => { + const result = CreateAgentSchema.safeParse({ + name: 'Coach', + role: 'Coach', + voiceId: 'alloy', + }); + expect(result.success).toBe(false); + }); + + it('rejects missing voiceId', () => { + const result = CreateAgentSchema.safeParse({ + name: 'Coach', + role: 'Coach', + systemPrompt: 'You are a coach.', + }); + expect(result.success).toBe(false); + }); + + it('rejects invalid accent color format', () => { + const result = CreateAgentSchema.safeParse({ + ...validMinimal, + accentColor: 'red', + }); + expect(result.success).toBe(false); + }); + + it('rejects name exceeding max length', () => { + const result = CreateAgentSchema.safeParse({ + ...validMinimal, + name: 'A'.repeat(101), + }); + expect(result.success).toBe(false); + }); + + it('rejects sessionLength out of range', () => { + expect(CreateAgentSchema.safeParse({ ...validMinimal, sessionLength: 0 }).success).toBe(false); + expect(CreateAgentSchema.safeParse({ ...validMinimal, sessionLength: 121 }).success).toBe( + false, + ); + }); + + it('rejects invalid difficulty level', () => { + const result = CreateAgentSchema.safeParse({ + ...validMinimal, + difficultyLevel: 'expert', + }); + expect(result.success).toBe(false); + }); + + it('rejects invalid coaching framework', () => { + const result = CreateAgentSchema.safeParse({ + ...validMinimal, + coachingFramework: 'unknown', + }); + expect(result.success).toBe(false); + }); +}); + +// ── UpdateAgentSchema ── + +describe('UpdateAgentSchema', () => { + it('accepts partial update with single field', () => { + const result = UpdateAgentSchema.safeParse({ name: 'Updated Coach' }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.name).toBe('Updated Coach'); + expect(result.data.role).toBeUndefined(); + } + }); + + it('accepts empty update (no fields)', () => { + const result = UpdateAgentSchema.safeParse({}); + expect(result.success).toBe(true); + }); + + it('rejects invalid accent color in update', () => { + const result = UpdateAgentSchema.safeParse({ accentColor: 'not-a-color' }); + expect(result.success).toBe(false); + }); + + it('accepts nullable checkInSchedule', () => { + const result = UpdateAgentSchema.safeParse({ checkInSchedule: null }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.checkInSchedule).toBeNull(); + } + }); +}); + +// ── ListAgentsQuerySchema ── + +describe('ListAgentsQuerySchema', () => { + it('applies defaults for empty query', () => { + const result = ListAgentsQuerySchema.safeParse({}); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.limit).toBe(50); + expect(result.data.offset).toBe(0); + } + }); + + it('accepts custom limit and offset', () => { + const result = ListAgentsQuerySchema.safeParse({ limit: '10', offset: '5' }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.limit).toBe(10); + expect(result.data.offset).toBe(5); + } + }); + + it('rejects limit exceeding max', () => { + const result = ListAgentsQuerySchema.safeParse({ limit: 101 }); + expect(result.success).toBe(false); + }); + + it('rejects negative offset', () => { + const result = ListAgentsQuerySchema.safeParse({ offset: -1 }); + expect(result.success).toBe(false); + }); +}); diff --git a/services/platform-service/src/modules/jarvis-agents/repository.ts b/services/platform-service/src/modules/jarvis-agents/repository.ts new file mode 100644 index 00000000..8381a8d3 --- /dev/null +++ b/services/platform-service/src/modules/jarvis-agents/repository.ts @@ -0,0 +1,90 @@ +/** + * JarvisJr agents repository — Cosmos DB CRUD. + * Container: jarvis_agents, partition key: /userId + */ + +import { getContainer } from '../../lib/cosmos.js'; +import type { JarvisAgentDoc, ListAgentsQuery } from './types.js'; + +function container() { + return getContainer('jarvis_agents'); +} + +export async function listByUser( + userId: string, + query: ListAgentsQuery, +): Promise<{ agents: JarvisAgentDoc[]; total: number }> { + const countResult = await container() + .items.query({ + query: 'SELECT VALUE COUNT(1) FROM c WHERE c.userId = @userId', + parameters: [{ name: '@userId', value: userId }], + }) + .fetchAll(); + const total = countResult.resources[0] ?? 0; + + const { resources } = await container() + .items.query({ + query: + 'SELECT * FROM c WHERE c.userId = @userId ORDER BY c.createdAt DESC OFFSET @offset LIMIT @limit', + parameters: [ + { name: '@userId', value: userId }, + { name: '@offset', value: query.offset }, + { name: '@limit', value: query.limit }, + ], + }) + .fetchAll(); + + return { agents: resources, total }; +} + +export async function getById(id: string, userId: string): Promise { + try { + const { resource } = await container().item(id, userId).read(); + return resource ?? null; + } catch { + return null; + } +} + +export async function create(doc: JarvisAgentDoc): Promise { + const { resource } = await container().items.create(doc); + return resource as JarvisAgentDoc; +} + +export async function update( + id: string, + userId: string, + updates: Partial, +): Promise { + try { + const { resource: existing } = await container().item(id, userId).read(); + if (!existing) return null; + const merged = { ...existing, ...updates, updatedAt: new Date().toISOString() }; + const { resource } = await container().item(id, userId).replace(merged); + return resource as JarvisAgentDoc; + } catch { + return null; + } +} + +export async function remove(id: string, userId: string): Promise { + try { + await container().item(id, userId).delete(); + return true; + } catch { + return false; + } +} + +export async function incrementSessionCount( + id: string, + userId: string, +): Promise { + const agent = await getById(id, userId); + if (agent) { + await update(id, userId, { + totalSessions: agent.totalSessions + 1, + lastSessionAt: new Date().toISOString(), + }); + } +} diff --git a/services/platform-service/src/modules/jarvis-agents/routes.ts b/services/platform-service/src/modules/jarvis-agents/routes.ts new file mode 100644 index 00000000..43abfda5 --- /dev/null +++ b/services/platform-service/src/modules/jarvis-agents/routes.ts @@ -0,0 +1,131 @@ +/** + * JarvisJr agent REST endpoints. + * + * GET /jarvis/agents — list user's agents + * POST /jarvis/agents — create agent + * GET /jarvis/agents/:id — get single agent + * PUT /jarvis/agents/:id — update agent + * DELETE /jarvis/agents/:id — delete agent + * POST /jarvis/agents/:id/duplicate — duplicate agent + */ + +import type { FastifyInstance } from 'fastify'; +import crypto from 'node:crypto'; +import { BadRequestError, NotFoundError } from '../../lib/errors.js'; +import { extractAuth } from '../../lib/auth.js'; +import * as repo from './repository.js'; +import { + CreateAgentSchema, + UpdateAgentSchema, + ListAgentsQuerySchema, + type JarvisAgentDoc, +} from './types.js'; + +const PRODUCT_ID = 'jarvisjr'; + +export async function jarvisAgentRoutes(app: FastifyInstance) { + // List agents + app.get('/jarvis/agents', async req => { + const auth = await extractAuth(req); + const parsed = ListAgentsQuerySchema.safeParse(req.query); + if (!parsed.success) { + throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; ')); + } + const { agents, total } = await repo.listByUser(auth.sub, parsed.data); + return { agents, total, limit: parsed.data.limit, offset: parsed.data.offset }; + }); + + // Get agent + app.get('/jarvis/agents/:id', async req => { + const auth = await extractAuth(req); + const { id } = req.params as { id: string }; + const agent = await repo.getById(id, auth.sub); + if (!agent) throw new NotFoundError('Agent not found'); + return agent; + }); + + // Create agent + app.post('/jarvis/agents', async (req, reply) => { + const auth = await extractAuth(req); + const parsed = CreateAgentSchema.safeParse(req.body); + if (!parsed.success) { + throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; ')); + } + const input = parsed.data; + const now = new Date().toISOString(); + + const doc: JarvisAgentDoc = { + id: `agent_${crypto.randomUUID()}`, + userId: auth.sub, + productId: PRODUCT_ID, + name: input.name, + role: input.role, + systemPrompt: input.systemPrompt, + voiceId: input.voiceId, + coachingFramework: input.coachingFramework, + accentColor: input.accentColor, + welcomeMessage: input.welcomeMessage, + sessionLength: input.sessionLength, + difficultyLevel: input.difficultyLevel, + language: input.language, + privacyLevel: input.privacyLevel, + checkInSchedule: input.checkInSchedule, + isTemplate: input.isTemplate, + templateSource: input.templateSource, + totalSessions: 0, + lastSessionAt: null, + createdAt: now, + updatedAt: now, + }; + + const created = await repo.create(doc); + reply.code(201); + return created; + }); + + // Update agent + app.put('/jarvis/agents/:id', async req => { + const auth = await extractAuth(req); + const { id } = req.params as { id: string }; + const parsed = UpdateAgentSchema.safeParse(req.body); + if (!parsed.success) { + throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; ')); + } + const updated = await repo.update(id, auth.sub, parsed.data); + if (!updated) throw new NotFoundError('Agent not found'); + return updated; + }); + + // Delete agent + app.delete('/jarvis/agents/:id', async (req, reply) => { + const auth = await extractAuth(req); + const { id } = req.params as { id: string }; + const deleted = await repo.remove(id, auth.sub); + if (!deleted) throw new NotFoundError('Agent not found'); + reply.code(204); + }); + + // Duplicate agent + app.post('/jarvis/agents/:id/duplicate', async (req, reply) => { + const auth = await extractAuth(req); + const { id } = req.params as { id: string }; + const source = await repo.getById(id, auth.sub); + if (!source) throw new NotFoundError('Agent not found'); + + const now = new Date().toISOString(); + const doc: JarvisAgentDoc = { + ...source, + id: `agent_${crypto.randomUUID()}`, + name: `${source.name} (Copy)`, + templateSource: source.id, + totalSessions: 0, + lastSessionAt: null, + createdAt: now, + updatedAt: now, + }; + + const created = await repo.create(doc); + reply.code(201); + return created; + }); +} diff --git a/services/platform-service/src/modules/jarvis-agents/types.ts b/services/platform-service/src/modules/jarvis-agents/types.ts new file mode 100644 index 00000000..bb1d0724 --- /dev/null +++ b/services/platform-service/src/modules/jarvis-agents/types.ts @@ -0,0 +1,88 @@ +/** + * JarvisJr agent configuration types. + * Each agent is a persistent coaching persona with voice, personality, and memory. + * Partition key: /userId + */ + +import { z } from 'zod'; + +export const DIFFICULTY_LEVELS = ['beginner', 'intermediate', 'advanced', 'adaptive'] as const; +export const PRIVACY_LEVELS = ['standard', 'local_only'] as const; +export const COACHING_FRAMEWORKS = [ + 'socratic', + 'star', + 'scamper', + 'immersive', + 'cognitive_reframing', + 'structured_feedback', + 'freeform', +] as const; + +export type DifficultyLevel = (typeof DIFFICULTY_LEVELS)[number]; +export type PrivacyLevel = (typeof PRIVACY_LEVELS)[number]; +export type CoachingFramework = (typeof COACHING_FRAMEWORKS)[number]; + +export interface JarvisAgentDoc { + id: string; + userId: string; + productId: string; + name: string; + role: string; + systemPrompt: string; + voiceId: string; + coachingFramework: CoachingFramework; + accentColor: string; + welcomeMessage: string; + sessionLength: number; + difficultyLevel: DifficultyLevel; + language: string; + privacyLevel: PrivacyLevel; + checkInSchedule: string | null; + isTemplate: boolean; + templateSource: string | null; + totalSessions: number; + lastSessionAt: string | null; + createdAt: string; + updatedAt: string; +} + +export const CreateAgentSchema = z.object({ + name: z.string().min(1).max(100), + role: z.string().min(1).max(200), + systemPrompt: z.string().min(1).max(10000), + voiceId: z.string().min(1).max(100), + coachingFramework: z.enum(COACHING_FRAMEWORKS).default('freeform'), + accentColor: z.string().regex(/^#[0-9A-Fa-f]{6}$/).default('#7C6BFF'), + welcomeMessage: z.string().max(500).default(''), + sessionLength: z.number().min(1).max(120).default(15), + difficultyLevel: z.enum(DIFFICULTY_LEVELS).default('adaptive'), + language: z.string().min(2).max(10).default('en'), + privacyLevel: z.enum(PRIVACY_LEVELS).default('standard'), + checkInSchedule: z.string().nullable().default(null), + isTemplate: z.boolean().default(false), + templateSource: z.string().nullable().default(null), +}); + +export const UpdateAgentSchema = z.object({ + name: z.string().min(1).max(100).optional(), + role: z.string().min(1).max(200).optional(), + systemPrompt: z.string().min(1).max(10000).optional(), + voiceId: z.string().min(1).max(100).optional(), + coachingFramework: z.enum(COACHING_FRAMEWORKS).optional(), + accentColor: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(), + welcomeMessage: z.string().max(500).optional(), + sessionLength: z.number().min(1).max(120).optional(), + difficultyLevel: z.enum(DIFFICULTY_LEVELS).optional(), + language: z.string().min(2).max(10).optional(), + privacyLevel: z.enum(PRIVACY_LEVELS).optional(), + checkInSchedule: z.string().nullable().optional(), +}); + +export const ListAgentsQuerySchema = z.object({ + limit: z.coerce.number().min(1).max(100).default(50), + offset: z.coerce.number().min(0).default(0), +}); + +export type CreateAgentInput = z.infer; +export type UpdateAgentInput = z.infer; +export type ListAgentsQuery = z.infer; diff --git a/services/platform-service/src/modules/jarvis-memory/jarvis-memory.test.ts b/services/platform-service/src/modules/jarvis-memory/jarvis-memory.test.ts new file mode 100644 index 00000000..0c63e1bf --- /dev/null +++ b/services/platform-service/src/modules/jarvis-memory/jarvis-memory.test.ts @@ -0,0 +1,207 @@ +/** + * JarvisJr agent memory module unit tests — validates schema parsing and constants. + */ + +import { describe, it, expect } from 'vitest'; +import { + CreateMemorySchema, + UpdateMemorySchema, + ListMemoryQuerySchema, + MEMORY_TYPES, +} from './types.js'; + +// ── Constants ── + +describe('JarvisJr memory constants', () => { + it('has expected memory types', () => { + expect(MEMORY_TYPES).toEqual(['skill_note', 'preference', 'goal', 'context', 'exercise']); + }); +}); + +// ── CreateMemorySchema ── + +describe('CreateMemorySchema', () => { + const validMinimal = { + agentId: 'agent_abc123', + sessionId: 'sess_xyz789', + type: 'skill_note', + content: 'User tends to use filler words like "um" and "uh" frequently.', + }; + + it('accepts minimal valid input with defaults', () => { + const result = CreateMemorySchema.safeParse(validMinimal); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.agentId).toBe('agent_abc123'); + expect(result.data.sessionId).toBe('sess_xyz789'); + expect(result.data.type).toBe('skill_note'); + expect(result.data.importance).toBe(0.5); + expect(result.data.tags).toEqual([]); + expect(result.data.expiresAt).toBeNull(); + } + }); + + it('accepts full input with all fields', () => { + const result = CreateMemorySchema.safeParse({ + ...validMinimal, + importance: 0.9, + tags: ['communication', 'filler_words'], + expiresAt: '2027-01-01T00:00:00Z', + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.importance).toBe(0.9); + expect(result.data.tags).toHaveLength(2); + expect(result.data.expiresAt).toBe('2027-01-01T00:00:00Z'); + } + }); + + it('accepts all memory types', () => { + for (const type of MEMORY_TYPES) { + const result = CreateMemorySchema.safeParse({ ...validMinimal, type }); + expect(result.success).toBe(true); + } + }); + + it('rejects missing agentId', () => { + const result = CreateMemorySchema.safeParse({ + sessionId: validMinimal.sessionId, + type: validMinimal.type, + content: validMinimal.content, + }); + expect(result.success).toBe(false); + }); + + it('rejects missing sessionId', () => { + const result = CreateMemorySchema.safeParse({ + agentId: validMinimal.agentId, + type: validMinimal.type, + content: validMinimal.content, + }); + expect(result.success).toBe(false); + }); + + it('rejects missing content', () => { + const result = CreateMemorySchema.safeParse({ + agentId: validMinimal.agentId, + sessionId: validMinimal.sessionId, + type: validMinimal.type, + }); + expect(result.success).toBe(false); + }); + + it('rejects empty content', () => { + const result = CreateMemorySchema.safeParse({ ...validMinimal, content: '' }); + expect(result.success).toBe(false); + }); + + it('rejects invalid memory type', () => { + const result = CreateMemorySchema.safeParse({ ...validMinimal, type: 'thought' }); + expect(result.success).toBe(false); + }); + + it('rejects importance out of range', () => { + expect( + CreateMemorySchema.safeParse({ ...validMinimal, importance: -0.1 }).success, + ).toBe(false); + expect( + CreateMemorySchema.safeParse({ ...validMinimal, importance: 1.1 }).success, + ).toBe(false); + }); + + it('rejects content exceeding max length', () => { + const result = CreateMemorySchema.safeParse({ + ...validMinimal, + content: 'A'.repeat(5001), + }); + expect(result.success).toBe(false); + }); +}); + +// ── UpdateMemorySchema ── + +describe('UpdateMemorySchema', () => { + it('accepts partial update with single field', () => { + const result = UpdateMemorySchema.safeParse({ importance: 0.8 }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.importance).toBe(0.8); + expect(result.data.content).toBeUndefined(); + } + }); + + it('accepts empty update', () => { + const result = UpdateMemorySchema.safeParse({}); + expect(result.success).toBe(true); + }); + + it('accepts tags update', () => { + const result = UpdateMemorySchema.safeParse({ tags: ['updated', 'tag'] }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.tags).toEqual(['updated', 'tag']); + } + }); + + it('accepts nullable expiresAt', () => { + const result = UpdateMemorySchema.safeParse({ expiresAt: null }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.expiresAt).toBeNull(); + } + }); + + it('rejects empty content string', () => { + const result = UpdateMemorySchema.safeParse({ content: '' }); + expect(result.success).toBe(false); + }); +}); + +// ── ListMemoryQuerySchema ── + +describe('ListMemoryQuerySchema', () => { + it('applies defaults for empty query', () => { + const result = ListMemoryQuerySchema.safeParse({}); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.limit).toBe(50); + expect(result.data.offset).toBe(0); + expect(result.data.type).toBeUndefined(); + expect(result.data.minImportance).toBeUndefined(); + } + }); + + it('accepts type filter', () => { + const result = ListMemoryQuerySchema.safeParse({ type: 'goal' }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe('goal'); + } + }); + + it('accepts minImportance filter', () => { + const result = ListMemoryQuerySchema.safeParse({ minImportance: '0.7' }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.minImportance).toBe(0.7); + } + }); + + it('accepts higher limit for memory', () => { + const result = ListMemoryQuerySchema.safeParse({ limit: 200 }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.limit).toBe(200); + } + }); + + it('rejects limit exceeding max', () => { + const result = ListMemoryQuerySchema.safeParse({ limit: 201 }); + expect(result.success).toBe(false); + }); + + it('rejects invalid type', () => { + const result = ListMemoryQuerySchema.safeParse({ type: 'thought' }); + expect(result.success).toBe(false); + }); +}); diff --git a/services/platform-service/src/modules/jarvis-memory/repository.ts b/services/platform-service/src/modules/jarvis-memory/repository.ts new file mode 100644 index 00000000..9bfd7f05 --- /dev/null +++ b/services/platform-service/src/modules/jarvis-memory/repository.ts @@ -0,0 +1,131 @@ +/** + * JarvisJr agent memory repository — Cosmos DB CRUD. + * Container: jarvis_memory, partition key: /agentId + */ + +import { getContainer } from '../../lib/cosmos.js'; +import type { JarvisMemoryDoc, ListMemoryQuery } from './types.js'; + +function container() { + return getContainer('jarvis_memory'); +} + +export async function listByAgent( + agentId: string, + query: ListMemoryQuery, +): Promise<{ memories: JarvisMemoryDoc[]; total: number }> { + const conditions = ['c.agentId = @agentId']; + const params: { name: string; value: string | number }[] = [ + { name: '@agentId', value: agentId }, + ]; + + if (query.type) { + conditions.push('c.type = @type'); + params.push({ name: '@type', value: query.type }); + } + if (query.minImportance !== undefined) { + conditions.push('c.importance >= @minImportance'); + params.push({ name: '@minImportance', value: query.minImportance }); + } + + const where = `WHERE ${conditions.join(' AND ')}`; + + const countResult = await container() + .items.query({ + query: `SELECT VALUE COUNT(1) FROM c ${where}`, + parameters: params, + }) + .fetchAll(); + const total = countResult.resources[0] ?? 0; + + const { resources } = await container() + .items.query({ + query: `SELECT * FROM c ${where} ORDER BY c.importance DESC, c.createdAt DESC OFFSET @offset LIMIT @limit`, + parameters: [ + ...params, + { name: '@offset', value: query.offset }, + { name: '@limit', value: query.limit }, + ], + }) + .fetchAll(); + + return { memories: resources, total }; +} + +export async function getById(id: string, agentId: string): Promise { + try { + const { resource } = await container().item(id, agentId).read(); + return resource ?? null; + } catch { + return null; + } +} + +export async function create(doc: JarvisMemoryDoc): Promise { + const { resource } = await container().items.create(doc); + return resource as JarvisMemoryDoc; +} + +export async function update( + id: string, + agentId: string, + updates: Partial, +): Promise { + try { + const { resource: existing } = await container().item(id, agentId).read(); + if (!existing) return null; + const merged = { ...existing, ...updates }; + const { resource } = await container().item(id, agentId).replace(merged); + return resource as JarvisMemoryDoc; + } catch { + return null; + } +} + +export async function remove(id: string, agentId: string): Promise { + try { + await container().item(id, agentId).delete(); + return true; + } catch { + return false; + } +} + +export async function pruneExpired(agentId: string): Promise { + const now = new Date().toISOString(); + const { resources } = await container() + .items.query({ + query: + 'SELECT * FROM c WHERE c.agentId = @agentId AND c.expiresAt != null AND c.expiresAt < @now', + parameters: [ + { name: '@agentId', value: agentId }, + { name: '@now', value: now }, + ], + }) + .fetchAll(); + + let count = 0; + for (const mem of resources) { + const deleted = await remove(mem.id, agentId); + if (deleted) count++; + } + return count; +} + +export async function getContextForSession( + agentId: string, + limit: number = 20, +): Promise { + const { resources } = await container() + .items.query({ + query: + 'SELECT * FROM c WHERE c.agentId = @agentId AND (c.expiresAt = null OR c.expiresAt > @now) ORDER BY c.importance DESC OFFSET 0 LIMIT @limit', + parameters: [ + { name: '@agentId', value: agentId }, + { name: '@now', value: new Date().toISOString() }, + { name: '@limit', value: limit }, + ], + }) + .fetchAll(); + return resources; +} diff --git a/services/platform-service/src/modules/jarvis-memory/routes.ts b/services/platform-service/src/modules/jarvis-memory/routes.ts new file mode 100644 index 00000000..874d3497 --- /dev/null +++ b/services/platform-service/src/modules/jarvis-memory/routes.ts @@ -0,0 +1,119 @@ +/** + * JarvisJr agent memory REST endpoints. + * + * GET /jarvis/agents/:agentId/memory — list memories for agent + * POST /jarvis/agents/:agentId/memory — create memory + * GET /jarvis/agents/:agentId/memory/context — get context for session + * GET /jarvis/agents/:agentId/memory/:id — get single memory + * PUT /jarvis/agents/:agentId/memory/:id — update memory + * DELETE /jarvis/agents/:agentId/memory/:id — delete memory + * POST /jarvis/agents/:agentId/memory/prune — prune expired memories + */ + +import type { FastifyInstance } from 'fastify'; +import crypto from 'node:crypto'; +import { BadRequestError, NotFoundError } from '../../lib/errors.js'; +import { extractAuth } from '../../lib/auth.js'; +import * as repo from './repository.js'; +import { + CreateMemorySchema, + UpdateMemorySchema, + ListMemoryQuerySchema, + type JarvisMemoryDoc, +} from './types.js'; + +const PRODUCT_ID = 'jarvisjr'; + +export async function jarvisMemoryRoutes(app: FastifyInstance) { + // Context retrieval — top memories for a session prompt + app.get('/jarvis/agents/:agentId/memory/context', async req => { + await extractAuth(req); + const { agentId } = req.params as { agentId: string }; + const limit = (req.query as { limit?: string }).limit + ? parseInt((req.query as { limit?: string }).limit!, 10) + : 20; + const memories = await repo.getContextForSession(agentId, limit); + return { memories, count: memories.length }; + }); + + // List memories + app.get('/jarvis/agents/:agentId/memory', async req => { + await extractAuth(req); + const { agentId } = req.params as { agentId: string }; + const parsed = ListMemoryQuerySchema.safeParse(req.query); + if (!parsed.success) { + throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; ')); + } + const { memories, total } = await repo.listByAgent(agentId, parsed.data); + return { memories, total, limit: parsed.data.limit, offset: parsed.data.offset }; + }); + + // Get memory + app.get('/jarvis/agents/:agentId/memory/:id', async req => { + await extractAuth(req); + const { agentId, id } = req.params as { agentId: string; id: string }; + const memory = await repo.getById(id, agentId); + if (!memory) throw new NotFoundError('Memory not found'); + return memory; + }); + + // Create memory + app.post('/jarvis/agents/:agentId/memory', async (req, reply) => { + const auth = await extractAuth(req); + const { agentId } = req.params as { agentId: string }; + const parsed = CreateMemorySchema.safeParse(req.body); + if (!parsed.success) { + throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; ')); + } + const input = parsed.data; + const now = new Date().toISOString(); + + const doc: JarvisMemoryDoc = { + id: `mem_${crypto.randomUUID()}`, + agentId, + userId: auth.sub, + productId: PRODUCT_ID, + sessionId: input.sessionId, + type: input.type, + content: input.content, + importance: input.importance, + tags: input.tags, + createdAt: now, + expiresAt: input.expiresAt, + }; + + const created = await repo.create(doc); + reply.code(201); + return created; + }); + + // Update memory + app.put('/jarvis/agents/:agentId/memory/:id', async req => { + await extractAuth(req); + const { agentId, id } = req.params as { agentId: string; id: string }; + const parsed = UpdateMemorySchema.safeParse(req.body); + if (!parsed.success) { + throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; ')); + } + const updated = await repo.update(id, agentId, parsed.data); + if (!updated) throw new NotFoundError('Memory not found'); + return updated; + }); + + // Delete memory + app.delete('/jarvis/agents/:agentId/memory/:id', async (req, reply) => { + await extractAuth(req); + const { agentId, id } = req.params as { agentId: string; id: string }; + const deleted = await repo.remove(id, agentId); + if (!deleted) throw new NotFoundError('Memory not found'); + reply.code(204); + }); + + // Prune expired memories + app.post('/jarvis/agents/:agentId/memory/prune', async req => { + await extractAuth(req); + const { agentId } = req.params as { agentId: string }; + const count = await repo.pruneExpired(agentId); + return { pruned: count }; + }); +} diff --git a/services/platform-service/src/modules/jarvis-memory/types.ts b/services/platform-service/src/modules/jarvis-memory/types.ts new file mode 100644 index 00000000..cc780e1a --- /dev/null +++ b/services/platform-service/src/modules/jarvis-memory/types.ts @@ -0,0 +1,53 @@ +/** + * JarvisJr agent memory types. + * Per-agent persistent memory for context retrieval during sessions. + * Partition key: /agentId + */ + +import { z } from 'zod'; + +export const MEMORY_TYPES = ['skill_note', 'preference', 'goal', 'context', 'exercise'] as const; + +export type MemoryType = (typeof MEMORY_TYPES)[number]; + +export interface JarvisMemoryDoc { + id: string; + agentId: string; + userId: string; + productId: string; + sessionId: string; + type: MemoryType; + content: string; + importance: number; + tags: string[]; + createdAt: string; + expiresAt: string | null; +} + +export const CreateMemorySchema = z.object({ + agentId: z.string().min(1), + sessionId: z.string().min(1), + type: z.enum(MEMORY_TYPES), + content: z.string().min(1).max(5000), + importance: z.number().min(0).max(1).default(0.5), + tags: z.array(z.string()).default([]), + expiresAt: z.string().nullable().default(null), +}); + +export const UpdateMemorySchema = z.object({ + content: z.string().min(1).max(5000).optional(), + importance: z.number().min(0).max(1).optional(), + tags: z.array(z.string()).optional(), + expiresAt: z.string().nullable().optional(), +}); + +export const ListMemoryQuerySchema = z.object({ + type: z.enum(MEMORY_TYPES).optional(), + minImportance: z.coerce.number().min(0).max(1).optional(), + limit: z.coerce.number().min(1).max(200).default(50), + offset: z.coerce.number().min(0).default(0), +}); + +export type CreateMemoryInput = z.infer; +export type UpdateMemoryInput = z.infer; +export type ListMemoryQuery = z.infer; diff --git a/services/platform-service/src/modules/jarvis-sessions/jarvis-sessions.test.ts b/services/platform-service/src/modules/jarvis-sessions/jarvis-sessions.test.ts new file mode 100644 index 00000000..d306f978 --- /dev/null +++ b/services/platform-service/src/modules/jarvis-sessions/jarvis-sessions.test.ts @@ -0,0 +1,193 @@ +/** + * JarvisJr sessions module unit tests — validates schema parsing and constants. + */ + +import { describe, it, expect } from 'vitest'; +import { + CreateSessionSchema, + CompleteSessionSchema, + ListSessionsQuerySchema, + SESSION_MODES, + SESSION_STATUSES, +} from './types.js'; + +// ── Constants ── + +describe('JarvisJr session constants', () => { + it('has expected session modes', () => { + expect(SESSION_MODES).toEqual(['text', 'voice']); + }); + + it('has expected session statuses', () => { + expect(SESSION_STATUSES).toEqual(['active', 'completed', 'abandoned']); + }); +}); + +// ── CreateSessionSchema ── + +describe('CreateSessionSchema', () => { + const validMinimal = { + agentId: 'agent_abc123', + mode: 'text', + }; + + it('accepts minimal valid input with defaults', () => { + const result = CreateSessionSchema.safeParse(validMinimal); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.agentId).toBe('agent_abc123'); + expect(result.data.mode).toBe('text'); + expect(result.data.transcript).toEqual([]); + expect(result.data.summary).toBe(''); + expect(result.data.coachingNotes).toEqual([]); + expect(result.data.exercises).toEqual([]); + expect(result.data.skillMetrics).toEqual([]); + expect(result.data.duration).toBe(0); + } + }); + + it('accepts voice mode', () => { + const result = CreateSessionSchema.safeParse({ ...validMinimal, mode: 'voice' }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.mode).toBe('voice'); + } + }); + + it('accepts full input with transcript and metrics', () => { + const result = CreateSessionSchema.safeParse({ + ...validMinimal, + transcript: [ + { role: 'agent', content: 'Hello!', ts: '2026-03-01T00:00:00Z' }, + { role: 'user', content: 'Hi there.', ts: '2026-03-01T00:00:01Z' }, + ], + summary: 'Good session on articulation.', + coachingNotes: ['Practice filler word awareness'], + exercises: ['Record 2-minute speech'], + skillMetrics: [{ name: 'articulation', score: 72, delta: 5 }], + duration: 900, + }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.transcript).toHaveLength(2); + expect(result.data.skillMetrics[0].score).toBe(72); + } + }); + + it('rejects missing agentId', () => { + const result = CreateSessionSchema.safeParse({ mode: 'text' }); + expect(result.success).toBe(false); + }); + + it('rejects missing mode', () => { + const result = CreateSessionSchema.safeParse({ agentId: 'agent_abc123' }); + expect(result.success).toBe(false); + }); + + it('rejects invalid mode', () => { + const result = CreateSessionSchema.safeParse({ ...validMinimal, mode: 'video' }); + expect(result.success).toBe(false); + }); + + it('rejects invalid transcript role', () => { + const result = CreateSessionSchema.safeParse({ + ...validMinimal, + transcript: [{ role: 'system', content: 'test', ts: '2026-03-01T00:00:00Z' }], + }); + expect(result.success).toBe(false); + }); + + it('rejects negative duration', () => { + const result = CreateSessionSchema.safeParse({ ...validMinimal, duration: -1 }); + expect(result.success).toBe(false); + }); + + it('rejects skill metric score out of range', () => { + const result = CreateSessionSchema.safeParse({ + ...validMinimal, + skillMetrics: [{ name: 'test', score: 101 }], + }); + expect(result.success).toBe(false); + }); +}); + +// ── CompleteSessionSchema ── + +describe('CompleteSessionSchema', () => { + const validComplete = { + transcript: [ + { role: 'agent' as const, content: 'Hello!', ts: '2026-03-01T00:00:00Z' }, + { role: 'user' as const, content: 'Hi.', ts: '2026-03-01T00:00:01Z' }, + ], + summary: 'Practiced public speaking introduction.', + duration: 600, + }; + + it('accepts valid complete input', () => { + const result = CompleteSessionSchema.safeParse(validComplete); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.transcript).toHaveLength(2); + expect(result.data.summary).toBe('Practiced public speaking introduction.'); + expect(result.data.duration).toBe(600); + expect(result.data.coachingNotes).toEqual([]); + } + }); + + it('rejects missing summary', () => { + const result = CompleteSessionSchema.safeParse({ + transcript: validComplete.transcript, + duration: 600, + }); + expect(result.success).toBe(false); + }); + + it('rejects empty summary', () => { + const result = CompleteSessionSchema.safeParse({ + ...validComplete, + summary: '', + }); + expect(result.success).toBe(false); + }); + + it('rejects missing transcript', () => { + const result = CompleteSessionSchema.safeParse({ + summary: 'Test', + duration: 600, + }); + expect(result.success).toBe(false); + }); +}); + +// ── ListSessionsQuerySchema ── + +describe('ListSessionsQuerySchema', () => { + it('applies defaults for empty query', () => { + const result = ListSessionsQuerySchema.safeParse({}); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.limit).toBe(50); + expect(result.data.offset).toBe(0); + expect(result.data.agentId).toBeUndefined(); + expect(result.data.status).toBeUndefined(); + } + }); + + it('accepts agentId filter', () => { + const result = ListSessionsQuerySchema.safeParse({ agentId: 'agent_xyz' }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.agentId).toBe('agent_xyz'); + } + }); + + it('accepts status filter', () => { + const result = ListSessionsQuerySchema.safeParse({ status: 'completed' }); + expect(result.success).toBe(true); + }); + + it('rejects invalid status', () => { + const result = ListSessionsQuerySchema.safeParse({ status: 'unknown' }); + expect(result.success).toBe(false); + }); +}); diff --git a/services/platform-service/src/modules/jarvis-sessions/repository.ts b/services/platform-service/src/modules/jarvis-sessions/repository.ts new file mode 100644 index 00000000..65a53cca --- /dev/null +++ b/services/platform-service/src/modules/jarvis-sessions/repository.ts @@ -0,0 +1,148 @@ +/** + * JarvisJr sessions repository — Cosmos DB CRUD. + * Container: jarvis_sessions, partition key: /userId + */ + +import { getContainer } from '../../lib/cosmos.js'; +import type { JarvisSessionDoc, ListSessionsQuery } from './types.js'; + +function container() { + return getContainer('jarvis_sessions'); +} + +export async function listByUser( + userId: string, + query: ListSessionsQuery, +): Promise<{ sessions: JarvisSessionDoc[]; total: number }> { + const conditions = ['c.userId = @userId']; + const params: { name: string; value: string | number }[] = [ + { name: '@userId', value: userId }, + ]; + + if (query.agentId) { + conditions.push('c.agentId = @agentId'); + params.push({ name: '@agentId', value: query.agentId }); + } + if (query.status) { + conditions.push('c.status = @status'); + params.push({ name: '@status', value: query.status }); + } + + const where = `WHERE ${conditions.join(' AND ')}`; + + const countResult = await container() + .items.query({ + query: `SELECT VALUE COUNT(1) FROM c ${where}`, + parameters: params, + }) + .fetchAll(); + const total = countResult.resources[0] ?? 0; + + const { resources } = await container() + .items.query({ + query: `SELECT * FROM c ${where} ORDER BY c.createdAt DESC OFFSET @offset LIMIT @limit`, + parameters: [ + ...params, + { name: '@offset', value: query.offset }, + { name: '@limit', value: query.limit }, + ], + }) + .fetchAll(); + + return { sessions: resources, total }; +} + +export async function getById(id: string, userId: string): Promise { + try { + const { resource } = await container().item(id, userId).read(); + return resource ?? null; + } catch { + return null; + } +} + +export async function create(doc: JarvisSessionDoc): Promise { + const { resource } = await container().items.create(doc); + return resource as JarvisSessionDoc; +} + +export async function update( + id: string, + userId: string, + updates: Partial, +): Promise { + try { + const { resource: existing } = await container().item(id, userId).read(); + if (!existing) return null; + const merged = { ...existing, ...updates }; + const { resource } = await container().item(id, userId).replace(merged); + return resource as JarvisSessionDoc; + } catch { + return null; + } +} + +export async function getStats(userId: string): Promise<{ + totalSessions: number; + currentStreak: number; + longestStreak: number; + perAgent: Record; +}> { + const { resources } = await container() + .items.query({ + query: + "SELECT c.agentId, c.createdAt FROM c WHERE c.userId = @userId AND c.status = 'completed' ORDER BY c.createdAt DESC", + parameters: [{ name: '@userId', value: userId }], + }) + .fetchAll(); + + const perAgent: Record = {}; + for (const s of resources) { + perAgent[s.agentId] = (perAgent[s.agentId] || 0) + 1; + } + + // Calculate streaks based on calendar days + let currentStreak = 0; + let longestStreak = 0; + let streak = 0; + let lastDate = ''; + + for (const s of resources) { + const date = s.createdAt.slice(0, 10); // YYYY-MM-DD + if (date === lastDate) continue; // same day + + if (!lastDate) { + streak = 1; + } else { + const prev = new Date(lastDate); + const curr = new Date(date); + const diffMs = prev.getTime() - curr.getTime(); // DESC order + const diffDays = Math.round(diffMs / 86400000); + if (diffDays === 1) { + streak += 1; + } else { + if (streak > longestStreak) longestStreak = streak; + streak = 1; + } + } + lastDate = date; + } + if (streak > longestStreak) longestStreak = streak; + + // Current streak: only if the most recent session is today or yesterday + if (resources.length > 0) { + const mostRecent = resources[0].createdAt.slice(0, 10); + const today = new Date().toISOString().slice(0, 10); + const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10); + if (mostRecent === today || mostRecent === yesterday) { + currentStreak = streak; + } + } + + return { + totalSessions: resources.length, + currentStreak, + longestStreak, + perAgent, + }; +} diff --git a/services/platform-service/src/modules/jarvis-sessions/routes.ts b/services/platform-service/src/modules/jarvis-sessions/routes.ts new file mode 100644 index 00000000..0647a352 --- /dev/null +++ b/services/platform-service/src/modules/jarvis-sessions/routes.ts @@ -0,0 +1,120 @@ +/** + * JarvisJr session REST endpoints. + * + * GET /jarvis/sessions — list user's sessions + * POST /jarvis/sessions — create/start session + * GET /jarvis/sessions/stats — streak + per-agent stats + * GET /jarvis/sessions/:id — get single session + * PUT /jarvis/sessions/:id/complete — complete a session with summary + */ + +import type { FastifyInstance } from 'fastify'; +import crypto from 'node:crypto'; +import { BadRequestError, NotFoundError } from '../../lib/errors.js'; +import { extractAuth } from '../../lib/auth.js'; +import * as repo from './repository.js'; +import * as agentRepo from '../jarvis-agents/repository.js'; +import { + CreateSessionSchema, + CompleteSessionSchema, + ListSessionsQuerySchema, + type JarvisSessionDoc, +} from './types.js'; + +const PRODUCT_ID = 'jarvisjr'; + +export async function jarvisSessionRoutes(app: FastifyInstance) { + // Stats — must be before :id route + app.get('/jarvis/sessions/stats', async req => { + const auth = await extractAuth(req); + return repo.getStats(auth.sub); + }); + + // List sessions + app.get('/jarvis/sessions', async req => { + const auth = await extractAuth(req); + const parsed = ListSessionsQuerySchema.safeParse(req.query); + if (!parsed.success) { + throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; ')); + } + const { sessions, total } = await repo.listByUser(auth.sub, parsed.data); + return { sessions, total, limit: parsed.data.limit, offset: parsed.data.offset }; + }); + + // Get session + app.get('/jarvis/sessions/:id', async req => { + const auth = await extractAuth(req); + const { id } = req.params as { id: string }; + const session = await repo.getById(id, auth.sub); + if (!session) throw new NotFoundError('Session not found'); + return session; + }); + + // Create / start session + app.post('/jarvis/sessions', async (req, reply) => { + const auth = await extractAuth(req); + const parsed = CreateSessionSchema.safeParse(req.body); + if (!parsed.success) { + throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; ')); + } + const input = parsed.data; + const now = new Date().toISOString(); + + const doc: JarvisSessionDoc = { + id: `sess_${crypto.randomUUID()}`, + userId: auth.sub, + productId: PRODUCT_ID, + agentId: input.agentId, + mode: input.mode, + status: 'active', + transcript: input.transcript, + summary: input.summary, + coachingNotes: input.coachingNotes, + exercises: input.exercises, + skillMetrics: input.skillMetrics, + duration: input.duration, + messageCount: input.transcript.length, + createdAt: now, + completedAt: null, + }; + + const created = await repo.create(doc); + reply.code(201); + return created; + }); + + // Complete session + app.put('/jarvis/sessions/:id/complete', async req => { + const auth = await extractAuth(req); + const { id } = req.params as { id: string }; + const parsed = CompleteSessionSchema.safeParse(req.body); + if (!parsed.success) { + throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; ')); + } + + const existing = await repo.getById(id, auth.sub); + if (!existing) throw new NotFoundError('Session not found'); + + const input = parsed.data; + const now = new Date().toISOString(); + + const updated = await repo.update(id, auth.sub, { + status: 'completed', + transcript: input.transcript, + summary: input.summary, + coachingNotes: input.coachingNotes, + exercises: input.exercises, + skillMetrics: input.skillMetrics, + duration: input.duration, + messageCount: input.transcript.length, + completedAt: now, + }); + + if (!updated) throw new NotFoundError('Session not found'); + + // Increment agent session count + await agentRepo.incrementSessionCount(existing.agentId, auth.sub); + + return updated; + }); +} diff --git a/services/platform-service/src/modules/jarvis-sessions/types.ts b/services/platform-service/src/modules/jarvis-sessions/types.ts new file mode 100644 index 00000000..daeafc1d --- /dev/null +++ b/services/platform-service/src/modules/jarvis-sessions/types.ts @@ -0,0 +1,74 @@ +/** + * JarvisJr session types. + * Each session is a coaching conversation between a user and an agent. + * Partition key: /userId + */ + +import { z } from 'zod'; + +export const SESSION_MODES = ['text', 'voice'] as const; +export const SESSION_STATUSES = ['active', 'completed', 'abandoned'] as const; + +export type SessionMode = (typeof SESSION_MODES)[number]; +export type SessionStatus = (typeof SESSION_STATUSES)[number]; + +export const TranscriptEntrySchema = z.object({ + role: z.enum(['user', 'agent']), + content: z.string(), + ts: z.string(), +}); + +export const SkillMetricSchema = z.object({ + name: z.string(), + score: z.number().min(0).max(100), + delta: z.number().optional(), +}); + +export interface JarvisSessionDoc { + id: string; + userId: string; + productId: string; + agentId: string; + mode: SessionMode; + status: SessionStatus; + transcript: Array<{ role: 'user' | 'agent'; content: string; ts: string }>; + summary: string; + coachingNotes: string[]; + exercises: string[]; + skillMetrics: Array<{ name: string; score: number; delta?: number }>; + duration: number; + messageCount: number; + createdAt: string; + completedAt: string | null; +} + +export const CreateSessionSchema = z.object({ + agentId: z.string().min(1), + mode: z.enum(SESSION_MODES), + transcript: z.array(TranscriptEntrySchema).default([]), + summary: z.string().default(''), + coachingNotes: z.array(z.string()).default([]), + exercises: z.array(z.string()).default([]), + skillMetrics: z.array(SkillMetricSchema).default([]), + duration: z.number().min(0).default(0), +}); + +export const CompleteSessionSchema = z.object({ + transcript: z.array(TranscriptEntrySchema), + summary: z.string().min(1), + coachingNotes: z.array(z.string()).default([]), + exercises: z.array(z.string()).default([]), + skillMetrics: z.array(SkillMetricSchema).default([]), + duration: z.number().min(0), +}); + +export const ListSessionsQuerySchema = z.object({ + agentId: z.string().optional(), + status: z.enum(SESSION_STATUSES).optional(), + limit: z.coerce.number().min(1).max(100).default(50), + offset: z.coerce.number().min(0).default(0), +}); + +export type CreateSessionInput = z.infer; +export type CompleteSessionInput = z.infer; +export type ListSessionsQuery = z.infer; diff --git a/services/platform-service/src/server.test.ts b/services/platform-service/src/server.test.ts index 42b20028..b1daebba 100644 --- a/services/platform-service/src/server.test.ts +++ b/services/platform-service/src/server.test.ts @@ -59,6 +59,9 @@ vi.mock('./modules/memory/routes.js', () => ({ memoryRoutes: vi.fn() })); vi.mock('./modules/public/routes.js', () => ({ publicRoutes: vi.fn() })); vi.mock('./modules/tokens/routes.js', () => ({ tokenRoutes: vi.fn() })); vi.mock('./modules/themes/routes.js', () => ({ themeRoutes: vi.fn() })); +vi.mock('./modules/jarvis-agents/routes.js', () => ({ jarvisAgentRoutes: vi.fn() })); +vi.mock('./modules/jarvis-sessions/routes.js', () => ({ jarvisSessionRoutes: vi.fn() })); +vi.mock('./modules/jarvis-memory/routes.js', () => ({ jarvisMemoryRoutes: vi.fn() })); vi.mock('./lib/cosmos-init.js', () => ({ initCosmosIfNeeded: initCosmosIfNeededMock })); vi.mock('./lib/config.js', () => ({ config: { CORS_ORIGIN: '*', PORT: 4003, HOST: '0.0.0.0' } })); vi.mock('./modules/auth/jwt.js', () => ({ verifyToken: verifyTokenMock })); diff --git a/services/platform-service/src/server.ts b/services/platform-service/src/server.ts index ca080e8f..784ae143 100644 --- a/services/platform-service/src/server.ts +++ b/services/platform-service/src/server.ts @@ -74,6 +74,10 @@ import { feedbackRoutes } from './modules/feedback/routes.js'; import { impersonationRoutes } from './modules/impersonation/routes.js'; import { changelogRoutes } from './modules/changelog/routes.js'; import { pushTriggerRoutes } from './modules/push-triggers/routes.js'; +// JarvisJr modules +import { jarvisAgentRoutes } from './modules/jarvis-agents/routes.js'; +import { jarvisSessionRoutes } from './modules/jarvis-sessions/routes.js'; +import { jarvisMemoryRoutes } from './modules/jarvis-memory/routes.js'; import { initCosmosIfNeeded } from './lib/cosmos-init.js'; import { config } from './lib/config.js'; import { seedDefaultFlags } from './modules/flags/seed.js'; @@ -191,5 +195,9 @@ await app.register(impersonationRoutes, { prefix: '/api' }); await app.register(changelogRoutes, { prefix: '/api' }); // Push notification triggers (NomGap) await app.register(pushTriggerRoutes, { prefix: '/api' }); +// JarvisJr modules (agents, sessions, memory) +await app.register(jarvisAgentRoutes, { prefix: '/api' }); +await app.register(jarvisSessionRoutes, { prefix: '/api' }); +await app.register(jarvisMemoryRoutes, { prefix: '/api' }); await startService(app, { port: config.PORT, host: config.HOST });