docs: add agent prompts sync guide and workspace anti-patterns
This commit is contained in:
parent
c3885059cf
commit
5663ef568a
383
docs/AGENT_PROMPTS_SYNC_AND_COSMOS.md
Normal file
383
docs/AGENT_PROMPTS_SYNC_AND_COSMOS.md
Normal file
@ -0,0 +1,383 @@
|
||||
# Agent Prompts: Client-Side Sync Integration + MindLyst Cosmos Persistence
|
||||
|
||||
> Two self-contained prompts for AI coding agents. Each prompt includes full context, exact file paths, what exists, what's missing, and verification commands. No guesswork required.
|
||||
|
||||
---
|
||||
|
||||
## Prompt 1: Client-Side Sync Integration (ChronoMind + NomGap)
|
||||
|
||||
### Context
|
||||
|
||||
Platform-service (Fastify 5, port 4003) in `learning_ai_common_plat/services/platform-service/` has fully built and tested REST APIs:
|
||||
|
||||
**ChronoMind modules (130 tests):**
|
||||
|
||||
- `timers` — 7 endpoints: GET /timers, GET /timers/sync, GET /timers/:id, POST /timers, PUT /timers/:id, DELETE /timers/:id, POST /timers/batch
|
||||
- `routines` — 7 endpoints: same pattern as timers at /routines/\*
|
||||
- `households` — 9 endpoints at /households/\*
|
||||
- `shared-timers` — 6 endpoints at /households/:householdId/timers/\*
|
||||
|
||||
**NomGap modules (52 tests):**
|
||||
|
||||
- `fasting-sessions` — 6 endpoints: POST/GET/GET/:id/PUT /fasting/sessions, GET /fasting/sessions/stats, GET /fasting/sessions/stats/weekly
|
||||
- `fasting-protocols` — 5 endpoints: GET/GET/:id/POST/PUT/:id/DELETE/:id /fasting/protocols
|
||||
- `body-stages` — 2 endpoints: GET /fasting/stages, POST /fasting/autophagy-confidence
|
||||
|
||||
All endpoints use JWT auth via `Authorization: Bearer <token>`, `x-product-id` header, `x-request-id` header, and `productId` field in Cosmos documents. Sync protocol uses `syncVersion` (monotonic integer) for optimistic concurrency, delta sync via `?since=<ISO>`, and batch upsert returning `{ synced, conflicts, errors }`.
|
||||
|
||||
### What Already Exists (DO NOT REBUILD)
|
||||
|
||||
#### ChronoMind Web (`learning_ai_clock/web/src/lib/`)
|
||||
|
||||
- **`platform-sync.ts`** (447 lines) — Full sync client with DTOs (`SyncTimerDTO`, `SyncRoutineDTO`, `OfflineQueueItem`), API functions (`pullDelta`, `pushTimer`, `updateRemoteTimer`, `deleteRemoteTimer`, `batchUpsert`, `pullRoutineDelta`, `pushRoutine`, `updateRemoteRoutine`, `deleteRemoteRoutine`, `batchUpsertRoutines`), offline queue (`enqueueChange`, `enqueueDeleteChange`, `loadOfflineQueue`, `saveOfflineQueue`), `fullSync()`, and DTO converters (`timerToDTO`, `dtoToTimerPatch`, `routineToDTO`, `dtoToRoutinePatch`).
|
||||
- **`use-sync.ts`** (198 lines) — React hook (`useSync`) with `syncNow`, `setSyncEnabled`, `login`, `logout`, `syncedAddTimer`, `syncedUpdateTimer`, `syncedRemoveTimer`. Auto-syncs on 60s interval. Merges pulled timers into Zustand store.
|
||||
- **`auth-api.ts`** (32 lines) — `@bytelyst/auth-client` wrapper with `getAuthClient()`, `PRODUCT_ID = 'chronomind'`.
|
||||
- **`auth-context.tsx`** — React auth context with login/register/logout/forgotPassword/changePassword/deleteAccount.
|
||||
- **`store.ts`** — Zustand store with `addTimer`, `removeTimer`, `updateTimer`, `pause`, `resume`, etc. Uses `localStorage` persistence.
|
||||
- **`routine-store.ts`** — Zustand store for routines.
|
||||
|
||||
**Dashboard.tsx** already imports `useSync` and destructures `{ isSyncing, syncEnabled, pendingChanges, lastError }` for status display. But **it does NOT call `syncedAddTimer`/`syncedUpdateTimer`/`syncedRemoveTimer`** — timer mutations go directly to the Zustand store without enqueueing sync changes.
|
||||
|
||||
#### ChronoMind iOS (`learning_ai_clock/ios/ChronoMind/Shared/Cloud/`)
|
||||
|
||||
- **`PlatformSyncManager.swift`** (450 lines) — Full `@MainActor ObservableObject` singleton with `pullDelta()`, `pushTimer()`, `updateTimer()`, `deleteTimer()`, `pushOfflineQueue()`, `enqueueChange()`, `enqueueDelete()`, periodic sync (60s), `timerToDTO()`, `dtoToTimerPatch()`. DTOs match server schema.
|
||||
- **BUT:** `PlatformSyncManager` is NOT called from any SwiftUI view or `TimerStore`. The `TimerStore` uses `UserDefaults` directly. `sync(localTimers:)` method exists but no view triggers it.
|
||||
|
||||
#### ChronoMind Android (`learning_ai_clock/android/app/src/main/java/com/chronomind/app/sync/`)
|
||||
|
||||
- **`PlatformApiClient.kt`** (182 lines) — HTTP client with `pullDelta()`, `createTimer()`, `updateTimer()`, `deleteTimer()`, `batchUpsert()`, `pullRoutinesDelta()`. Uses `HttpURLConnection`.
|
||||
- **`SyncRepository.kt`** (246 lines) — Hilt `@Singleton` with `sync()`, `enqueueCreate()`, `enqueueUpdate()`, `enqueueDelete()`, offline queue in `SharedPreferences`. Merges pulled timers into Room via `timerDao.upsert()`.
|
||||
- **BUT:** `SyncRepository` is NOT injected into `TimerViewModel`. The `TimerViewModel` calls `TimerDao` directly without sync.
|
||||
|
||||
#### NomGap (`learning_ai_fastgap/src/`)
|
||||
|
||||
- **`api/client.ts`** (116 lines) — Fetch wrapper with auth token via `@bytelyst/auth-client`, `x-product-id: nomgap`, `x-request-id`, timeout, 401 retry.
|
||||
- **`api/auth-api.ts`** — Auth client with `getAuthClient()`, `PRODUCT_ID = 'nomgap'`.
|
||||
- **`api/fasting-api.ts`** (90 lines) — `createSession()`, `getSession()`, `listSessions()`, `updateSession()`, `getUserStats()`, `getWeeklyStats()` calling platform-service `/fasting/sessions/*`.
|
||||
- **`store/fasting-store.ts`** — Has a fire-and-forget `syncSessionToBackend()` that calls `createSession()` on session start, but `.catch(() => {})` silently swallows errors. No session-end sync, no session-update sync, no history pull from server.
|
||||
- **`store/user-store.ts`** — Has `loginWithAuth`, `registerWithAuth`, `hydrateFromToken`, `logout` wired.
|
||||
- **NO login/register UI screens exist yet** (only store actions).
|
||||
|
||||
### What Needs To Be Built
|
||||
|
||||
#### A. ChronoMind Web — Wire sync into timer/routine CRUD
|
||||
|
||||
**Files to modify:**
|
||||
|
||||
1. `web/src/components/Dashboard.tsx` — Currently destructures `useSync()` but only uses status fields. Need to also destructure `syncedAddTimer`, `syncedUpdateTimer`, `syncedRemoveTimer` and call them alongside the Zustand store mutations.
|
||||
2. `web/src/components/CreateTimerModal.tsx` — After `addTimer(newTimer)`, also call `syncedAddTimer(newTimer)`.
|
||||
3. `web/src/components/TimerCard.tsx` — After dismiss/complete/snooze actions that call store mutations, also call `syncedUpdateTimer`.
|
||||
4. `web/src/components/AlarmOverlay.tsx` — After dismiss/snooze, call `syncedUpdateTimer`.
|
||||
5. `web/src/components/PomodoroView.tsx` — After round complete/session complete, call `syncedUpdateTimer`.
|
||||
6. `web/src/components/RoutineEditor.tsx` + `web/src/components/RoutineRunner.tsx` — Need routine sync. The `useSync` hook currently only syncs timers. Extend `fullSync()` in `platform-sync.ts` to also pull/push routines in the same sync cycle.
|
||||
|
||||
**Pattern to follow:**
|
||||
|
||||
```typescript
|
||||
// In Dashboard.tsx, pass sync functions down or lift via context
|
||||
const { syncedAddTimer, syncedUpdateTimer, syncedRemoveTimer } = useSync();
|
||||
|
||||
// In every component that mutates timers:
|
||||
// AFTER the Zustand store call (which updates UI immediately), ALSO enqueue sync
|
||||
addTimer(newTimer);
|
||||
syncedAddTimer(newTimer); // non-blocking — just enqueues in localStorage
|
||||
```
|
||||
|
||||
**Critical rules:**
|
||||
|
||||
- Zustand store is the source of truth for UI. Sync is fire-and-enqueue, never blocking.
|
||||
- `syncedAddTimer` etc. check `isSyncEnabled()` internally — safe to call always.
|
||||
- Don't wrap in try/catch — the enqueue is synchronous localStorage write.
|
||||
- Pass sync functions through props or React context — do NOT import `useSync` in every child component (breaks hook rules if used in non-component functions).
|
||||
|
||||
**Bug to fix in `fullSync()` (`platform-sync.ts` line 294-339):**
|
||||
|
||||
- `fullSync()` currently only syncs timers. It needs to ALSO call `pullRoutineDelta()` and `batchUpsertRoutines()` for routines in the offline queue. The routine DTO conversion functions already exist (`routineToDTO`, `dtoToRoutinePatch`). Extend `SyncResult` to include `pulledRoutines` and merge them into the routine store.
|
||||
|
||||
#### B. ChronoMind iOS — Wire PlatformSyncManager into TimerStore
|
||||
|
||||
**Files to modify:**
|
||||
|
||||
1. `ios/ChronoMind/Shared/Store/TimerStore.swift` — Add a reference to `PlatformSyncManager.shared`. After every timer add/update/delete in the store, call `PlatformSyncManager.shared.enqueueChange(timer, .create/.update)` or `.enqueueDelete(timerId:)`.
|
||||
2. `ios/ChronoMind/App/ContentView.swift` or root view — On app launch, call `PlatformSyncManager.shared.restoreAuthToken()` equivalent (token is already restored in `init`). On appear, trigger initial sync.
|
||||
3. `ios/ChronoMind/Views/Components/` — No changes needed; store is the single source of truth.
|
||||
|
||||
**Critical rules:**
|
||||
|
||||
- `PlatformSyncManager` is `@MainActor` — safe to call from SwiftUI views.
|
||||
- The `sync(localTimers:)` method pulls delta AND pushes offline queue. Call it on app foreground (`scenePhase == .active`).
|
||||
- `enqueueChange` is synchronous (UserDefaults write) — call it inline after store mutations.
|
||||
- Do NOT block UI on sync. The `async` sync runs in background.
|
||||
|
||||
#### C. ChronoMind Android — Wire SyncRepository into TimerViewModel
|
||||
|
||||
**Files to modify:**
|
||||
|
||||
1. `android/app/src/main/java/com/chronomind/app/viewmodel/TimerViewModel.kt` — Inject `SyncRepository` via Hilt constructor. After every `timerDao.insert/update/delete`, also call `syncRepository.enqueueCreate/enqueueUpdate/enqueueDelete`.
|
||||
2. `android/app/src/main/java/com/chronomind/app/di/AppModule.kt` — Ensure `SyncRepository` is provided (it's already `@Singleton` with `@Inject constructor` — Hilt should auto-provide).
|
||||
3. App startup (e.g., `MainActivity.kt`) — Call `syncRepository.restoreAuthToken()` and trigger initial `sync()` in a coroutine scope.
|
||||
|
||||
**Critical rules:**
|
||||
|
||||
- `sync()` is a `suspend fun` — call from `viewModelScope.launch(Dispatchers.IO)`.
|
||||
- `enqueueCreate/enqueueUpdate/enqueueDelete` are regular (non-suspend) functions — safe to call inline.
|
||||
- Trigger sync on app resume (Activity `onResume`).
|
||||
|
||||
#### D. NomGap — Complete session sync + add login UI
|
||||
|
||||
**Files to modify:**
|
||||
|
||||
1. `src/store/fasting-store.ts` — The `syncSessionToBackend()` function is fire-and-forget on session START only. Add:
|
||||
- On `endFast()`: call `updateSession(session.id, { status, endedAt, waterIntake, notes })`.
|
||||
- On `pauseFast()`/`resumeFast()`: call `updateSession(session.id, { status, pausedAt })`.
|
||||
- On `addMoodCheckin()`: call `updateSession(session.id, { ... })` with updated checkins.
|
||||
- On `tick()` stage transitions: optionally batch these (don't call API on every tick — debounce to every 5 minutes or on stage change).
|
||||
- Add a `loadSessionHistory()` action that calls `listSessions()` and populates `sessionHistory` from the server (for cross-device history).
|
||||
|
||||
2. `src/api/fasting-api.ts` — Already complete. No changes needed.
|
||||
|
||||
3. **New file: `src/screens/auth/AuthScreen.tsx`** — Login/register screen. Use `useUserStore().loginWithAuth` and `registerWithAuth` which already exist. Include:
|
||||
- Email + password fields
|
||||
- Login / Register toggle
|
||||
- Error display
|
||||
- "Continue without account" option (sets local-only mode)
|
||||
- Navigate to main app on success
|
||||
|
||||
4. `src/app/(tabs)/_layout.tsx` or navigation — Add auth gate: if user not authenticated and `preference !== 'local_only'`, show AuthScreen.
|
||||
|
||||
5. `src/store/fasting-store.ts` `startFast()` — Currently generates `userId: 'local'`. When authenticated, use `useUserStore.getState().profile?.id` instead.
|
||||
|
||||
**Critical rules:**
|
||||
|
||||
- NomGap is React Native (Expo) — NO web-specific APIs (`localStorage`, `window`). Use MMKV for persistence.
|
||||
- `@bytelyst/auth-client` is already configured in `src/api/auth-api.ts`.
|
||||
- Session sync should be resilient to offline — the existing `.catch(() => {})` pattern is correct for fire-and-forget, but add an offline queue similar to ChronoMind's pattern for session updates that fail.
|
||||
- Do NOT sync on every `tick()` — debounce stage transitions to avoid API spam.
|
||||
|
||||
### Verification Commands
|
||||
|
||||
```bash
|
||||
# ChronoMind web
|
||||
cd learning_ai_clock/web && npm test && npm run typecheck
|
||||
|
||||
# ChronoMind iOS
|
||||
# Open ChronoMind.xcodeproj, Cmd+B
|
||||
|
||||
# ChronoMind Android
|
||||
cd learning_ai_clock/android && ./gradlew :app:compileDebugKotlin
|
||||
|
||||
# NomGap
|
||||
cd learning_ai_fastgap && npm test && npm run typecheck
|
||||
|
||||
# Platform-service (should still pass — no server changes)
|
||||
cd learning_ai_common_plat && pnpm --filter @lysnrai/platform-service test
|
||||
```
|
||||
|
||||
### Commit Convention
|
||||
|
||||
- `feat(web): wire timer sync into Dashboard CRUD operations`
|
||||
- `feat(ios): connect PlatformSyncManager to TimerStore`
|
||||
- `feat(android): inject SyncRepository into TimerViewModel`
|
||||
- `feat(app): add session sync on end/pause/resume + auth screen`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 2: MindLyst Cosmos DB Persistence — Remove In-Memory Fallbacks
|
||||
|
||||
### Context
|
||||
|
||||
MindLyst web (`learning_multimodal_memory_agents/mindlyst-native/web/`) is a Next.js 16 App Router application with 33 API route files in `src/app/api/`. The data layer has a dual-mode pattern:
|
||||
|
||||
```typescript
|
||||
const container = isCosmosConfigured() ? getCosmosContainer('brains') : null;
|
||||
if (container) {
|
||||
// Cosmos path
|
||||
} else {
|
||||
// In-memory fallback path
|
||||
}
|
||||
```
|
||||
|
||||
**11 routes already have working Cosmos paths** (they use `isCosmosConfigured()` + `getCosmosContainer()`):
|
||||
|
||||
1. `brains/route.ts` — container: `brains`
|
||||
2. `memory/route.ts` — container: `memory_items`
|
||||
3. `streak/route.ts` — container: `streaks`
|
||||
4. `notifications/route.ts` — container: `notification_log`
|
||||
5. `brief/route.ts` — container: `daily_briefs`
|
||||
6. `reflection/route.ts` — container: `reflections`
|
||||
7. `share-card/route.ts` — container: `share_cards`
|
||||
8. `brain-growth/route.ts` — container: `brain_insights`
|
||||
9. `analytics/route.ts` — container: `analytics_events`
|
||||
10. `insights/route.ts` — container: `brain_insights`
|
||||
11. `seed/route.ts` — calls `ensureContainers()`
|
||||
|
||||
**~22 routes are PURELY in-memory** — they use module-level `const items: T[] = []` or `let state = {...}` or `new Map()`. These lose all data on server restart. They need to be wired to Cosmos using the same dual-mode pattern.
|
||||
|
||||
### What Already Exists (DO NOT REBUILD)
|
||||
|
||||
- **`src/lib/cosmos.ts`** (74 lines) — `isCosmosConfigured()`, `getCosmosContainer(containerId)`, `MINDLYST_CONTAINERS` array (9 containers), `ensureContainers()`.
|
||||
- **`src/lib/user.ts`** — `resolveUserId(headers)` for extracting userId from request.
|
||||
- **`src/lib/abuse.ts`** — `checkRateLimit()` for rate limiting.
|
||||
- **`.env.example`** — Shows `COSMOS_ENDPOINT`, `COSMOS_KEY`, `COSMOS_DATABASE` env vars.
|
||||
- **Pattern in existing routes** (e.g., `brains/route.ts`) — Shows exactly how to do the dual-mode: check `isCosmosConfigured()`, get container, use parameterized queries with `partitionKey: userId`.
|
||||
|
||||
### What Needs To Be Built
|
||||
|
||||
#### Phase 1: Add new containers to `cosmos.ts`
|
||||
|
||||
Add these containers to `MINDLYST_CONTAINERS` in `src/lib/cosmos.ts`:
|
||||
|
||||
```typescript
|
||||
// Add to MINDLYST_CONTAINERS array:
|
||||
{ id: "triage_results", partitionKey: "/userId" },
|
||||
{ id: "brain_packs", partitionKey: "/userId" },
|
||||
{ id: "referrals", partitionKey: "/userId" },
|
||||
{ id: "ab_tests", partitionKey: "/userId" },
|
||||
{ id: "waitlist", partitionKey: "/id" }, // no userId for public waitlist
|
||||
{ id: "email_captures", partitionKey: "/userId" },
|
||||
{ id: "engagement_data", partitionKey: "/userId" },
|
||||
{ id: "context_triggers", partitionKey: "/userId" },
|
||||
{ id: "brain_chats", partitionKey: "/userId" },
|
||||
```
|
||||
|
||||
#### Phase 2: Wire each purely-in-memory route to Cosmos
|
||||
|
||||
For each route below, follow this exact pattern:
|
||||
|
||||
1. Import `getCosmosContainer, isCosmosConfigured` from `@/lib/cosmos`
|
||||
2. At the top of the handler, get the container: `const container = isCosmosConfigured() ? getCosmosContainer("<name>") : null;`
|
||||
3. For every READ operation: if `container`, query Cosmos with parameterized SQL; else use the existing in-memory array/map.
|
||||
4. For every WRITE operation: if `container`, use `container.items.create()` / `container.item(id, partitionKey).replace()` / `container.item(id, partitionKey).delete()`; else use the existing in-memory mutation.
|
||||
5. **Keep the in-memory fallback intact** — don't remove it. It's needed for local dev without Cosmos.
|
||||
|
||||
**Routes to wire (sorted by data importance):**
|
||||
|
||||
| # | Route File | Container | Partition Key | In-Memory Pattern |
|
||||
| --- | ------------------------------- | ------------------ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | `triage/route.ts` | `triage_results` | `/userId` | Module-level arrays for triage results and retry queue |
|
||||
| 2 | `brain-chat/route.ts` | `brain_chats` | `/userId` | `new Map()` for conversation history + embedding cache |
|
||||
| 3 | `brain-packs/route.ts` | `brain_packs` | `/userId` | Arrays for packs, submissions, moderation queue |
|
||||
| 4 | `referral/route.ts` | `referrals` | `/userId` | Arrays for referral links and activations |
|
||||
| 5 | `ab-test/route.ts` | `ab_tests` | `/userId` | Arrays for experiments and user assignments |
|
||||
| 6 | `waitlist/route.ts` | `waitlist` | `/id` | Array for waitlist entries (public, no userId) |
|
||||
| 7 | `email-capture/route.ts` | `email_captures` | `/userId` | Array for captured emails |
|
||||
| 8 | `engagement/route.ts` | `engagement_data` | `/userId` | Arrays for segments and campaigns |
|
||||
| 9 | `context-triggers/route.ts` | `context_triggers` | `/userId` | Arrays for location/calendar triggers |
|
||||
| 10 | `nudge/route.ts` | `memory_items` | `/userId` | Queries existing memory_items container (reads memory items > 48h not acted on) — may just need container reference, not new container |
|
||||
| 11 | `monitoring/route.ts` | `analytics_events` | `/userId` | Reads from analytics — may just need container reference |
|
||||
| 12 | `share-templates/route.ts` | — | — | Static data (12 templates). **DO NOT persist to Cosmos** — these are constants, not user data. |
|
||||
| 13 | `store-listing/route.ts` | — | — | Static data. **DO NOT persist.** |
|
||||
| 14 | `launch/route.ts` | — | — | Static launch plan data. **DO NOT persist.** |
|
||||
| 15 | `onboarding-email/route.ts` | — | — | Static email templates. **DO NOT persist.** |
|
||||
| 16 | `prompts/route.ts` | — | — | Computed prompts. **DO NOT persist.** |
|
||||
| 17 | `capture-config/route.ts` | — | — | Static config. **DO NOT persist.** |
|
||||
| 18 | `accessibility-config/route.ts` | — | — | Static config. **DO NOT persist.** |
|
||||
| 19 | `push-content/route.ts` | — | — | Static content definitions. **DO NOT persist.** |
|
||||
| 20 | `scheduler/route.ts` | — | — | Config/definitions for offline queue + sync. **DO NOT persist.** |
|
||||
| 21 | `thumbnails/route.ts` | — | — | Metadata cache. **DO NOT persist** (cache is ephemeral by design). |
|
||||
| 22 | `extract/route.ts` | — | — | Proxy to extraction-service. **No data to persist.** |
|
||||
|
||||
**Only routes 1-11 need Cosmos wiring.** Routes 12-22 are static data, computed responses, or proxies — they don't store user data and should NOT be persisted.
|
||||
|
||||
#### Phase 3: Document type consistency
|
||||
|
||||
Every Cosmos document MUST include:
|
||||
|
||||
- `id: string` — unique document ID
|
||||
- `userId: string` — partition key value
|
||||
- `productId: 'mindlyst'` — **CRITICAL: missing from current routes.** Add `productId: 'mindlyst'` to every `create()` call.
|
||||
- `createdAt: string` — ISO timestamp
|
||||
- `updatedAt: string | null` — ISO timestamp on updates
|
||||
|
||||
Check existing routes (`brains`, `memory`, `streak`, etc.) and add `productId: 'mindlyst'` where missing.
|
||||
|
||||
### Critical Rules
|
||||
|
||||
1. **Never remove the in-memory fallback.** The `else` branch must always work for local dev without Cosmos.
|
||||
2. **Always use parameterized queries** — never string-interpolate user input into SQL. Pattern: `{ query: "SELECT * FROM c WHERE c.userId = @userId", parameters: [{ name: "@userId", value: userId }] }`.
|
||||
3. **Always pass `{ partitionKey: userId }` to queries and point reads.** Cosmos requires this for cross-partition query avoidance.
|
||||
4. **Use `container.item(id, partitionKey).read/replace/delete()`** for single-document operations — NOT queries.
|
||||
5. **Add `productId: 'mindlyst'`** to every document creation.
|
||||
6. **Do NOT add new npm dependencies.** `@azure/cosmos` is already in `package.json`.
|
||||
7. **Do NOT modify the `cosmos.ts` client pattern.** The singleton + `getCosmosContainer()` approach is correct.
|
||||
8. **Handle Cosmos errors gracefully** — wrap in try/catch, return 500 with generic error message (never expose Cosmos error details to client).
|
||||
9. **Brain chat embedding cache** (`brain-chat/route.ts`) uses a `Map` with 5-min TTL for performance. Keep the in-memory cache even when Cosmos is configured — persist conversation history to Cosmos but keep the embedding cache in memory.
|
||||
|
||||
### Example Transformation
|
||||
|
||||
**Before (pure in-memory):**
|
||||
|
||||
```typescript
|
||||
const referrals: Referral[] = [];
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
const referral = { id: `ref_${Date.now()}`, ...body };
|
||||
referrals.push(referral);
|
||||
return NextResponse.json(referral, { status: 201 });
|
||||
}
|
||||
```
|
||||
|
||||
**After (dual-mode):**
|
||||
|
||||
```typescript
|
||||
import { getCosmosContainer, isCosmosConfigured } from '@/lib/cosmos';
|
||||
import { resolveUserId } from '@/lib/user';
|
||||
|
||||
const referrals: Referral[] = []; // in-memory fallback
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const container = isCosmosConfigured() ? getCosmosContainer('referrals') : null;
|
||||
const userId = resolveUserId(request.headers);
|
||||
const body = await request.json();
|
||||
const referral = {
|
||||
id: `ref_${Date.now()}_${crypto.randomUUID()}`,
|
||||
userId,
|
||||
productId: 'mindlyst',
|
||||
...body,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (container) {
|
||||
await container.items.create(referral);
|
||||
} else {
|
||||
referrals.push(referral);
|
||||
}
|
||||
|
||||
return NextResponse.json(referral, { status: 201 });
|
||||
}
|
||||
```
|
||||
|
||||
### Verification Commands
|
||||
|
||||
```bash
|
||||
# Type-check (catches import errors, type mismatches)
|
||||
cd learning_multimodal_memory_agents/mindlyst-native/web && npx tsc --noEmit
|
||||
|
||||
# Build (catches runtime import issues)
|
||||
cd learning_multimodal_memory_agents/mindlyst-native/web && npx next build --webpack
|
||||
|
||||
# Manual smoke test: start dev server, hit API routes
|
||||
cd learning_multimodal_memory_agents/mindlyst-native/web && npm run dev -- -p 3050
|
||||
# Then: curl http://localhost:3050/api/brains (should return default brains in-memory mode)
|
||||
# Then: curl http://localhost:3050/api/streak (should return streak)
|
||||
```
|
||||
|
||||
### Verification Checklist (agent must confirm each)
|
||||
|
||||
- [ ] `npx tsc --noEmit` passes with zero errors
|
||||
- [ ] `npx next build --webpack` succeeds
|
||||
- [ ] All 11 routes that need Cosmos wiring now import `isCosmosConfigured` + `getCosmosContainer`
|
||||
- [ ] Every `container.items.create()` call includes `productId: 'mindlyst'`
|
||||
- [ ] In-memory fallback still works (test with no `COSMOS_*` env vars set)
|
||||
- [ ] No new npm dependencies added
|
||||
- [ ] `MINDLYST_CONTAINERS` in `cosmos.ts` updated with new container definitions
|
||||
- [ ] `seed/route.ts` `ensureContainers()` will auto-create all new containers
|
||||
|
||||
### Commit Convention
|
||||
|
||||
- `feat(web): add Cosmos containers for triage, brain-chat, brain-packs, referrals, ab-tests, waitlist, email-capture, engagement, context-triggers`
|
||||
- `feat(web): wire triage + brain-chat + brain-packs routes to Cosmos DB`
|
||||
- `feat(web): wire referral + ab-test + waitlist + email-capture routes to Cosmos DB`
|
||||
- `feat(web): wire engagement + context-triggers + nudge + monitoring routes to Cosmos DB`
|
||||
- `fix(web): add productId: 'mindlyst' to all Cosmos document creation calls`
|
||||
297
docs/WORKSPACE_ANTI_PATTERNS.md
Normal file
297
docs/WORKSPACE_ANTI_PATTERNS.md
Normal file
@ -0,0 +1,297 @@
|
||||
# Workspace Anti-Pattern Audit
|
||||
|
||||
> **Date:** 2026-02-28
|
||||
> **Scope:** All 5 workspace repos (`learning_ai_common_plat`, `learning_voice_ai_agent`, `learning_multimodal_memory_agents`, `learning_ai_clock`, `learning_ai_fastgap`)
|
||||
> **Method:** Automated grep/scan across all repos + manual review
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Severity | Count | Category |
|
||||
| ------------------------------- | ------ | -------------------------------------------------------- |
|
||||
| **P0 — Security / Data Loss** | 4 | Auth gaps, secrets exposure, CORS wildcard |
|
||||
| **P1 — Reliability / Crashes** | 6 | Missing error handling, no retries, no graceful shutdown |
|
||||
| **P2 — Maintainability / Debt** | 8 | Code duplication, version mismatches, package divergence |
|
||||
| **P3 — Operational / DX** | 6 | CI gaps, env sprawl, missing observability |
|
||||
| **Total** | **24** | |
|
||||
|
||||
---
|
||||
|
||||
## P0 — Security / Data Loss
|
||||
|
||||
### 1. Admin API routes missing auth guards — CRITICAL
|
||||
|
||||
**28 of 53** admin-web API routes have **no auth check** at all. This includes sensitive endpoints:
|
||||
|
||||
| Route | Risk |
|
||||
| -------------------------------------- | -------------------------------------------- |
|
||||
| `/api/ops/secrets` (GET/POST) | **Lists and writes Azure Key Vault secrets** |
|
||||
| `/api/ops/secrets/[name]` (GET/DELETE) | **Reads and deletes individual secrets** |
|
||||
| `/api/telemetry/*` (7 routes) | Queries/mutates telemetry data |
|
||||
| `/api/themes/*` (4 routes) | Modifies platform themes |
|
||||
| `/api/tokens/*` (2 routes) | API token management |
|
||||
| `/api/stripe/config` | Stripe configuration |
|
||||
|
||||
The secrets routes are the most critical — they interact directly with Azure Key Vault with **zero authentication**. Anyone who can reach the admin dashboard can read/write/delete all production secrets.
|
||||
|
||||
**Fix:** Add Next.js edge middleware (`middleware.ts`) that validates JWT on all `/api/*` routes except `/api/auth/login` and `/api/auth/forgot-password`. This is a single file, ~30 lines, that protects all routes uniformly.
|
||||
|
||||
### 2. User-dashboard API routes missing auth — HIGH
|
||||
|
||||
**31 of 36** user-dashboard API routes lack explicit auth checks. Includes:
|
||||
|
||||
- `/api/payments`, `/api/subscription` — billing operations
|
||||
- `/api/sessions/*` — user session data
|
||||
- `/api/stripe/portal`, `/api/stripe/config`
|
||||
- `/api/transcripts` — user transcript data
|
||||
- `/api/dashboard` — dashboard aggregations
|
||||
|
||||
**Fix:** Same middleware.ts pattern. Protect all `/api/*` except `/api/auth/*`.
|
||||
|
||||
### 3. CORS defaults to wildcard (`origin: true`) — MEDIUM
|
||||
|
||||
In `@bytelyst/fastify-core`, when `CORS_ORIGIN` env var is not set, CORS defaults to `origin: true` (allow all origins). In production, if someone forgets to set this variable, any website can make authenticated requests to platform-service.
|
||||
|
||||
```typescript
|
||||
// packages/fastify-core/src/create-app.ts:34
|
||||
const origin = corsOrigin ? corsOrigin.split(',').map(o => o.trim()) : true;
|
||||
```
|
||||
|
||||
**Fix:** Default to `false` (deny all) when `CORS_ORIGIN` is unset. Require explicit opt-in.
|
||||
|
||||
### 4. No CSP / security headers on MindLyst-web and ChronoMind-web — MEDIUM
|
||||
|
||||
Admin-web, tracker-web, and user-dashboard all have security headers in `next.config.ts`. MindLyst-web and ChronoMind-web have **zero** security headers configured — no CSP, no X-Frame-Options, no HSTS.
|
||||
|
||||
**Fix:** Copy the security headers block from admin-web's `next.config.ts` to both apps.
|
||||
|
||||
---
|
||||
|
||||
## P1 — Reliability / Crashes
|
||||
|
||||
### 5. 21 API routes with no try/catch — crash on any DB/network error — HIGH
|
||||
|
||||
| Dashboard | Total Routes | Without try/catch | % Unprotected |
|
||||
| -------------- | ------------ | ----------------- | ------------- |
|
||||
| user-dashboard | 36 | 12 | 33% |
|
||||
| mindlyst-web | 33 | 6 | 18% |
|
||||
| admin-web | 53 | 3 | 6% |
|
||||
|
||||
Unprotected routes include payments, subscriptions, sessions, transcripts, SSO callbacks. Any Cosmos timeout or network blip returns an unhandled 500 with a stack trace (information leak + poor UX).
|
||||
|
||||
**Fix:** Wrap each handler body in try/catch, or create a shared `withErrorHandler()` HOF that all API routes use.
|
||||
|
||||
### 6. No retry / timeout / circuit breaker in `@bytelyst/api-client` — HIGH
|
||||
|
||||
The shared `createApiClient()` has **no timeout**, **no retry logic**, and **no circuit breaker**. Every consumer (6 dashboards + mobile apps) inherits this:
|
||||
|
||||
- A single Cosmos slowdown cascades to all dashboards
|
||||
- Network blips cause immediate failures with no recovery
|
||||
- No AbortController timeout — requests can hang indefinitely
|
||||
|
||||
**Fix:** Add `timeout` option (default 10s via AbortController), `retries` option (default 2 for GET, 0 for mutations), and exponential backoff.
|
||||
|
||||
### 7. No graceful shutdown in Fastify services — MEDIUM
|
||||
|
||||
`startService()` in `@bytelyst/fastify-core` calls `process.exit(1)` on startup failure but has **no SIGTERM/SIGINT handler**. In Docker/K8s, this means:
|
||||
|
||||
- In-flight requests are dropped on deploy
|
||||
- Database connections not cleaned up
|
||||
- Potential data corruption on writes
|
||||
|
||||
**Fix:** Add to `startService()`:
|
||||
|
||||
```typescript
|
||||
for (const signal of ['SIGTERM', 'SIGINT']) {
|
||||
process.on(signal, async () => {
|
||||
app.log.info(`Received ${signal}, shutting down gracefully`);
|
||||
await app.close();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 8. No `error.tsx` in any Next.js app — MEDIUM
|
||||
|
||||
**Zero** of the 5 Next.js apps have an `error.tsx` file. When a React component throws during render, users see a blank white page (or the browser's default error). This is the #1 source of "the app is broken" reports.
|
||||
|
||||
**Fix:** Add `error.tsx` to each app's root `app/` directory — ~20 lines showing a "Something went wrong" UI with a retry button.
|
||||
|
||||
### 9. No `not-found.tsx` in 4 of 5 Next.js apps — LOW
|
||||
|
||||
Only MindLyst has a custom 404. The other 4 apps show Next.js's default 404 page.
|
||||
|
||||
**Fix:** Add `not-found.tsx` to each app.
|
||||
|
||||
### 10. Missing `loading.tsx` in 4 of 5 dashboards — LOW
|
||||
|
||||
Only admin-web has a `loading.tsx`. Other dashboards show no loading indicator during route transitions.
|
||||
|
||||
**Fix:** Add a skeleton loader `loading.tsx` to each app's layout group.
|
||||
|
||||
---
|
||||
|
||||
## P2 — Maintainability / Code Duplication
|
||||
|
||||
### 11. MindLyst-web uses raw `@azure/cosmos` v3 instead of `@bytelyst/cosmos` — HIGH
|
||||
|
||||
MindLyst-web has its own 86-line `cosmos.ts` with a hand-rolled Cosmos client using **v3.17.3** of the SDK. Every other dashboard uses `@bytelyst/cosmos` (which uses v4.x). This means:
|
||||
|
||||
- **Different API surface** (v3 vs v4 have breaking changes)
|
||||
- **No container registry** (MindLyst manages containers ad-hoc)
|
||||
- **Hardcoded `PRODUCT_ID = "mindlyst"`** instead of using `@bytelyst/config`
|
||||
- Bug fixes to the shared package don't reach MindLyst
|
||||
|
||||
**Fix:** Migrate MindLyst-web to `@bytelyst/cosmos` + `@bytelyst/config`. Replace the 86-line file with ~40 lines matching user-dashboard pattern.
|
||||
|
||||
### 12. MindLyst billing-client uses raw fetch instead of `@bytelyst/api-client` — MEDIUM
|
||||
|
||||
MindLyst's `billing-client.ts` has its own `billingFetch()` wrapper with hardcoded headers, token management, and error handling. User-dashboard's `billing-client.ts` correctly uses `createApiClient()`.
|
||||
|
||||
**Fix:** Rewrite MindLyst's billing-client to use `@bytelyst/api-client` like every other dashboard.
|
||||
|
||||
### 13. Duplicate `feature-flags.ts` across repos — MEDIUM
|
||||
|
||||
`feature-flags.ts` is nearly identical in user-dashboard and MindLyst-web (only differs by `PRODUCT_ID` fallback). Both have their own raw `fetch()` calls.
|
||||
|
||||
**Fix:** Either add a `createFeatureFlagClient()` to `@bytelyst/api-client` or create a thin `@bytelyst/feature-flags` package.
|
||||
|
||||
### 14. 5 copies of `product-config.ts` with identical boilerplate — LOW
|
||||
|
||||
Every service and dashboard has its own `product-config.ts` that wraps `@bytelyst/config`. The files are 5-10 lines of identical code.
|
||||
|
||||
**Fix:** Consider making `@bytelyst/config` export a ready-to-use `PRODUCT_ID` constant (lazy-loaded) to eliminate the wrapper files.
|
||||
|
||||
### 15. 4 copies of `docker-prep.sh` across repos — LOW
|
||||
|
||||
Each consumer repo has its own `docker-prep.sh` script (22-45 lines each) for packing `@bytelyst/*` tarballs. They diverge in package lists and paths.
|
||||
|
||||
**Fix:** Move the canonical script to `learning_ai_common_plat/scripts/docker-prep.sh` and have consumer repos call it, or use a shared Makefile target.
|
||||
|
||||
### 16. Duplicate `error-boundary.tsx` in admin + user dashboards — LOW
|
||||
|
||||
Nearly identical class component (differs by 3 whitespace lines). Should be in a shared UI package.
|
||||
|
||||
**Fix:** Move to `@bytelyst/react-auth` (or create `@bytelyst/react-ui`) and re-export.
|
||||
|
||||
### 17. Zod v4 vs v3 conflict — ChronoMind uses Zod 4 — MEDIUM
|
||||
|
||||
ChronoMind-web uses `zod: "^4.3.6"` while **every other package and service** uses Zod 3.x. The `@bytelyst/*` packages (config, events) all depend on Zod 3. This means:
|
||||
|
||||
- ChronoMind cannot use `@bytelyst/config` or any Zod-dependent shared package without runtime conflicts
|
||||
- Schema types are incompatible between Zod 3 and Zod 4
|
||||
|
||||
**Fix:** Either downgrade ChronoMind to Zod 3 to match ecosystem, or upgrade the entire ecosystem to Zod 4 (breaking change for all services).
|
||||
|
||||
### 18. TypeScript version skew — MINOR
|
||||
|
||||
| Range | Repos |
|
||||
| --------------------------- | ------------------------------------------------------ |
|
||||
| `^5` (loose) | admin-web, tracker-web, user-dashboard, chronomind-web |
|
||||
| `^5.7.0` – `^5.7.3` | common-plat root, services |
|
||||
| `~5.9.2` – `5.9.3` (pinned) | NomGap, MindLyst-web |
|
||||
|
||||
MindLyst pins `5.9.3` (exact) while NomGap uses `~5.9.2`. The common-plat root uses `^5.7.0`. This can cause type-checking discrepancies.
|
||||
|
||||
**Fix:** Standardize all repos to `^5.9.0` in a coordinated PR.
|
||||
|
||||
---
|
||||
|
||||
## P3 — Operational / DX
|
||||
|
||||
### 19. 10 disabled CI workflows — no automated quality gate — HIGH
|
||||
|
||||
| Repo | Disabled Workflows |
|
||||
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| learning_voice_ai_agent | 7 (ci.yml, ci-python-backend, ci-admin-dashboard, ci-user-dashboard, ci-tracker-dashboard, churn-alert, release) |
|
||||
| learning_ai_common_plat | 2 (ci.yml, trigger-consumers) |
|
||||
| learning_multimodal_memory_agents | 1 (ci.yml) |
|
||||
|
||||
Only ChronoMind and NomGap have active CI. The **three largest repos** have zero automated CI on push/PR. Regressions go undetected until manual testing.
|
||||
|
||||
**Fix:** Re-enable CI workflows. Even a minimal `typecheck + test` workflow on PR catches most regressions.
|
||||
|
||||
### 20. Zero `x-request-id` propagation in dashboard API routes — MEDIUM
|
||||
|
||||
**All 122 dashboard API routes** (53 admin + 36 user + 33 mindlyst) lack `x-request-id` propagation. When a dashboard API route calls platform-service, there's no way to correlate the request across services in logs.
|
||||
|
||||
**Fix:** Add a shared middleware or utility that auto-generates and forwards `x-request-id` from incoming request to all outgoing `fetch()` / `createApiClient()` calls.
|
||||
|
||||
### 21. 80+ unique env vars with no central registry — MEDIUM
|
||||
|
||||
Across all services and dashboards, there are **80+ unique `process.env.*` references**. There's no single document listing which vars each app needs, their valid values, and which are required vs optional.
|
||||
|
||||
**Fix:** Create an `ENV_REGISTRY.md` in common-plat docs, auto-generated by scanning all repos. Each entry: var name, required/optional, which apps use it, description.
|
||||
|
||||
### 22. No `middleware.ts` in any Next.js app — MEDIUM
|
||||
|
||||
None of the 5 Next.js apps have a `middleware.ts` file. This means:
|
||||
|
||||
- No edge-level auth protection (each API route must check auth individually — and most don't)
|
||||
- No redirect logic for unauthenticated users
|
||||
- No request logging at the edge
|
||||
|
||||
**Fix:** Add `middleware.ts` to admin-web, user-dashboard, and mindlyst-web. Tracker-web may not need it if it's mostly public.
|
||||
|
||||
### 23. No `instrumentation.ts` in ChronoMind-web — LOW
|
||||
|
||||
All other Next.js apps have `instrumentation.ts` for AKV secret resolution at startup. ChronoMind-web is missing it — secrets won't resolve from Key Vault.
|
||||
|
||||
**Fix:** Add `instrumentation.ts` following the pattern from user-dashboard-web.
|
||||
|
||||
### 24. Package manager split: pnpm (common-plat) vs npm (all consumers) — INFO
|
||||
|
||||
Common-plat uses pnpm workspace. All 4 consumer repos use npm with `package-lock.json`. This isn't a bug but creates friction:
|
||||
|
||||
- Contributors must know which tool to use where
|
||||
- `file:` refs from npm to pnpm workspace packages require `pnpm build` first
|
||||
- Lock file formats differ
|
||||
|
||||
**Recommendation:** Document this clearly. Long-term, consider migrating consumers to pnpm or publishing `@bytelyst/*` to a private registry.
|
||||
|
||||
---
|
||||
|
||||
## Priority Action Plan
|
||||
|
||||
### Sprint 1 — Security (1-2 days)
|
||||
|
||||
1. Add `middleware.ts` to admin-web (blocks unauthenticated access to secrets, telemetry, themes, tokens)
|
||||
2. Add `middleware.ts` to user-dashboard (blocks unauthenticated access to payments, sessions, transcripts)
|
||||
3. Fix CORS default to deny-all when `CORS_ORIGIN` is unset
|
||||
4. Add security headers to MindLyst-web and ChronoMind-web `next.config.ts`
|
||||
|
||||
### Sprint 2 — Reliability (2-3 days)
|
||||
|
||||
5. Add `error.tsx` + `not-found.tsx` to all 5 Next.js apps
|
||||
6. Add try/catch to all 21 unprotected API routes (or create shared error handler HOF)
|
||||
7. Add timeout + retry to `@bytelyst/api-client`
|
||||
8. Add graceful shutdown to `@bytelyst/fastify-core`
|
||||
|
||||
### Sprint 3 — Deduplication (2-3 days)
|
||||
|
||||
9. Migrate MindLyst-web from raw `@azure/cosmos` v3 → `@bytelyst/cosmos` v4
|
||||
10. Migrate MindLyst billing-client to `@bytelyst/api-client`
|
||||
11. Consolidate `feature-flags.ts` into shared package
|
||||
12. Resolve Zod v3/v4 conflict (ChronoMind)
|
||||
|
||||
### Sprint 4 — Ops & DX (1-2 days)
|
||||
|
||||
13. Re-enable CI on the 3 largest repos (even minimal typecheck + test)
|
||||
14. Add `x-request-id` propagation to dashboard API layer
|
||||
15. Standardize TypeScript version across all repos
|
||||
16. Create `ENV_REGISTRY.md` with all env vars documented
|
||||
|
||||
---
|
||||
|
||||
## Items Confirmed Correct (Not Anti-Patterns)
|
||||
|
||||
| # | Item | Status |
|
||||
| --- | ---------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
||||
| A | Tracker-web has no direct Cosmos access — uses `@bytelyst/api-client` → platform-service | Correct by design |
|
||||
| B | MindLyst native (KMP) has no Azure wiring — all Azure goes through web API routes | Correct by design |
|
||||
| C | ChronoMind/NomGap have no direct Azure SDK usage — REST API only | Correct by design |
|
||||
| D | `console.log` in `@bytelyst/logger` | Intentional (it IS the logger) |
|
||||
| E | `console.log` in `design-tokens/generate.ts` | Build script, not production code |
|
||||
| F | `print()` in Python `cli_output.py` | Intentional CLI output (has `noqa` comment) |
|
||||
| G | `as any` in `api-client/client.ts:44` | Single occurrence, casting Headers — acceptable |
|
||||
Loading…
Reference in New Issue
Block a user