diff --git a/.windsurf/workflows/implement-shared-packages.md b/.windsurf/workflows/implement-shared-packages.md new file mode 100644 index 00000000..84b7f430 --- /dev/null +++ b/.windsurf/workflows/implement-shared-packages.md @@ -0,0 +1,298 @@ +--- +description: Implement all 9 shared @bytelyst/* client packages from the SHARED_CLIENT_PACKAGES_ROADMAP +--- + +# Implement Shared @bytelyst/\* Client Packages + +## Pre-requisites + +// turbo + +1. Read the full roadmap doc: + +```bash +cat docs/roadmaps/SHARED_CLIENT_PACKAGES_ROADMAP.md +``` + +// turbo 2. Study the canonical reference package structure: + +```bash +cat packages/feature-flag-client/package.json packages/feature-flag-client/tsconfig.json packages/feature-flag-client/src/index.ts packages/feature-flag-client/src/types.ts packages/feature-flag-client/src/client.ts packages/feature-flag-client/src/client.test.ts +``` + +## Critical Rules + +- **Package manager is pnpm** — NEVER use npm +- **ESM everywhere** — `"type": "module"`, `.js` extensions in all imports +- **No Node.js deps** — use `globalThis.fetch`, not node-fetch +- **No React/RN deps** — pure TypeScript only +- **Factory pattern** for API clients: `createXxxClient(config)` returning an interface +- **No `console.log`**, no `any` type +- **Every request to platform-service MUST include headers:** + - `x-product-id` (from config.productId) + - `Authorization: Bearer ` (from config.getAccessToken()) +- **Tests in `src/client.test.ts`** (co-located, same as `@bytelyst/feature-flag-client`) +- **tsconfig.json must include `"lib": ["ES2022", "DOM"]`** and `"exclude": ["src/**/*.test.ts"]` +- **All API interfaces, backend types, and ⚠️ warnings are in the roadmap doc** — follow them exactly + +## Implementation (commit after each package) + +### Package 1: `packages/referral-client/` + +Create `packages/referral-client/` with: + +- `package.json` — `@bytelyst/referral-client`, version 0.1.0 +- `tsconfig.json` — extends ../../tsconfig.base.json, lib: ["ES2022", "DOM"] +- `src/types.ts` — ReferralDoc, ReferralClientConfig (from roadmap doc) +- `src/client.ts` — `createReferralClient(config)` factory +- `src/index.ts` — re-exports +- `src/client.test.ts` — 8+ Vitest tests + +⚠️ There is NO `/referrals/apply` endpoint. Use `PUT /referrals/:id` for status updates. + +// turbo 3. Verify package 1: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && pnpm --filter @bytelyst/referral-client build && pnpm --filter @bytelyst/referral-client test +``` + +4. Commit package 1: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && git add packages/referral-client/ && git commit -m "feat(referral-client): add @bytelyst/referral-client shared package" +``` + +### Package 2: `packages/subscription-client/` + +Create `packages/subscription-client/` with: + +- `package.json` — `@bytelyst/subscription-client`, version 0.1.0 +- `tsconfig.json` — extends ../../tsconfig.base.json, lib: ["ES2022", "DOM"] +- `src/types.ts` — SubscriptionDoc, PlanConfig, SubscriptionClientConfig (from roadmap doc) +- `src/client.ts` — `createSubscriptionClient(config)` factory with caching +- `src/index.ts` — re-exports +- `src/client.test.ts` — 10+ Vitest tests + +⚠️ `GET /plans` returns `{ plans: [...] }` — unwrap `.plans` in client. +⚠️ Routes use `:userId` not `:id` — use `config.userId`. +⚠️ `PlanConfig` fields `words`, `dictations`, `tokens` are LysnrAI-specific legacy. Use `features: string[]` for entitlements. + +// turbo 5. Verify package 2: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && pnpm --filter @bytelyst/subscription-client build && pnpm --filter @bytelyst/subscription-client test +``` + +6. Commit package 2: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && git add packages/subscription-client/ && git commit -m "feat(subscription-client): add @bytelyst/subscription-client shared package" +``` + +### Package 3: `packages/celebrations/` + +Create `packages/celebrations/` with: + +- `package.json` — `@bytelyst/celebrations`, version 0.1.0 +- `tsconfig.json` — extends ../../tsconfig.base.json, lib: ["ES2022", "DOM"] +- `src/types.ts` — CelebrationTrigger, Celebration, CelebrationConfig +- `src/client.ts` — `createCelebrationEngine(config?)` factory +- `src/index.ts` — re-exports +- `src/client.test.ts` — 8+ Vitest tests + +Pure TS, no backend. Products register custom triggers via `customTriggers` config. Messages ALWAYS positive. + +// turbo 7. Verify package 3: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && pnpm --filter @bytelyst/celebrations build && pnpm --filter @bytelyst/celebrations test +``` + +8. Commit package 3: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && git add packages/celebrations/ && git commit -m "feat(celebrations): add @bytelyst/celebrations shared package" +``` + +### Package 4: `packages/gentle-notifications/` + +Create `packages/gentle-notifications/` with: + +- `package.json` — `@bytelyst/gentle-notifications`, version 0.1.0 +- `tsconfig.json` — extends ../../tsconfig.base.json, lib: ["ES2022", "DOM"] +- `src/types.ts` — GentleNotificationConfig, GentleMessage +- `src/client.ts` — `createGentleNotificationEngine(config?)` factory + `FORBIDDEN_PHRASES` export +- `src/index.ts` — re-exports +- `src/client.test.ts` — 8+ Vitest tests + +Pure TS, no backend. Export `FORBIDDEN_PHRASES` constant. Support `registerMessages()` for product-specific pools. + +// turbo 9. Verify package 4: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && pnpm --filter @bytelyst/gentle-notifications build && pnpm --filter @bytelyst/gentle-notifications test +``` + +10. Commit package 4: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && git add packages/gentle-notifications/ && git commit -m "feat(gentle-notifications): add @bytelyst/gentle-notifications shared package" +``` + +### Package 5: `packages/accessibility/` + +Create `packages/accessibility/` with: + +- `package.json` — `@bytelyst/accessibility`, version 0.1.0 +- `tsconfig.json` — extends ../../tsconfig.base.json, lib: ["ES2022", "DOM"] +- `src/types.ts` — A11yProps interface +- `src/client.ts` — label generator functions (buttonLabel, timerLabel, progressLabel, etc.) +- `src/index.ts` — re-exports +- `src/client.test.ts` — 10+ Vitest tests + +Pure TS, no backend. Return A11yProps objects compatible with React Native accessibilityLabel/Role. + +// turbo 11. Verify package 5: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && pnpm --filter @bytelyst/accessibility build && pnpm --filter @bytelyst/accessibility test +``` + +12. Commit package 5: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && git add packages/accessibility/ && git commit -m "feat(accessibility): add @bytelyst/accessibility shared package" +``` + +### Package 6: `packages/quick-actions/` + +Create `packages/quick-actions/` with: + +- `package.json` — `@bytelyst/quick-actions`, version 0.1.0 +- `tsconfig.json` — extends ../../tsconfig.base.json, lib: ["ES2022", "DOM"] +- `src/types.ts` — QuickAction, ProgressiveSection, SmartDefault +- `src/client.ts` — getVisibleSections, getAvailableActions, pickSmartDefault +- `src/index.ts` — re-exports +- `src/client.test.ts` — 6+ Vitest tests + +Pure TS, no backend. + +// turbo 13. Verify package 6: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && pnpm --filter @bytelyst/quick-actions build && pnpm --filter @bytelyst/quick-actions test +``` + +14. Commit package 6: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && git add packages/quick-actions/ && git commit -m "feat(quick-actions): add @bytelyst/quick-actions shared package" +``` + +### Package 7: `packages/time-references/` + +Create `packages/time-references/` with: + +- `package.json` — `@bytelyst/time-references`, version 0.1.0 +- `tsconfig.json` — extends ../../tsconfig.base.json, lib: ["ES2022", "DOM"] +- `src/types.ts` — TimeReference interface +- `src/client.ts` — getTimeReference, getEpisodeComparison, getEncouragingMessage, registerReferences +- `src/index.ts` — re-exports +- `src/client.test.ts` — 6+ Vitest tests + +Pure TS, no backend. Support `registerReferences()` for custom reference databases. + +// turbo 15. Verify package 7: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && pnpm --filter @bytelyst/time-references build && pnpm --filter @bytelyst/time-references test +``` + +16. Commit package 7: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && git add packages/time-references/ && git commit -m "feat(time-references): add @bytelyst/time-references shared package" +``` + +### Package 8: `packages/org-client/` + +Create `packages/org-client/` with: + +- `package.json` — `@bytelyst/org-client`, version 0.1.0 +- `tsconfig.json` — extends ../../tsconfig.base.json, lib: ["ES2022", "DOM"] +- `src/types.ts` — OrganizationDoc, WorkspaceDoc, MembershipDoc, LicenseDoc, OrgClientConfig +- `src/client.ts` — `createOrgClient(config)` factory +- `src/index.ts` — re-exports +- `src/client.test.ts` — 10+ Vitest tests + +⚠️ All org routes require admin JWT role (`super_admin` or `admin`). Regular user tokens get 403. +Covers orgs + workspaces + memberships + licenses (4 entity types). + +// turbo 17. Verify package 8: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && pnpm --filter @bytelyst/org-client build && pnpm --filter @bytelyst/org-client test +``` + +18. Commit package 8: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && git add packages/org-client/ && git commit -m "feat(org-client): add @bytelyst/org-client shared package" +``` + +### Package 9: `packages/marketplace-client/` + +Create `packages/marketplace-client/` with: + +- `package.json` — `@bytelyst/marketplace-client`, version 0.1.0 +- `tsconfig.json` — extends ../../tsconfig.base.json, lib: ["ES2022", "DOM"] +- `src/types.ts` — MarketplaceListingDoc, MarketplaceReviewDoc, MarketplaceInstallDoc, MarketplaceClientConfig, CreateListingInput +- `src/client.ts` — `createMarketplaceClient(config)` factory +- `src/index.ts` — re-exports +- `src/client.test.ts` — 10+ Vitest tests + +⚠️ NomGap's influencer.ts is product-specific. This is the GENERIC marketplace client. +Covers listings + reviews + installs + reports. + +// turbo 19. Verify package 9: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && pnpm --filter @bytelyst/marketplace-client build && pnpm --filter @bytelyst/marketplace-client test +``` + +20. Commit package 9: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && git add packages/marketplace-client/ && git commit -m "feat(marketplace-client): add @bytelyst/marketplace-client shared package" +``` + +## Final Verification + +// turbo 21. Run full workspace verification: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && pnpm build && pnpm test && pnpm typecheck +``` + +22. Update roadmap status — in `docs/roadmaps/SHARED_CLIENT_PACKAGES_ROADMAP.md`, change `> **Status:** Not Started` to `> **Status:** ✅ Complete — 9 packages, ~76 tests` + +23. Final commit: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && git add docs/roadmaps/SHARED_CLIENT_PACKAGES_ROADMAP.md && git commit -m "docs: mark shared client packages roadmap as complete" +``` + +24. Push all commits: + +```bash +cd /Users/sd9235/code/mygh/learning_ai_common_plat && git push origin main +``` + +## DO NOT + +- Do NOT modify any files outside `packages/` and the roadmap doc +- Do NOT create packages in any other directory +- Do NOT install external dependencies — these packages have zero deps +- Do NOT skip tests — every package must have passing Vitest tests +- Do NOT modify platform-service or any product repo +- Do NOT use npm — this is a pnpm workspace diff --git a/docs/roadmaps/SHARED_CLIENT_PACKAGES_ROADMAP.md b/docs/roadmaps/SHARED_CLIENT_PACKAGES_ROADMAP.md new file mode 100644 index 00000000..6e12622d --- /dev/null +++ b/docs/roadmaps/SHARED_CLIENT_PACKAGES_ROADMAP.md @@ -0,0 +1,923 @@ +# Shared @bytelyst/\* Client Packages — Extraction from Product Repos + +> **Status:** Not Started +> **Priority:** High — eliminates duplication across 9+ products +> **Estimated effort:** 8–10 sessions +> **Owner:** Cascade agent working in `learning_ai_common_plat` + +--- + +## Context & Motivation + +During NomGap (learning_ai_fastgap) Phase 5 buildout, we created several pure-TS engine modules that are **product-agnostic** and should live in `learning_ai_common_plat/packages/` as shared `@bytelyst/*` packages. The same patterns apply (or will apply) to all 9+ ByteLyst products: LysnrAI, MindLyst, ChronoMind, JarvisJr, NomGap, PeakPulse, FlowMonk, NoteLett, ActionTrail. + +Additionally, platform-service already has backend modules for referrals, subscriptions, plans, orgs, and licenses — but **no corresponding client packages** exist for products to consume them. Each product has been hand-rolling its own fetch calls or building bespoke modules. + +### What already exists in platform-service (port 4003) + +> **IMPORTANT:** All platform-service endpoints require an `x-product-id` header (read by `getRequestProductId(req)`). Client packages MUST send this header on every request. + +| Backend Module | Exact Endpoints | Auth | Cosmos Container | +| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| `modules/referrals/` | `GET /referrals`, `POST /referrals`, `PUT /referrals/:id`, `GET /referrals/stats`, `GET /referrals/by-referrer/:referrerId`, `GET /referrals/by-email/:email` | JWT | `referrals` (`/referrerId`) | +| `modules/subscriptions/` | `GET /subscriptions/:userId`, `POST /subscriptions`, `PUT /subscriptions/:userId` (NOTE: keyed by userId, not id) | JWT | `subscriptions` (`/userId`) | +| `modules/plans/` | `GET /plans` → `{ plans: [...] }`, `GET /plans/:name`, `POST /plans`, `PUT /plans/:id`, `POST /plans/seed` | JWT | `plans` (`/productId`) | +| `modules/orgs/` | `GET/POST /orgs`, `GET/PATCH /orgs/:id`, workspaces: `GET/POST /orgs/:id/workspaces`, `PATCH /orgs/:id/workspaces/:workspaceId`, memberships: `GET/POST /orgs/:id/memberships`, `PATCH /orgs/:id/memberships/:membershipId` | **Admin-only** (`super_admin` or `admin` JWT role) | `organizations` (`/productId`), `workspaces` (`/orgId`), `memberships` (`/orgId`) | +| `modules/licenses/` | Generate, activate, deactivate, list by user | JWT | `licenses` (`/userId`) | +| `modules/marketplace/` | Listings CRUD + certification + reviews + installs + reports + votes (6 entity types, 309 lines of types) | JWT (some public) | `marketplace_listings` (`/productId`), `marketplace_reviews` (`/listingId`), `marketplace_installs` (`/userId`), etc. | + +### What NomGap built as product-specific (should be shared) + +| NomGap File | What It Does | Should Become | +| --------------------------------- | --------------------------------------------------------------------- | -------------------------------- | +| `src/lib/referral.ts` | Referral code share links, apply codes, stats | `@bytelyst/referral-client` | +| `src/lib/monetization.ts` | Plan definitions, Pro feature gate, trial, restore | `@bytelyst/monetization` | +| `src/lib/b2b-wellness.ts` | Org dashboard, team challenges, licenses, SSO | `@bytelyst/org-client` | +| `src/lib/influencer.ts` | Branded challenges, affiliate tracking, share cards | `@bytelyst/marketplace-client` | +| `src/lib/celebrations.ts` | 14 celebration triggers, haptics, confetti, sounds, positive messages | `@bytelyst/celebrations` | +| `src/lib/gentle-notifications.ts` | ND-friendly messaging, adaptive frequency, forbidden phrases | `@bytelyst/gentle-notifications` | +| `src/lib/accessibility.ts` | VoiceOver/TalkBack label generators for common UI patterns | `@bytelyst/accessibility` | +| `src/lib/quick-actions.ts` | Progressive disclosure, smart defaults, action definitions | `@bytelyst/quick-actions` | +| `src/lib/time-blindness.ts` | Familiar duration references ("About as long as a movie") | `@bytelyst/time-references` | + +--- + +## Packages to Create (Priority Order) + +### Tier 1 — Highest Leverage (backend already exists, every product needs these) + +#### 1. `@bytelyst/referral-client` + +**Purpose:** Browser/React Native-safe client for platform-service `/referrals/*` endpoints. + +**Reference implementation:** `learning_ai_fastgap/src/lib/referral.ts` (73 lines) + +**Backend already exists:** `platform-service/src/modules/referrals/` — full CRUD + webhook dispatch + Zod schemas. + +**Backend types (from `referrals/types.ts`):** + +```ts +interface ReferralDoc { + id: string; + productId: string; + referrerId: string; + referrerEmail: string; + referredUserId: string | null; + referredEmail: string; + status: 'pending' | 'signed_up' | 'subscribed' | 'rewarded'; + referrerRewardTokens: number; + referredRewardTokens: number; + referrerRewarded: boolean; + referredRewarded: boolean; + createdAt: string; + completedAt: string | null; +} +``` + +**Public API to implement:** + +```ts +export interface ReferralClientConfig { + baseUrl: string; // e.g. 'http://localhost:4003/api' + productId: string; // sent as x-product-id header on every request + getAccessToken: () => string | null; + // Defaults used when creating referrals (so callers only provide referredEmail) + defaultRewardTokens?: { referrer: number; referred: number }; // default: 1000 / 500 +} + +export interface ReferralClient { + // Core CRUD (delegates to platform-service) + listMyReferrals(referrerId: string): Promise<{ referrals: ReferralDoc[]; count: number }>; + getReferralStats(): Promise<{ total: number; completed: number; rewarded: number }>; + createReferral(input: { + referrerId: string; // from auth context + referrerEmail: string; // from auth context + referredEmail: string; // user input + }): Promise; + // NOTE: There is NO /referrals/apply endpoint in platform-service. + // "Applying" a referral = calling PUT /referrals/:id to update status to 'signed_up'. + // The client should expose this as a semantic method: + updateReferralStatus( + id: string, + referrerId: string, + status: ReferralDoc['status'] + ): Promise; + getByEmail(email: string): Promise; + + // Client-side helpers (pure TS, no network) + buildShareLink(code: string): string; + buildShareMessage(code: string, productName: string): string; + calculateEarnedDays(conversions: number, daysPerReferral?: number): number; +} + +export function createReferralClient(config: ReferralClientConfig): ReferralClient; +``` + +**⚠️ Backend gap:** Platform-service has no "apply referral code" endpoint. The flow for a referred user is: + +1. `getByEmail(referredEmail)` → finds existing pending referral +2. `updateReferralStatus(id, referrerId, 'signed_up')` → marks as completed +3. Backend fires webhook via `dispatchReferralStatusChanged()` + +If a single "apply code" endpoint is needed, a new `POST /referrals/apply` route should be added to platform-service first. + +**Pattern:** Follow `@bytelyst/feature-flag-client` exactly — `createXxxClient()` factory, `types.ts` + `client.ts` + `index.ts`, `globalThis.fetch`, no Node.js deps. Every request MUST include `x-product-id` and `Authorization: Bearer ` headers. + +**Tests:** 8+ Vitest tests — create, listMyReferrals, stats, updateStatus, getByEmail, buildShareLink, buildShareMessage, calculateEarnedDays, error handling. + +--- + +#### 2. `@bytelyst/subscription-client` + +**Purpose:** Client for platform-service `/subscriptions/*` + `/plans/*` — check entitlements, manage trials, restore purchases. + +**Reference implementation:** `learning_ai_fastgap/src/lib/monetization.ts` (200 lines) + +**Backend already exists:** `platform-service/src/modules/subscriptions/` + `platform-service/src/modules/plans/` + +**Backend types (from `subscriptions/types.ts`):** + +```ts +interface SubscriptionDoc { + id: string; + productId: string; + userId: string; + plan: 'free' | 'pro' | 'enterprise'; + status: 'active' | 'cancelled' | 'past_due' | 'trialing'; + currentPeriodStart: string; + currentPeriodEnd: string; + cancelAtPeriodEnd: boolean; + monthlyPrice: number; + tokensIncluded: number; + tokensUsed: number; + stripeCustomerId?: string; + stripeSubscriptionId?: string; + createdAt: string; + updatedAt: string; +} +``` + +**Backend types (from `plans/types.ts`):** + +```ts +interface PlanConfig { + id: string; + productId: string; + name: string; + displayName: string; + price: number; + tokens: number; + words: number; + dictations: number; + features: string[]; + stripePriceId?: string; + active: boolean; + createdAt: string; + updatedAt: string; +} +``` + +**Public API to implement:** + +```ts +export interface SubscriptionClientConfig { + baseUrl: string; + productId: string; // sent as x-product-id header on every request + userId: string; // REQUIRED — subscription routes are keyed by userId, not id + getAccessToken: () => string | null; + storage?: { getItem(k: string): string | null; setItem(k: string, v: string): void }; +} + +export interface SubscriptionClient { + // Server-authoritative checks (all hit platform-service) + getMySubscription(): Promise; + // NOTE: GET /plans returns { plans: [...] } — client must unwrap .plans + getPlans(): Promise; + startTrial(planName?: string): Promise; + cancelSubscription(): Promise; + // NOTE: PUT /subscriptions/:userId (not :id) — use config.userId + updateSubscription(updates: Partial): Promise; + + // Client-side helpers (cached, offline-safe) + isPro(): boolean; // cached: plan !== 'free' && status === 'active' | 'trialing' + isTrialing(): boolean; // cached: status === 'trialing' + hasFeature(feature: string): boolean; // cached: looks up PlanConfig.features.includes(feature) + daysRemaining(): number | null; // cached: days until currentPeriodEnd + getCachedSubscription(): SubscriptionDoc | null; + getCachedPlans(): PlanConfig[]; + refresh(): Promise; // re-fetch from server + update cache +} + +export function createSubscriptionClient(config: SubscriptionClientConfig): SubscriptionClient; +``` + +**⚠️ Backend gaps & notes:** + +- **No restore-purchase endpoint** — Platform-service has no store receipt verification. `restorePurchase()` was removed from the client API. To support IAP restore, either: (a) add a `POST /subscriptions/restore` endpoint to platform-service that verifies Apple/Google receipts, or (b) products verify receipts client-side and call `updateSubscription()` with the new status. Recommend (a) as a future platform-service enhancement. +- **`PlanConfig.features` is the entitlement source** — `hasFeature('ai_coaching')` checks `PlanConfig.features.includes('ai_coaching')` from the cached plan matching the user's subscription tier. Products define their own feature strings. +- **`PlanConfig` fields `words`, `dictations`, `tokens`** are LysnrAI-specific legacy fields. Other products should ignore them and rely on `features: string[]` for entitlement checks. The `PlanConfig` type should be treated as extensible. +- **Subscription routes use `:userId` not `:id`** — `GET /subscriptions/:userId` and `PUT /subscriptions/:userId`. The client must use `config.userId` for these calls. + +**Key design:** Cache subscription + plans in `storage` for offline reads. `isPro()`, `hasFeature()`, `isTrialing()` read from cache (never block on network). `refresh()` fetches from server and updates cache. + +**Tests:** 10+ Vitest tests — getMySubscription, getPlans (unwraps .plans), startTrial, isPro, hasFeature (with features array), isTrialing, daysRemaining, cache persistence, refresh, error handling. + +--- + +#### 3. `@bytelyst/celebrations` + +**Purpose:** Product-agnostic celebration engine — milestone triggers, haptic configs, confetti, sounds, positive reinforcement messages. + +**Reference implementation:** `learning_ai_fastgap/src/lib/celebrations.ts` (190 lines) + +**No backend needed** — pure client-side TS. + +**Public API to implement:** + +```ts +export type CelebrationTrigger = + | 'task_completed' + | 'streak_continued' + | 'streak_milestone' + | 'achievement_unlocked' + | 'level_up' + | 'personal_best' + | 'milestone_reached' + | 'goal_completed' + | 'first_action' + | 'halfway' + | 'session_completed' + | 'session_started'; + +export interface Celebration { + id: string; + title: string; + body: string; + emoji: string; + hapticType: 'light' | 'medium' | 'heavy' | 'success' | 'warning'; + confetti: boolean; + sound: 'chime' | 'success' | 'level_up' | 'none'; +} + +export interface CelebrationConfig { + // Products register their own trigger→message mappings + customTriggers?: Record; +} + +export function createCelebrationEngine(config?: CelebrationConfig): { + getCelebration(trigger: CelebrationTrigger | string): Celebration; + getTimedCelebrations(elapsedMs: number, targetMs: number, shownIds: Set): Celebration[]; + isPersonalBest(current: number, previous: number): boolean; + getPositiveMessage(progressPercent: number): string; + // For early breaks / incomplete actions — NEVER negative + getPositiveIncompleteMessage(progressPercent: number): string; +}; +``` + +**Key design:** Products register custom triggers via `customTriggers` config. Default triggers are universal (streaks, achievements, levels). Messages are ALWAYS positive — never guilt-inducing. + +**Tests:** 8+ tests — all trigger types, custom triggers, timed celebrations, personal best, positive messages, incomplete messages. + +--- + +#### 4. `@bytelyst/gentle-notifications` + +**Purpose:** Neurodivergent-friendly notification messaging system — encouraging tone, adaptive frequency, forbidden phrases. + +**Reference implementation:** `learning_ai_fastgap/src/lib/gentle-notifications.ts` (120 lines) + +**No backend needed** — pure client-side TS. + +**Public API to implement:** + +```ts +export interface GentleNotificationConfig { + maxPerHour: number; + tone: 'encouraging' | 'neutral' | 'minimal'; + adaptiveFrequency: boolean; + dismissCount: number; + suppressThreshold: number; +} + +export interface GentleMessage { + title: string; + body: string; + tone: 'encouraging' | 'neutral' | 'minimal'; +} + +export function createGentleNotificationEngine(config?: Partial): { + getDefaultConfig(): GentleNotificationConfig; + getMessage(type: string, config?: GentleNotificationConfig): GentleMessage; + shouldSuppress(config: GentleNotificationConfig): boolean; + recordDismissal(config: GentleNotificationConfig): GentleNotificationConfig; + resetDismissals(config: GentleNotificationConfig): GentleNotificationConfig; + + // Products register their own message pools + registerMessages(type: string, messages: GentleMessage[]): void; + + // Ecosystem-wide forbidden phrases — NEVER use these in any notification + getForbiddenPhrases(): readonly string[]; + containsForbiddenPhrase(text: string): boolean; +}; + +// Exported constant for lint rules / CI checks +export const FORBIDDEN_PHRASES: readonly string[]; +``` + +**Forbidden phrases (ecosystem-wide policy):** + +```ts +[ + "You haven't", + 'You forgot', + "Don't forget", + 'You should have', + "Why didn't you", + 'You missed', + 'You failed', + 'You need to', +]; +``` + +**Key design:** Products register their own message pools via `registerMessages()`. The forbidden phrases list is an ecosystem-wide policy enforced across all products. `containsForbiddenPhrase()` can be used in CI/lint. + +**Tests:** 8+ tests — getMessage, shouldSuppress, recordDismissal, resetDismissals, registerMessages, forbiddenPhrases, containsForbiddenPhrase, adaptive frequency reduction. + +--- + +#### 5. `@bytelyst/accessibility` + +**Purpose:** VoiceOver/TalkBack/Dynamic Type accessibility label generators for common UI patterns used across all products. + +**Reference implementation:** `learning_ai_fastgap/src/lib/accessibility.ts` (220 lines) + +**No backend needed** — pure client-side TS. + +**Public API to implement:** + +```ts +export interface A11yProps { + accessible: boolean; + accessibilityLabel: string; + accessibilityHint?: string; + accessibilityRole?: + | 'button' + | 'header' + | 'text' + | 'timer' + | 'progressbar' + | 'image' + | 'alert' + | 'summary' + | 'adjustable'; + accessibilityState?: { + disabled?: boolean; + selected?: boolean; + checked?: boolean; + busy?: boolean; + expanded?: boolean; + }; + accessibilityValue?: { min?: number; max?: number; now?: number; text?: string }; +} + +// Common UI pattern label generators +export function buttonLabel(label: string, hint?: string): A11yProps; +export function timerLabel(status: string, elapsedText: string, context?: string): A11yProps; +export function progressLabel(name: string, percent: number, description?: string): A11yProps; +export function sliderLabel(metric: string, value: number, max?: number): A11yProps; +export function alertLabel(severity: string, message: string): A11yProps; +export function achievementLabel(name: string, description: string, earned: boolean): A11yProps; +export function streakLabel(current: number, longest: number): A11yProps; +export function listItemLabel(title: string, subtitle?: string, badge?: string): A11yProps; + +// Utilities +export function formatDurationForA11y(ms: number): string; // "16 hours 30 minutes" +export function formatNumberForA11y(n: number): string; // "1,234" → "one thousand two hundred thirty four" +export function buildAnnouncement(headline: string, detail: string): string; + +// Positive messaging (never negative, complements celebrations package) +export function getPositiveBreakMessage(progressPercent: number): string; +``` + +**Key design:** React Native `accessibilityLabel` / `accessibilityRole` compatible. Web apps can map to `aria-label` / `role`. Every label function returns a complete `A11yProps` object ready to spread onto a component. + +**Tests:** 10+ tests — each label generator, formatDurationForA11y, formatNumberForA11y, buildAnnouncement, positiveBreakMessage, edge cases (0%, 100%, negative). + +--- + +### Tier 2 — High Value (no backend dependency) + +#### 6. `@bytelyst/quick-actions` + +**Purpose:** Progressive disclosure system, smart defaults, quick action definitions for reducing cognitive load. + +**Reference implementation:** `learning_ai_fastgap/src/lib/quick-actions.ts` (170 lines) + +**Public API to implement:** + +```ts +export interface QuickAction { + id: string; + label: string; + icon: string; + shortLabel: string; + action: string; + requiresAuth: boolean; +} + +export interface ProgressiveSection { + id: string; + title: string; + defaultExpanded: boolean; + priority: 'primary' | 'secondary' | 'detail'; +} + +export interface SmartDefault { + key: string; + value: unknown; + source: 'last_used' | 'most_common' | 'recommendation' | 'system'; +} + +export function getVisibleSections( + sections: ProgressiveSection[], + expandedIds: Set +): ProgressiveSection[]; +export function getAvailableActions( + actions: QuickAction[], + context: { isActive?: boolean; isAuthenticated?: boolean } +): QuickAction[]; +export function pickSmartDefault(candidates: SmartDefault[]): SmartDefault | null; + +export const MAX_VISIBLE_ITEMS = 3; +export const MAX_VISIBLE_LIST = 5; +``` + +**Tests:** 6+ tests. + +--- + +#### 7. `@bytelyst/time-references` + +**Purpose:** Familiar duration references for time-blindness aids ("About as long as a movie", "3 episodes of The Office"). + +**Reference implementation:** `learning_ai_fastgap/src/lib/time-blindness.ts` (175 lines) + +**Public API to implement:** + +```ts +export interface TimeReference { + text: string; + emoji: string; + category: 'media' | 'activity' | 'travel' | 'nature'; +} + +export function getTimeReference(elapsedHours: number): TimeReference; +export function getEpisodeComparison( + elapsedHours: number, + showName?: string, + episodeMins?: number +): string; +export function getEncouragingMessage(elapsedHours: number): string; + +// Products can register custom reference databases +export function registerReferences( + entries: Array<{ minHours: number; maxHours: number; references: TimeReference[] }> +): void; +``` + +**Tests:** 6+ tests. + +--- + +### Tier 3 — Backend Client Wrappers + +#### 8. `@bytelyst/org-client` + +**Purpose:** Client for platform-service `/orgs/*` + `/licenses/*` — B2B org management, workspace management, membership management. + +**Reference implementation:** `learning_ai_fastgap/src/lib/b2b-wellness.ts` (100 lines) — but the actual backend is far richer. + +**Backend already exists:** `platform-service/src/modules/orgs/` (3 entity types) + `platform-service/src/modules/licenses/` + +**⚠️ Auth requirement:** All org routes require **admin-only** access (`super_admin` or `admin` JWT role via `requireAdmin(req)`). Regular user tokens will get `403 Forbidden`. + +**Backend types (from `orgs/types.ts`):** + +```ts +interface OrganizationDoc { + id: string; // org_ + productId: string; + name: string; + slug: string; + status: 'active' | 'disabled'; + ownerUserId: string; + metadata?: Record; + createdAt: string; + updatedAt: string; +} + +interface WorkspaceDoc { + id: string; // ws_ + orgId: string; + productId: string; + name: string; + slug: string; + status: 'active' | 'archived'; + description?: string; + metadata?: Record; + createdAt: string; + updatedAt: string; +} + +interface MembershipDoc { + id: string; // mbr___ + orgId: string; + productId: string; + scope: 'org' | 'workspace'; + workspaceId?: string; + userId: string; + role: 'owner' | 'admin' | 'member' | 'viewer'; + status: 'active' | 'invited' | 'disabled'; + invitedBy?: string; + createdAt: string; + updatedAt: string; +} + +interface LicenseDoc { + id: string; + productId: string; + key: string; // LYSNR-XXXX-XXXX-XXXX format + userId: string; + plan: 'free' | 'pro' | 'enterprise'; + status: 'active' | 'revoked' | 'expired'; + activatedAt: string | null; + expiresAt: string | null; + deviceIds: string[]; + maxDevices: number; + createdAt: string; + updatedAt: string; +} +``` + +**Public API to implement:** + +```ts +export interface OrgClientConfig { + baseUrl: string; + productId: string; + getAccessToken: () => string | null; +} + +export interface OrgClient { + // Organizations + listOrgs(query?: { status?: string; limit?: number }): Promise; + createOrg(input: { name: string; slug: string; ownerUserId?: string }): Promise; + getOrg(id: string): Promise; + updateOrg(id: string, updates: Partial): Promise; + + // Workspaces + listWorkspaces(orgId: string): Promise; + createWorkspace( + orgId: string, + input: { name: string; slug: string; description?: string } + ): Promise; + updateWorkspace( + orgId: string, + workspaceId: string, + updates: Partial + ): Promise; + + // Memberships + listMemberships( + orgId: string, + query?: { scope?: string; limit?: number } + ): Promise; + addMember( + orgId: string, + input: { userId: string; role?: string; scope?: string; workspaceId?: string } + ): Promise; + updateMember( + orgId: string, + membershipId: string, + updates: { role?: string; status?: string } + ): Promise; + + // Licenses + generateLicense(input: { + userId: string; + plan: string; + maxDevices?: number; + }): Promise; + activateLicense(input: { key: string; deviceId: string }): Promise; + deactivateLicense(input: { key: string; deviceId: string }): Promise; +} + +export function createOrgClient(config: OrgClientConfig): OrgClient; +``` + +**Tests:** 10+ Vitest tests — listOrgs, createOrg, getOrg, listWorkspaces, createWorkspace, listMemberships, addMember, generateLicense, activateLicense, error handling (403 for non-admin). + +--- + +#### 9. `@bytelyst/marketplace-client` + +**Purpose:** Client for platform-service `/marketplace/*` — template marketplace with listings, reviews, installs, certification, reports. + +**Reference implementation:** `learning_ai_fastgap/src/lib/influencer.ts` (95 lines) covers a subset. The actual backend is a full marketplace with 6 entity types. + +**Backend already exists:** `platform-service/src/modules/marketplace/` — 309 lines of types, 6 entity types. + +**⚠️ Note:** NomGap's `influencer.ts` (branded challenges, affiliate tracking) is a **product-specific layer** on top of the marketplace. The shared client should expose the generic marketplace API. Products build influencer/branded features on top. + +**Backend types (key entities from `marketplace/types.ts`):** + +```ts +interface MarketplaceListingDoc { + id: string; // lst_ + productId: string; + templateType: string; // product-specific: 'fasting_protocol', 'agent_persona', 'timer_preset', etc. + authorId: string; + authorName: string; + title: string; + shortDescription: string; + description: string; + tags: string[]; + category: string; + payload: Record; // product-specific JSON + pricingModel: 'free' | 'paid' | 'freemium'; + priceInCents: number; + certificationStatus: 'draft' | 'submitted' | 'in_review' | 'approved' | 'rejected' | 'suspended'; + installCount: number; + reviewCount: number; + averageRating: number; + visibility: 'private' | 'unlisted' | 'public'; + featured: boolean; + version: string; + createdAt: string; + updatedAt: string; +} + +interface MarketplaceReviewDoc { + id: string; // rev_ + listingId: string; + productId: string; + authorId: string; + rating: number; // 1-5 + title: string; + body: string; + verified: boolean; + createdAt: string; +} + +interface MarketplaceInstallDoc { + id: string; // inst_ + listingId: string; + productId: string; + userId: string; + version: string; + installedAt: string; + uninstalledAt: string | null; +} +``` + +**Public API to implement:** + +```ts +export interface MarketplaceClientConfig { + baseUrl: string; + productId: string; + getAccessToken: () => string | null; +} + +export interface MarketplaceClient { + // Listings + listListings(query?: { + templateType?: string; + category?: string; + tags?: string; + pricingModel?: string; + sortBy?: string; + q?: string; + limit?: number; + offset?: number; + }): Promise<{ listings: MarketplaceListingDoc[]; total: number }>; + getListing(id: string): Promise; + createListing(input: CreateListingInput): Promise; + updateListing( + id: string, + updates: Partial + ): Promise; + submitForCertification(id: string, notes?: string): Promise; + + // Installs + installListing(listingId: string): Promise; + uninstallListing(listingId: string): Promise; + listMyInstalls(query?: { limit?: number; offset?: number }): Promise; + + // Reviews + listReviews( + listingId: string, + query?: { sortBy?: string; limit?: number } + ): Promise; + createReview( + listingId: string, + input: { rating: number; title: string; body: string } + ): Promise; + + // Reports + reportListing(listingId: string, input: { reason: string; details: string }): Promise; +} + +export function createMarketplaceClient(config: MarketplaceClientConfig): MarketplaceClient; +``` + +**Tests:** 10+ Vitest tests — listListings (with filters), getListing, createListing, submitForCertification, installListing, listMyInstalls, listReviews, createReview, reportListing, error handling. + +--- + +## Implementation Instructions + +### For each package, follow this exact pattern: + +``` +packages// +├── package.json # @bytelyst/, version 0.1.0, type: module +├── tsconfig.json # extends ../../tsconfig.base.json +├── vitest.config.ts # (optional) only if tests need special config; root config handles most cases +├── src/ +│ ├── index.ts # re-exports from client.ts + types.ts +│ ├── types.ts # all interfaces + config types +│ └── client.ts # createXxxClient() factory or pure functions +└── tests/ + └── client.test.ts # Vitest tests +``` + +### package.json template: + +```json +{ + "name": "@bytelyst/", + "version": "0.1.0", + "type": "module", + "description": "", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": ["dist"], + "scripts": { + "build": "tsc", + "test": "vitest run" + } +} +``` + +> **Note on devDependencies:** `typescript` and `vitest` are provided by the pnpm workspace root and do NOT need to be listed as devDependencies in individual packages. The root `vitest.config.ts` (with `passWithNoTests: true`) handles all packages. Only add a per-package `vitest.config.ts` if tests need special setup (e.g., custom globals or test environment). + +### tsconfig.json template: + +```json +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} +``` + +### Conventions (MUST follow): + +- **ESM everywhere:** `"type": "module"`, `.js` extensions in imports +- **No Node.js deps:** Use `globalThis.fetch`, not node-fetch +- **No React/RN deps:** Pure TS only — consumed by web, mobile, Node.js +- **Factory pattern** for API clients: `createXxxClient(config)` returning an interface +- **Storage optional:** Accept `{ getItem, setItem }` for cache/persistence +- **Vitest tests:** Every public function must have at least one test +- **No `console.log`** — silent by default +- **No `any` type** — use explicit types or generics + +### After creating packages: + +1. Add each to `pnpm-workspace.yaml` (already covered by `packages/*` glob) +2. Run `pnpm build` to verify all compile +3. Run `pnpm test` to verify all tests pass +4. Run `pnpm typecheck` to verify no type errors + +### After packages are built, migrate product repos: + +For each product (NomGap first, then others): + +1. Add `"@bytelyst/": "file:../../learning_ai_common_plat/packages/"` to product's `package.json` +2. Replace product-specific module with thin wrapper that delegates to shared package +3. Remove duplicated code +4. Run product's `npm test && npm run typecheck` + +--- + +## Execution Order + +| # | Package | Tier | Est. Lines | Backend Dep | Tests | +| --------- | -------------------------------- | ---- | ---------- | -------------------------------------- | ------ | +| 1 | `@bytelyst/referral-client` | 1 | ~150 | Yes (referrals) | 8 | +| 2 | `@bytelyst/subscription-client` | 1 | ~220 | Yes (subscriptions + plans) | 10 | +| 3 | `@bytelyst/celebrations` | 1 | ~150 | No | 8 | +| 4 | `@bytelyst/gentle-notifications` | 1 | ~130 | No | 8 | +| 5 | `@bytelyst/accessibility` | 1 | ~200 | No | 10 | +| 6 | `@bytelyst/quick-actions` | 2 | ~120 | No | 6 | +| 7 | `@bytelyst/time-references` | 2 | ~150 | No | 6 | +| 8 | `@bytelyst/org-client` | 3 | ~250 | Yes (orgs + licenses) — **admin-only** | 10 | +| 9 | `@bytelyst/marketplace-client` | 3 | ~200 | Yes (marketplace — 6 entity types) | 10 | +| **Total** | | | **~1,570** | | **76** | + +--- + +## Verification + +After all packages are created: + +```bash +cd learning_ai_common_plat +pnpm build && pnpm test && pnpm typecheck +``` + +After NomGap migration: + +```bash +cd learning_ai_fastgap +npm test && npm run typecheck +``` + +--- + +## Products That Will Consume These Packages + +> ✅ = immediate consumer, ⭐ = primary driver (has existing code to migrate), — = not needed initially (can adopt later) + +| Package | NomGap | LysnrAI | MindLyst | ChronoMind | JarvisJr | PeakPulse | FlowMonk | NoteLett | ActionTrail | +| -------------------- | ------ | ------- | -------- | ---------- | -------- | --------- | -------- | -------- | ----------- | +| referral-client | ⭐ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| subscription-client | ⭐ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| celebrations | ⭐ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| gentle-notifications | ⭐ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| accessibility | ⭐ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| quick-actions | ⭐ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| time-references | ⭐ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| org-client | ⭐ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| marketplace-client | ✅ | — | — | — | ⭐ | — | — | ✅ | — | + +> **Notes on matrix:** +> +> - `time-references` is useful for ANY app with timers, sessions, streaks, or progress tracking — not just fasting apps. Products customize the reference database via `registerReferences()`. +> - `org-client` is relevant for all products if they offer a B2B tier. Initially only products with active B2B plans need it. +> - `marketplace-client` is currently relevant for NomGap (fasting protocols) and JarvisJr (agent personas). Other products can adopt it when they add marketplace features. + +--- + +## NomGap Migration Plan (First Product) + +After the shared packages are built, NomGap (`learning_ai_fastgap`) should be migrated first since it has the most existing code to replace. + +### Files to replace in NomGap: + +| NomGap File | Replace With | Action | +| --------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------- | +| `src/lib/referral.ts` (73 lines) | `@bytelyst/referral-client` | Rewrite as thin wrapper: `import { createReferralClient } from '@bytelyst/referral-client'` | +| `src/lib/monetization.ts` (200 lines) | `@bytelyst/subscription-client` | Rewrite: `createSubscriptionClient(config)` + product-specific plan names | +| `src/lib/b2b-wellness.ts` (100 lines) | `@bytelyst/org-client` | Rewrite: `createOrgClient(config)` + NomGap-specific wellness types on top | +| `src/lib/influencer.ts` (95 lines) | `@bytelyst/marketplace-client` | Rewrite: `createMarketplaceClient(config)` + influencer layer on top | +| `src/lib/celebrations.ts` (190 lines) | `@bytelyst/celebrations` | Rewrite: `createCelebrationEngine({ customTriggers: nomgapTriggers })` | +| `src/lib/gentle-notifications.ts` (120 lines) | `@bytelyst/gentle-notifications` | Rewrite: `createGentleNotificationEngine()` + `registerMessages('fasting', [...])` | +| `src/lib/accessibility.ts` (220 lines) | `@bytelyst/accessibility` | Rewrite: import shared label generators + NomGap-specific labels | +| `src/lib/quick-actions.ts` (170 lines) | `@bytelyst/quick-actions` | Rewrite: import shared functions + NomGap-specific action/section defs | +| `src/lib/time-blindness.ts` (175 lines) | `@bytelyst/time-references` | Rewrite: `import { getTimeReference } from '@bytelyst/time-references'` | + +### Steps: + +1. Add `file:` deps to `package.json`: `"@bytelyst/referral-client": "file:../../learning_ai_common_plat/packages/referral-client"`, etc. +2. For each file, replace the standalone implementation with a thin wrapper that delegates to the shared package +3. Keep any NomGap-specific logic (fasting-specific celebrations, meal-specific suggestions) in the product repo +4. Run `npm test && npm run typecheck` after each migration +5. Total estimated reduction: ~1,340 lines of NomGap-specific code replaced with ~300 lines of thin wrappers + +### Other product migrations: + +After NomGap is migrated, other products can adopt packages as needed. Each product adds `file:` deps and creates thin wrappers. No product needs to migrate all 9 packages at once — adopt incrementally. + +--- + +## Known Backend Gaps (Require Platform-Service Changes) + +These gaps were identified during the review. They should be addressed in platform-service BEFORE or IN PARALLEL with the client packages: + +| Gap | Impact | Recommendation | +| -------------------------------------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------- | +| No `POST /referrals/apply` endpoint | Referred users can't "apply" a code in one step | Add a `POST /referrals/apply` route that finds by email + updates status atomically | +| No `POST /subscriptions/restore` endpoint | IAP restore requires receipt verification | Add a store receipt verification endpoint (Apple/Google JWKS) | +| `PlanConfig` has LysnrAI-specific fields (`words`, `dictations`, `tokens`) | Other products ignore these fields | Consider making plan schema extensible with `metadata: Record` | +| Org routes require admin JWT role | Regular users can't view their own org | Consider adding user-facing org endpoints (e.g., `GET /my/org`) |