docs: fix 10 inaccuracies in DRY audit + roadmap after thorough review

Verified claims against actual codebase. Key corrections:

Audit (CROSS_REPO_DRY_AUDIT.md):
- Fix #5: all 9 repos already use @bytelyst/datastore — issue is only
  DB_PROVIDER missing from config.ts in 3 older repos (not a full migration)
- Fix #6: only 3 repos have product-config.ts (not 4) — NomGap was wrong
- Fix #11: web telemetry.ts is in NomGap+NoteLett+ChronoMind+LysnrAI (not JarvisJr)
- Fix #12: web diagnostics.ts includes LysnrAI user-dashboard-web (5 repos total)
- Fix auth.ts LOC: exactly 79 lines × 9 repos = 711 (was '60-90')
- Fix request-context.ts LOC: 30-49 lines range (was '~30-50')
- Fix package count: 50 packages (not 53)
- Add items 15-16: web auth.ts + billing-client.ts (noted as keep-as-is)
- Fix LOC math: ~2,700 total (was inflated ~4,200)
- Add cosmos-init.ts note (5 repos, product-specific, not consolidation candidates)

Roadmap (CROSS_REPO_DRY_MIGRATION_ROADMAP.md):
- Phase 0.2: 6 repos need product-config.ts (add NomGap)
- Phase 2.3: rewritten from 'migrate to datastore' to 'add DB_PROVIDER to config'
- Phase 4: add LysnrAI user-dashboard-web to affected repos, remove JarvisJr from telemetry
- Fix product-config.ts template path (was ../../../../, now ../../../ with depth note)
- Fix success metrics: packages 50→55, product-config 3→9, LOC ~1,760
- Fix overview table: Phase 2 name, Phase 4 repo count
This commit is contained in:
saravanakumardb1 2026-03-20 07:15:07 -07:00
parent a769f671e2
commit d10322095a
2 changed files with 99 additions and 90 deletions

View File

@ -1,16 +1,16 @@
# Cross-Repo DRY Audit — Migration & Integration Candidates
> **Date:** 2026-03-20
> **Scope:** 9 product backends + web dashboards vs. 53 `@bytelyst/*` shared packages
> **Scope:** 9 product backends + web dashboards vs. 50 `@bytelyst/*` shared packages
> **Goal:** Identify duplicated patterns across product repos that should be consolidated into common platform packages.
---
## Executive Summary
Audited all 9 product repos with Fastify backends. Found **6 high-priority** and **4 medium-priority** duplication patterns across backends and web clients. The most impactful candidates involve backend `lib/` files that are **copy-pasted identically** (or near-identically) across all repos — differing only by product ID, port number, or service name.
Audited all 9 product repos with Fastify backends + 5 Next.js web dashboards. Found **6 high-priority** and **4 medium-priority** backend duplication patterns, plus **6 web-side** duplication patterns. The most impactful candidates involve backend `lib/` files that are **copy-pasted identically** (or near-identically) across all repos — differing only by product ID, port number, or service name.
**Estimated LOC savings:** ~2,400 lines backend + ~1,800 lines web = **~4,200 lines** of duplicated code that could be replaced by shared packages.
**Estimated LOC savings:** ~1,700 lines backend + ~1,000 lines web = **~2,700 lines** of duplicated code that could be replaced by shared packages or eliminated entirely.
---
@ -18,7 +18,7 @@ Audited all 9 product repos with Fastify backends. Found **6 high-priority** and
### 1. `auth.ts` — JWT verification + JWKS + role checking
**Status:** Identical across all 9 repos (60-90 lines each = ~700 LOC total)
**Status:** Identical across all 9 repos (79 lines each = 711 LOC total)
**Pattern:** RS256 JWKS verification → HS256 fallback → `extractAuth()` + `requireRole()`
**Only difference:** imports `config` from local `./config.js`
@ -37,7 +37,7 @@ export function requireRole(req: FastifyRequest, ...roles: string[]): Promise<Au
### 2. `request-context.ts` — productId validation + getUserId()
**Status:** Nearly identical across all 9 repos (~30-50 lines each = ~350 LOC total)
**Status:** Nearly identical across all 9 repos (30-49 lines each = ~315 LOC total)
**Only difference:** `const PRODUCT_ID = '<product>'` string literal
**Recommendation:** Add to `@bytelyst/fastify-auth` or new `@bytelyst/request-context`:
@ -90,25 +90,27 @@ export const config = createBackendConfig({
---
### 5. `datastore.ts` — DB_PROVIDER switch (Cosmos vs Memory)
### 5. `datastore.ts` — DB_PROVIDER in config vs. hardcoded env read
**Status:** Identical in 6/9 repos; 3 older repos use direct Cosmos without the provider abstraction
**Status:** All 9 repos already use `@bytelyst/datastore` (CosmosDatastoreProvider / MemoryDatastoreProvider). However, 3 older repos (LysnrAI, MindLyst, ChronoMind) read `DB_PROVIDER` directly from `process.env` inside `datastore.ts` instead of declaring it in their Zod `config.ts` schema. The 6 newer repos properly declare `DB_PROVIDER` in config.
**Pattern:** Import `@bytelyst/datastore`, configure based on `DB_PROVIDER` env var
**Recommendation:** Already have `@bytelyst/datastore` package. The 3 older repos (LysnrAI, MindLyst, ChronoMind) should be **migrated** to use `@bytelyst/datastore` with `DB_PROVIDER` support instead of direct `@azure/cosmos` calls.
**Recommendation:** Add `DB_PROVIDER` to the Zod config schema in the 3 older repos so it's validated at startup like other env vars. This is a minor cleanup (not a full migration).
**Migration needed:** LysnrAI, MindLyst, ChronoMind backends
**Cleanup needed:** LysnrAI, MindLyst, ChronoMind — add `DB_PROVIDER` to `config.ts`
**Note:** 5 repos also have a supplementary `cosmos-init.ts` file (MindLyst, ChronoMind, JarvisJr, FlowMonk, NoteLett) that registers Cosmos containers. These are product-specific and NOT candidates for consolidation — each repo registers different containers.
---
### 6. `product-config.ts` — Backend product identity
**Status:** Present in 4/9 repos (FlowMonk, ActionTrail, NoteLett, NomGap), hardcoded in others
**Status:** Present in 3/9 repos (FlowMonk, ActionTrail, NoteLett). The other 6 repos hardcode `const PRODUCT_ID = '<name>'` in `request-context.ts`.
**Pattern:** Read `shared/product.json`, export `PRODUCT_ID`, `productConfig`
**Recommendation:** Already solved by `@bytelyst/config` (loadProductIdentity). The 5 repos without `product-config.ts` should adopt `@bytelyst/config/product-identity` instead of hardcoding product IDs.
**Recommendation:** The 6 repos without `product-config.ts` should add one that reads from `shared/product.json` (all repos have this file). This eliminates hardcoded product IDs and ensures consistency.
**Migration needed:** LysnrAI, MindLyst, ChronoMind, JarvisJr, PeakPulse
**Migration needed:** LysnrAI, MindLyst, ChronoMind, JarvisJr, NomGap, PeakPulse
---
@ -180,23 +182,23 @@ export function createTelemetryBuffer(opts: { enabled: boolean }) { ... }
### 11. `telemetry.ts` — Web telemetry init
**Status:** NomGap, NoteLett, ChronoMind, JarvisJr have near-identical wrappers (~35 lines each)
**Status:** NomGap, NoteLett, ChronoMind, LysnrAI (user-dashboard-web) have near-identical wrappers (~35 lines each). JarvisJr does NOT have this file.
**Only difference:** `channel` name string
**Recommendation:** The `@bytelyst/telemetry-client` package already provides `createTelemetryClient()`. Products should call it directly in their `providers.tsx` instead of maintaining a wrapper file. Alternatively, add a `createWebTelemetry(productId, channel)` convenience to the package.
**Affected repos:** NomGap, NoteLett, ChronoMind, JarvisJr web apps
**Affected repos:** NomGap, NoteLett, ChronoMind, LysnrAI web apps
---
### 12. `diagnostics.ts` — Web diagnostics init
**Status:** NomGap, NoteLett, ChronoMind, JarvisJr have near-identical wrappers (~40 lines each)
**Status:** NomGap, NoteLett, ChronoMind, JarvisJr, LysnrAI (user-dashboard-web) have near-identical wrappers (~40 lines each)
**Only difference:** `channel` name, auth token retrieval method
**Recommendation:** Add `createWebDiagnostics(opts)` convenience to `@bytelyst/diagnostics-client` that handles install ID generation, localStorage auth token, and default config.
**Affected repos:** NomGap, NoteLett, ChronoMind, JarvisJr web apps
**Affected repos:** NomGap, NoteLett, ChronoMind, JarvisJr, LysnrAI web apps
---
@ -212,11 +214,32 @@ export function createTelemetryBuffer(opts: { enabled: boolean }) { ... }
**Status:** 5 different patterns across repos:
- NomGap/NoteLett: import `shared/product.json` + env var overrides
- NomGap/NoteLett: import `shared/product.json` + env var overrides (best pattern)
- FlowMonk/ActionTrail: inline object literal
- JarvisJr/ChronoMind: custom exports
- JarvisJr: custom `getPlatformBaseURL()` function
- ChronoMind: product config embedded in `auth-api.ts` (`PRODUCT_ID` + `getBaseUrl()`)
- LysnrAI user-dashboard: uses `@bytelyst/config` `loadProductIdentity()` (slightly different)
**Recommendation:** Standardize on `shared/product.json` import pattern. Could add `@bytelyst/web-config` convenience that reads product.json and provides typed exports with NEXT*PUBLIC* env var overrides.
**Recommendation:** Standardize on `shared/product.json` import pattern (NomGap/NoteLett style). This reads the canonical `shared/product.json` and provides typed exports with `NEXT_PUBLIC_*` env var overrides.
---
### 15. `auth.ts` — Web auth provider wrapper
**Status:** NomGap and NoteLett have near-identical `auth.ts` files (~40 lines each) wrapping `createAuthProvider()` from `@bytelyst/react-auth` with product-specific types.
**Only difference:** LoginResponse type shape varies slightly.
**Recommendation:** Minor duplication — keep as-is since each product may have different user types. Not worth a shared package.
**Affected repos:** NomGap, NoteLett (ChronoMind uses `@bytelyst/auth-client` instead)
---
### 16. `billing-client.ts` — Web subscription/billing wrapper
**Status:** NomGap and ChronoMind wrap `@bytelyst/subscription-client`; LysnrAI wraps `@bytelyst/api-client`. Three different approaches to the same problem.
**Recommendation:** Minor duplication — keep as-is since each product has different billing UI needs. The shared `@bytelyst/subscription-client` already handles the heavy lifting.
---
@ -224,20 +247,22 @@ export function createTelemetryBuffer(opts: { enabled: boolean }) { ... }
| # | Candidate | LOC Saved | Repos | Effort | Priority |
| --- | -------------------------------------------------------- | --------- | ----- | -------- | -------- |
| 1 | `auth.ts``@bytelyst/fastify-auth` | ~700 | 9 | 2 days | **P0** |
| 2 | `request-context.ts` → merge into fastify-auth | ~350 | 9 | 1 day | **P0** |
| 1 | `auth.ts``@bytelyst/fastify-auth` | ~711 | 9 | 2 days | **P0** |
| 2 | `request-context.ts` → merge into fastify-auth | ~315 | 9 | 1 day | **P0** |
| 3 | `errors.ts` → delete (import directly) | ~100 | 8 | 0.5 day | **P0** |
| 4 | `config.ts``@bytelyst/backend-config` | ~200 | 9 | 2 days | **P1** |
| 5 | Older repos → `@bytelyst/datastore` | ~300 | 3 | 1 day | **P1** |
| 6 | Hardcoded productId → `@bytelyst/config` | ~50 | 5 | 0.5 day | **P1** |
| 5 | `DB_PROVIDER` cleanup in older config.ts | ~15 | 3 | 0.5 day | **P1** |
| 6 | Hardcoded productId → `product-config.ts` | ~50 | 6 | 0.5 day | **P1** |
| 7 | `events.ts``@bytelyst/domain-events` | ~300 | 2 | 1.5 days | **P2** |
| 8 | `feature-flags.ts``@bytelyst/backend-flags` | ~50 | 2 | 0.5 day | **P2** |
| 9 | `telemetry.ts``@bytelyst/backend-telemetry` | ~70 | 2 | 0.5 day | **P2** |
| 10 | Web telemetry/diagnostics wrappers → package convenience | ~400 | 4 | 1 day | **P2** |
| 10 | Web telemetry/diagnostics wrappers → package convenience | ~400 | 5 | 1 day | **P2** |
| 11 | Web `product-config.ts` → standardize pattern | ~200 | 5+ | 1 day | **P2** |
**Total estimated effort: ~11 days for full DRY migration**
**Total LOC eliminated: ~2,700+ duplicated lines**
**Total LOC eliminated: ~2,435+ duplicated lines**
**Note:** Items 15-16 (web auth.ts, billing-client.ts) are intentionally NOT in this matrix — the duplication is minor and each product legitimately needs different types/behavior.
---

View File

@ -4,7 +4,7 @@
> **Created:** 2026-03-20
> **Audit:** See [`../CROSS_REPO_DRY_AUDIT.md`](../CROSS_REPO_DRY_AUDIT.md)
> **Total effort:** ~11 days across 5 phases
> **Total LOC eliminated:** ~4,200 duplicated lines across 9 product repos
> **Total LOC eliminated:** ~2,700 duplicated lines across 9 product repos + 5 web dashboards
---
@ -12,11 +12,11 @@
| Phase | Name | New Packages | Repos Touched | Est. Effort | Status |
| ----- | --------------------------------------------------- | ------------ | ------------- | ----------- | ------ |
| **0** | Quick Wins (no new packages) | 0 | 8 | 1 day | ⬜ |
| **0** | Quick Wins (no new packages) | 0 | 9 | 1 day | ⬜ |
| **1** | `@bytelyst/fastify-auth` | 1 | 10 | 3 days | ⬜ |
| **2** | `@bytelyst/backend-config` + datastore migration | 1 | 12 | 3 days | ⬜ |
| **2** | `@bytelyst/backend-config` + DB_PROVIDER cleanup | 1 | 12 | 3 days | ⬜ |
| **3** | Backend utilities (flags, telemetry, domain events) | 3 | 3 | 2 days | ⬜ |
| **4** | Web client DRY (telemetry, diagnostics, config) | 0 | 6 | 2 days | ⬜ |
| **4** | Web client DRY (telemetry, diagnostics, config) | 0 | 7 | 2 days | ⬜ |
**Repos involved (9 product backends):**
@ -69,7 +69,7 @@ These files exist in 8 repos and contain only re-exports from `@bytelyst/errors`
### 0.2 Add `product-config.ts` to repos that hardcode product IDs
Five repos hardcode `const PRODUCT_ID = '<name>'` in `request-context.ts` instead of reading from `shared/product.json`.
Six repos hardcode `const PRODUCT_ID = '<name>'` in `request-context.ts` instead of reading from `shared/product.json`.
**Repos needing `product-config.ts`:**
@ -79,6 +79,7 @@ Five repos hardcode `const PRODUCT_ID = '<name>'` in `request-context.ts` instea
| MindLyst | `'mindlyst'` in `request-context.ts` | 4014 |
| ChronoMind | `'chronomind'` in `request-context.ts` | 4011 |
| JarvisJr | `'jarvisjr'` in `request-context.ts` | 4012 |
| NomGap | `'nomgap'` in `request-context.ts` | 4013 |
| PeakPulse | `'peakpulse'` in `request-context.ts` | 4010 |
**Steps per repo:**
@ -92,9 +93,10 @@ Five repos hardcode `const PRODUCT_ID = '<name>'` in `request-context.ts` instea
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const raw = JSON.parse(
readFileSync(resolve(__dirname, '../../../../shared/product.json'), 'utf8')
);
// Path works from both src/ (dev with tsx) and dist/ (compiled):
// src/lib/ -> ../../shared/ OR dist/lib/ -> ../../shared/
// Both resolve to <repo>/shared/product.json since backend/ is a subdirectory.
const raw = JSON.parse(readFileSync(resolve(__dirname, '../../../shared/product.json'), 'utf8'));
export const PRODUCT_ID: string = raw.productId;
export const productConfig = raw as {
@ -106,6 +108,12 @@ Five repos hardcode `const PRODUCT_ID = '<name>'` in `request-context.ts` instea
};
```
> **IMPORTANT:** The relative path `../../../shared/product.json` resolves from
> `backend/src/lib/` (3 levels up = repo root) and also from `backend/dist/lib/`
> (3 levels up = repo root). Verify with: `ls $(dirname $0)/../../../shared/product.json`.
> For repos where `backend/` is at repo root, the path may need to be `../../../../shared/product.json`
> — check the existing `product-config.ts` in FlowMonk/ActionTrail/NoteLett for the correct depth.
3. Update `request-context.ts`: replace `const PRODUCT_ID = '<hardcoded>'` with `import { PRODUCT_ID } from './product-config.js'`
4. Update `config.ts` if it hardcodes the port: use `productConfig.backendPort`
5. Run: `cd backend && npm run typecheck && npm test`
@ -408,59 +416,27 @@ export const config = createBackendConfig({
**Per-repo commit:** `refactor(backend): migrate config to @bytelyst/backend-config`
### 2.3 Migrate LysnrAI, MindLyst, ChronoMind to `@bytelyst/datastore`
### 2.3 Add `DB_PROVIDER` to config schema in 3 older repos
These 3 repos use direct `@azure/cosmos` calls with custom `cosmos-init.ts` files instead of the `@bytelyst/datastore` abstraction that the 6 newer repos use.
All 9 repos already use `@bytelyst/datastore` in their `datastore.ts`. However, 3 older repos (LysnrAI, MindLyst, ChronoMind) read `DB_PROVIDER` directly from `process.env` inside `datastore.ts` instead of declaring it in their Zod config schema. This means `DB_PROVIDER` isn't validated at startup.
**Repos to migrate:**
**Repos to fix:**
| Repo | Current Pattern | Target |
| ---------- | ----------------------------------------- | ------------------------------------- |
| LysnrAI | Direct `@azure/cosmos` + `cosmos-init.ts` | `@bytelyst/datastore` + `DB_PROVIDER` |
| MindLyst | Direct `@azure/cosmos` + `cosmos-init.ts` | `@bytelyst/datastore` + `DB_PROVIDER` |
| ChronoMind | Direct `@azure/cosmos` + `cosmos-init.ts` | `@bytelyst/datastore` + `DB_PROVIDER` |
| ---------- | ------------------------------------------- | -------------------------------------------------- |
| LysnrAI | `process.env.DB_PROVIDER` in `datastore.ts` | `config.DB_PROVIDER` via Zod schema in `config.ts` |
| MindLyst | `process.env.DB_PROVIDER` in `datastore.ts` | `config.DB_PROVIDER` via Zod schema in `config.ts` |
| ChronoMind | `process.env.DB_PROVIDER` in `datastore.ts` | `config.DB_PROVIDER` via Zod schema in `config.ts` |
**Steps per repo:**
1. Add `"@bytelyst/datastore": "file:../../learning_ai_common_plat/packages/datastore"` to `package.json`
2. Add `DB_PROVIDER` to config schema (default: `'cosmos'`)
3. Create/update `backend/src/lib/datastore.ts` matching the 6 newer repos' pattern:
1. Add `DB_PROVIDER: z.enum(['cosmos', 'memory']).default('cosmos')` to `config.ts` Zod schema
2. Update `datastore.ts` to read `config.DB_PROVIDER` instead of `process.env.DB_PROVIDER`
3. `cd backend && npm run typecheck && npm test`
```ts
import {
CosmosDatastoreProvider,
MemoryDatastoreProvider,
type DatastoreProvider,
} from '@bytelyst/datastore';
import { config } from './config.js';
**Note:** Do NOT delete `cosmos-init.ts` files — these register product-specific Cosmos containers and are still needed alongside `@bytelyst/datastore`.
let provider: DatastoreProvider;
export function getDatastoreProvider(): DatastoreProvider {
if (!provider) {
provider =
config.DB_PROVIDER === 'memory'
? new MemoryDatastoreProvider()
: new CosmosDatastoreProvider({
endpoint: config.COSMOS_ENDPOINT,
key: config.COSMOS_KEY,
database: config.COSMOS_DATABASE,
});
}
return provider;
}
export function getCollection(name: string) {
return getDatastoreProvider().getCollection(name);
}
```
4. Update each repository file to use `getCollection()` instead of direct Cosmos container calls
5. Delete `cosmos-init.ts` if fully replaced
6. Add `DB_PROVIDER=memory` to test env for zero-dependency test runs
7. `cd backend && npm install && npm run typecheck && npm test`
**Per-repo commit:** `refactor(backend): migrate to @bytelyst/datastore with DB_PROVIDER`
**Per-repo commit:** `refactor(backend): add DB_PROVIDER to config schema`
### 2.4 Verification checkpoint
@ -477,7 +453,7 @@ for repo in learning_voice_ai_agent learning_multimodal_memory_agents learning_a
done
```
**Expected:** All 1,217+ backend tests pass. 3 repos now support `DB_PROVIDER=memory` for offline testing.
**Expected:** All 1,217+ backend tests pass. All 9 repos now have `DB_PROVIDER` in their validated config schema.
---
@ -665,7 +641,9 @@ export function createWebTelemetry(opts: {
export { client as telemetryClient, init as initTelemetry, trackPageView };
```
**Repos:** NomGap, NoteLett, ChronoMind, JarvisJr
**Repos:** NomGap, NoteLett, ChronoMind, LysnrAI (user-dashboard-web)
**Note:** JarvisJr does NOT have a `telemetry.ts` wrapper file.
**Commit:** `feat(telemetry-client): add createWebTelemetry() convenience`
@ -688,7 +666,7 @@ export function createWebDiagnostics(opts: {
**Migration per web app:** Replace ~40-line file with ~8-line wrapper.
**Repos:** NomGap, NoteLett, ChronoMind, JarvisJr
**Repos:** NomGap, NoteLett, ChronoMind, JarvisJr, LysnrAI (user-dashboard-web)
**Commit:** `feat(diagnostics-client): add createWebDiagnostics() convenience`
@ -719,6 +697,7 @@ export const TELEMETRY_TRANSPORT =
- ActionTrail web: same
- JarvisJr web: uses custom `getPlatformBaseURL()` → standardize
- ChronoMind web: uses `auth-api.ts` for product config → separate into `product-config.ts`
- LysnrAI user-dashboard-web: uses `@bytelyst/config` `loadProductIdentity()` → align with `shared/product.json` pattern
**Per-repo commit:** `refactor(web): standardize product-config.ts to shared/product.json pattern`
@ -732,7 +711,8 @@ pnpm --filter @bytelyst/diagnostics-client build
# Typecheck all web apps
for repo in learning_ai_fastgap/web learning_ai_notes/web learning_ai_clock/web \
learning_ai_jarvis_jr/web learning_ai_flowmonk/web learning_ai_trails/web; do
learning_ai_jarvis_jr/web learning_ai_flowmonk/web learning_ai_trails/web \
learning_voice_ai_agent/user-dashboard-web; do
echo "=== $repo ===" && cd /Users/sd9235/code/mygh/$repo && npx tsc --noEmit 2>&1 | tail -3 && cd -
done
```
@ -779,7 +759,7 @@ Every migration follows the same rollback strategy:
| `datastore.ts` | `@bytelyst/datastore` with `DB_PROVIDER` | ~15 |
| ~~`errors.ts`~~ | **DELETED** — import `@bytelyst/errors` directly | 0 |
**Average backend `lib/` shrinks from ~250 lines to ~60 lines (76% reduction).**
**Average backend `lib/` shrinks from ~200 lines to ~60 lines (70% reduction).**
### New packages in common-plat
@ -806,11 +786,15 @@ Every migration follows the same rollback strategy:
| Metric | Before | After |
| ------------------------------------------- | ------ | ----------------------------------- |
| Duplicated backend `lib/` LOC | ~2,400 | ~540 (thin wrappers) |
| Duplicated web `lib/` LOC | ~1,800 | ~400 (thin wrappers) |
| Total duplicated LOC | ~4,200 | ~940 |
| **LOC eliminated** | — | **~3,260** |
| Shared packages | 53 | 58 |
| Repos with `DB_PROVIDER` support | 6 | 9 |
| Repos with standardized `product-config.ts` | 4 | 9 |
| Duplicated backend `lib/` LOC | ~1,700 | ~540 (thin wrappers) |
| Duplicated web `lib/` LOC | ~1,000 | ~400 (thin wrappers) |
| Total duplicated LOC | ~2,700 | ~940 |
| **LOC eliminated** | — | **~1,760** |
| Shared packages | 50 | 55 |
| Repos with `DB_PROVIDER` in config schema | 6 | 9 |
| Repos with standardized `product-config.ts` | 3 | 9 |
| Backend test count | 1,217 | 1,217+ (unchanged + ~58 new shared) |
> **Note on package count:** The 50-package baseline counts directories in
> `learning_ai_common_plat/packages/` that contain a `package.json`. The 5 new
> packages proposed in this roadmap bring the total to 55.