docs: fix 17 bugs/gaps in shared client packages roadmap + add workflow

Review findings and fixes:
- Fix subscription routes use :userId not :id
- Fix GET /plans returns { plans: [...] } wrapper
- Fix org routes require admin-only JWT role
- Flag missing POST /referrals/apply endpoint
- Flag missing POST /subscriptions/restore endpoint
- Expand org-client and marketplace-client with full API signatures
- Add x-product-id header requirement
- Add NomGap Migration Plan
- Add Known Backend Gaps appendix
- Add /implement-shared-packages workflow
This commit is contained in:
saravanakumardb1 2026-03-19 11:02:42 -07:00
parent 0d20987f75
commit c87a8e9ef1
2 changed files with 1221 additions and 0 deletions

View File

@ -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 <token>` (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

View File

@ -0,0 +1,923 @@
# Shared @bytelyst/\* Client Packages — Extraction from Product Repos
> **Status:** Not Started
> **Priority:** High — eliminates duplication across 9+ products
> **Estimated effort:** 810 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<ReferralDoc>;
// 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<ReferralDoc>;
getByEmail(email: string): Promise<ReferralDoc | null>;
// 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 <token>` 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<SubscriptionDoc | null>;
// NOTE: GET /plans returns { plans: [...] } — client must unwrap .plans
getPlans(): Promise<PlanConfig[]>;
startTrial(planName?: string): Promise<SubscriptionDoc>;
cancelSubscription(): Promise<SubscriptionDoc>;
// NOTE: PUT /subscriptions/:userId (not :id) — use config.userId
updateSubscription(updates: Partial<SubscriptionDoc>): Promise<SubscriptionDoc>;
// 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<void>; // 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<string, Celebration>;
}
export function createCelebrationEngine(config?: CelebrationConfig): {
getCelebration(trigger: CelebrationTrigger | string): Celebration;
getTimedCelebrations(elapsedMs: number, targetMs: number, shownIds: Set<string>): 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<GentleNotificationConfig>): {
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<string>
): 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_<uuid>
productId: string;
name: string;
slug: string;
status: 'active' | 'disabled';
ownerUserId: string;
metadata?: Record<string, unknown>;
createdAt: string;
updatedAt: string;
}
interface WorkspaceDoc {
id: string; // ws_<uuid>
orgId: string;
productId: string;
name: string;
slug: string;
status: 'active' | 'archived';
description?: string;
metadata?: Record<string, unknown>;
createdAt: string;
updatedAt: string;
}
interface MembershipDoc {
id: string; // mbr_<orgId>_<userId>_<scope>
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<OrganizationDoc[]>;
createOrg(input: { name: string; slug: string; ownerUserId?: string }): Promise<OrganizationDoc>;
getOrg(id: string): Promise<OrganizationDoc>;
updateOrg(id: string, updates: Partial<OrganizationDoc>): Promise<OrganizationDoc>;
// Workspaces
listWorkspaces(orgId: string): Promise<WorkspaceDoc[]>;
createWorkspace(
orgId: string,
input: { name: string; slug: string; description?: string }
): Promise<WorkspaceDoc>;
updateWorkspace(
orgId: string,
workspaceId: string,
updates: Partial<WorkspaceDoc>
): Promise<WorkspaceDoc>;
// Memberships
listMemberships(
orgId: string,
query?: { scope?: string; limit?: number }
): Promise<MembershipDoc[]>;
addMember(
orgId: string,
input: { userId: string; role?: string; scope?: string; workspaceId?: string }
): Promise<MembershipDoc>;
updateMember(
orgId: string,
membershipId: string,
updates: { role?: string; status?: string }
): Promise<MembershipDoc>;
// Licenses
generateLicense(input: {
userId: string;
plan: string;
maxDevices?: number;
}): Promise<LicenseDoc>;
activateLicense(input: { key: string; deviceId: string }): Promise<LicenseDoc>;
deactivateLicense(input: { key: string; deviceId: string }): Promise<void>;
}
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_<uuid>
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<string, unknown>; // 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_<uuid>
listingId: string;
productId: string;
authorId: string;
rating: number; // 1-5
title: string;
body: string;
verified: boolean;
createdAt: string;
}
interface MarketplaceInstallDoc {
id: string; // inst_<uuid>
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<MarketplaceListingDoc>;
createListing(input: CreateListingInput): Promise<MarketplaceListingDoc>;
updateListing(
id: string,
updates: Partial<MarketplaceListingDoc>
): Promise<MarketplaceListingDoc>;
submitForCertification(id: string, notes?: string): Promise<MarketplaceListingDoc>;
// Installs
installListing(listingId: string): Promise<MarketplaceInstallDoc>;
uninstallListing(listingId: string): Promise<void>;
listMyInstalls(query?: { limit?: number; offset?: number }): Promise<MarketplaceInstallDoc[]>;
// Reviews
listReviews(
listingId: string,
query?: { sortBy?: string; limit?: number }
): Promise<MarketplaceReviewDoc[]>;
createReview(
listingId: string,
input: { rating: number; title: string; body: string }
): Promise<MarketplaceReviewDoc>;
// Reports
reportListing(listingId: string, input: { reason: string; details: string }): Promise<void>;
}
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-name>/
├── package.json # @bytelyst/<name>, 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/<package-name>",
"version": "0.1.0",
"type": "module",
"description": "<one-line 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/<package>": "file:../../learning_ai_common_plat/packages/<package>"` 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<string, unknown>` |
| 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`) |