@variantlab/core 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,394 @@
1
+ # Framework SSR quirks
2
+
3
+ Notes on how each target framework handles server-side rendering, hydration, and the implications for variantlab. This document is the reference for implementing each adapter.
4
+
5
+ ## Table of contents
6
+
7
+ - [Common concerns](#common-concerns)
8
+ - [Next.js App Router](#nextjs-app-router)
9
+ - [Next.js Pages Router](#nextjs-pages-router)
10
+ - [Remix](#remix)
11
+ - [SvelteKit](#sveltekit)
12
+ - [SolidStart](#solidstart)
13
+ - [Nuxt](#nuxt)
14
+ - [Astro](#astro)
15
+ - [React Native](#react-native)
16
+ - [Edge runtime targets](#edge-runtime-targets)
17
+
18
+ ---
19
+
20
+ ## Common concerns
21
+
22
+ Every SSR framework has the same core problem for A/B testing: the server renders one variant, the client hydrates, and if the client picks a different variant, React/Vue/Svelte throws a hydration mismatch error.
23
+
24
+ ### Solutions available
25
+
26
+ 1. **Sticky cookie**: server sets a cookie with the variant assignment; client reads the same cookie on hydration.
27
+ 2. **Streaming with suspense**: server holds rendering until it knows the variant; client streams the result.
28
+ 3. **Defer to client**: render a placeholder on server, pick the variant on client. This is the easy way but causes layout shift.
29
+ 4. **Edge middleware**: assign the variant at the edge before the request hits the renderer, then pass it via headers.
30
+
31
+ variantlab supports all four, with sticky cookie as the recommended default.
32
+
33
+ ### Invariants variantlab must uphold
34
+
35
+ - **Deterministic assignment**: given the same `(userId, experimentId, context)`, always produce the same variant.
36
+ - **No `Math.random()` on the hot path**: non-deterministic → hydration mismatches.
37
+ - **No `Date.now()` in pure assignment**: time-based targeting only at evaluation boundaries.
38
+ - **Stable ordering of experiments**: iteration order must match server and client.
39
+ - **No environment-specific branches in assignment logic**: server code must match client code byte-for-byte.
40
+
41
+ ---
42
+
43
+ ## Next.js App Router
44
+
45
+ Next.js 14/15 App Router uses React Server Components (RSC). Server components can read variants freely; client components need a context.
46
+
47
+ ### Architecture
48
+
49
+ ```
50
+ ┌─────────────────────────────────────────────────┐
51
+ │ middleware.ts │
52
+ │ Reads cookies, sets sticky variant cookie │
53
+ └──────────────┬──────────────────────────────────┘
54
+
55
+
56
+ ┌─────────────────────────────────────────────────┐
57
+ │ app/layout.tsx (Server Component) │
58
+ │ Reads cookies via next/headers │
59
+ │ Creates initial variant map │
60
+ │ Wraps children in VariantLabProvider │
61
+ │ (client component, re-hydrates from initial map)│
62
+ └──────────────┬──────────────────────────────────┘
63
+
64
+
65
+ ┌─────────────────────────────────────────────────┐
66
+ │ Server components: read via getVariantSSR() │
67
+ │ Client components: useVariant() hook │
68
+ └──────────────────────────────────────────────────┘
69
+ ```
70
+
71
+ ### Our implementation
72
+
73
+ - **Middleware** sets a cookie `variantlab_v1` containing an encoded map of `(experimentId → variantId)` for eligible experiments
74
+ - **Root layout** reads the cookie via `cookies()` from `next/headers`, decodes it, and passes it to `<VariantLabProvider initialVariants={...}>`
75
+ - **Server components** import `getVariantSSR(experimentId, cookies())` which is a pure function
76
+ - **Client components** use the React hooks via the provider
77
+
78
+ ### Pitfalls
79
+
80
+ 1. **Dynamic vs static rendering.** If a page reads cookies, Next.js marks it as dynamic. This affects caching. We document this clearly.
81
+ 2. **RSC boundary.** `VariantLabProvider` must be a client component. We re-export it from `@variantlab/next/client` with `"use client"` directive.
82
+ 3. **Cookie size.** Next.js cookies are limited to 4 KB. If you have many experiments, we compress the cookie (base64-encoded minified JSON, or pack variant IDs as small integers).
83
+ 4. **Parallel routes and intercepting routes.** These can render the same provider multiple times. Our provider detects this and de-duplicates.
84
+
85
+ ### Example
86
+
87
+ ```tsx
88
+ // middleware.ts
89
+ import { variantLabMiddleware } from "@variantlab/next/middleware";
90
+ import config from "./experiments.json";
91
+
92
+ export default variantLabMiddleware(config);
93
+ export const config = { matcher: ["/((?!_next|api).*)"] };
94
+
95
+ // app/layout.tsx
96
+ import { cookies } from "next/headers";
97
+ import { VariantLabProvider } from "@variantlab/next/client";
98
+ import experiments from "./experiments.json";
99
+
100
+ export default function RootLayout({ children }) {
101
+ const cookieStore = cookies();
102
+ const initialVariants = JSON.parse(
103
+ cookieStore.get("variantlab_v1")?.value ?? "{}"
104
+ );
105
+ return (
106
+ <html>
107
+ <body>
108
+ <VariantLabProvider
109
+ config={experiments}
110
+ initialVariants={initialVariants}
111
+ >
112
+ {children}
113
+ </VariantLabProvider>
114
+ </body>
115
+ </html>
116
+ );
117
+ }
118
+
119
+ // app/page.tsx (server component)
120
+ import { getVariantSSR } from "@variantlab/next";
121
+ import experiments from "./experiments.json";
122
+
123
+ export default async function Page() {
124
+ const cookieStore = cookies();
125
+ const variant = getVariantSSR("cta-copy", cookieStore, experiments);
126
+ return <button>{variant === "buy-now" ? "Buy now" : "Get started"}</button>;
127
+ }
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Next.js Pages Router
133
+
134
+ Older model. Simpler but less flexible.
135
+
136
+ ### Architecture
137
+
138
+ - `getServerSideProps` reads cookies, picks variant, passes as prop
139
+ - `_app.tsx` wraps in `<VariantLabProvider>` with initial variants
140
+ - Client hooks work as in any React app
141
+
142
+ ### Example
143
+
144
+ ```tsx
145
+ // pages/_app.tsx
146
+ import { VariantLabProvider } from "@variantlab/next/client";
147
+ import experiments from "../experiments.json";
148
+
149
+ export default function MyApp({ Component, pageProps }) {
150
+ return (
151
+ <VariantLabProvider
152
+ config={experiments}
153
+ initialVariants={pageProps.variantlab ?? {}}
154
+ >
155
+ <Component {...pageProps} />
156
+ </VariantLabProvider>
157
+ );
158
+ }
159
+
160
+ // pages/index.tsx
161
+ import { getVariantSSR } from "@variantlab/next";
162
+ import experiments from "../experiments.json";
163
+
164
+ export async function getServerSideProps({ req, res }) {
165
+ const variant = getVariantSSR("cta-copy", req, experiments);
166
+ return {
167
+ props: {
168
+ variantlab: { "cta-copy": variant },
169
+ },
170
+ };
171
+ }
172
+ ```
173
+
174
+ ### Pitfalls
175
+
176
+ - `getStaticProps` cannot run variant assignment (no cookies). Users must use `getServerSideProps` or client-side assignment.
177
+
178
+ ---
179
+
180
+ ## Remix
181
+
182
+ Remix uses loaders for server-side data fetching. Loaders run on every request, making them ideal for variant assignment.
183
+
184
+ ### Architecture
185
+
186
+ - A root loader reads cookies, creates an initial variant map, returns it
187
+ - `<VariantLabProvider>` wraps the root
188
+ - Action functions can also read variants
189
+
190
+ ### Example
191
+
192
+ ```tsx
193
+ // app/root.tsx
194
+ import { json } from "@remix-run/node";
195
+ import { useLoaderData } from "@remix-run/react";
196
+ import { VariantLabProvider } from "@variantlab/remix";
197
+ import experiments from "./experiments.json";
198
+ import { parseVariantCookie } from "@variantlab/remix/server";
199
+
200
+ export async function loader({ request }) {
201
+ const cookieHeader = request.headers.get("Cookie") ?? "";
202
+ const initialVariants = parseVariantCookie(cookieHeader);
203
+ return json({ initialVariants });
204
+ }
205
+
206
+ export default function App() {
207
+ const { initialVariants } = useLoaderData<typeof loader>();
208
+ return (
209
+ <VariantLabProvider config={experiments} initialVariants={initialVariants}>
210
+ <Outlet />
211
+ </VariantLabProvider>
212
+ );
213
+ }
214
+ ```
215
+
216
+ ### Pitfalls
217
+
218
+ - Remix nested routes mean the provider sits at the root. Inner routes cannot create their own provider without duplicating state.
219
+ - Action functions that redirect must re-set the variant cookie, otherwise the next loader will re-assign.
220
+
221
+ ---
222
+
223
+ ## SvelteKit
224
+
225
+ SvelteKit uses `load` functions (server and client) and hooks.
226
+
227
+ ### Architecture
228
+
229
+ - `src/hooks.server.ts` uses `handle` to read cookies and set a local variant context
230
+ - `src/routes/+layout.server.ts` exposes the variant map via `event.locals.variantlab`
231
+ - `src/routes/+layout.svelte` wraps children in `<VariantLabProvider>`
232
+
233
+ ### Example
234
+
235
+ ```ts
236
+ // src/hooks.server.ts
237
+ import type { Handle } from "@sveltejs/kit";
238
+ import { createEngine } from "@variantlab/core";
239
+ import experiments from "../experiments.json";
240
+
241
+ export const handle: Handle = async ({ event, resolve }) => {
242
+ const engine = createEngine(experiments, { storage: event.cookies });
243
+ event.locals.variantlab = engine;
244
+ return resolve(event);
245
+ };
246
+ ```
247
+
248
+ ```svelte
249
+ <!-- src/routes/+layout.svelte -->
250
+ <script>
251
+ import { VariantLabProvider } from "@variantlab/svelte";
252
+ import experiments from "../experiments.json";
253
+ export let data;
254
+ </script>
255
+
256
+ <VariantLabProvider config={experiments} initialVariants={data.variantlab}>
257
+ <slot />
258
+ </VariantLabProvider>
259
+ ```
260
+
261
+ ### Pitfalls
262
+
263
+ - Svelte 4 vs Svelte 5 have different runes. We support both with separate entry points.
264
+ - `$app/stores` vs `$app/state` (Svelte 5) — we abstract over both.
265
+
266
+ ---
267
+
268
+ ## SolidStart
269
+
270
+ Solid is similar to React but uses fine-grained reactivity via signals.
271
+
272
+ ### Architecture
273
+
274
+ - `createVariantLabEngine` returns a signal-based store
275
+ - `useVariant` returns an accessor function (not a value directly)
276
+ - `<VariantLabProvider>` is a regular component
277
+
278
+ ### Pitfalls
279
+
280
+ - Solid's reactivity graph is different from React's. We must not use React's `useSyncExternalStore`-style pattern; instead we create signals.
281
+ - SSR in SolidStart uses `createAsync` and `isServer` checks. We handle both.
282
+
283
+ ---
284
+
285
+ ## Nuxt
286
+
287
+ Vue 3 meta-framework. Uses Nitro server routes and the Vue composition API.
288
+
289
+ ### Architecture
290
+
291
+ - `nuxt.config.ts` installs `@variantlab/nuxt` module
292
+ - Module auto-imports `useVariant()` composable
293
+ - Server-side: `event.context.variantlab` via Nitro middleware
294
+ - Client-side: `<VariantLabProvider>` via plugin
295
+
296
+ ### Pitfalls
297
+
298
+ - Nuxt's auto-import can cause name collisions. We prefix our composables with `useVariant*` to minimize conflict.
299
+ - Server and client must share the same engine state via `useState`-style hydration.
300
+
301
+ ---
302
+
303
+ ## Astro
304
+
305
+ Astro uses "islands" — interactive components inside a mostly-static HTML page. Different framework islands can coexist on the same page.
306
+
307
+ ### Architecture
308
+
309
+ - `astro.config.mjs` installs `@variantlab/astro` integration
310
+ - Pages and components read variants via `Astro.locals.variantlab` at build/request time
311
+ - Islands hydrate with initial variants passed as props
312
+
313
+ ### Pitfalls
314
+
315
+ - Each island is its own framework instance (React island, Vue island, etc.). Each needs its own provider hydration.
316
+ - SSG vs SSR: for static pages, variant selection happens at build time or at the edge middleware layer.
317
+
318
+ ---
319
+
320
+ ## React Native
321
+
322
+ No SSR. Simpler, but has its own quirks.
323
+
324
+ ### Architecture
325
+
326
+ - `<VariantLabProvider>` wraps the app
327
+ - Storage uses AsyncStorage / MMKV / SecureStore
328
+ - Debug overlay uses RN native components
329
+ - Deep link handler integrates with Expo Linking or React Navigation
330
+
331
+ ### Pitfalls
332
+
333
+ 1. **Hermes optimization**: Some JS features that work in V8 don't optimize well in Hermes. We avoid `Proxy` in hot paths.
334
+ 2. **New Architecture (Fabric) compatibility**: We test against both Old and New architectures.
335
+ 3. **Offline**: storage reads can fail. We fall back to in-memory defaults.
336
+ 4. **AsyncStorage vs MMKV**: AsyncStorage is async, MMKV is sync. We handle both in the `Storage` interface.
337
+ 5. **Expo Go vs bare**: some APIs (SecureStore) are available only in custom dev builds. We document this clearly.
338
+
339
+ ---
340
+
341
+ ## Edge runtime targets
342
+
343
+ ### Cloudflare Workers
344
+
345
+ - Web API subset (no Node built-ins)
346
+ - `crypto.subtle` available — our HMAC verification works out of the box
347
+ - KV storage for remote config caching
348
+ - 1 MB unzipped bundle limit — our 3 KB target is well within
349
+
350
+ ### Vercel Edge
351
+
352
+ - Similar to Cloudflare Workers
353
+ - `NextRequest` / `NextResponse` APIs
354
+ - Middleware runs at the edge — perfect for cookie-based assignment
355
+
356
+ ### Deno Deploy
357
+
358
+ - Full Deno runtime
359
+ - `crypto.subtle` available
360
+ - Standard Web Fetch API
361
+
362
+ ### Bun
363
+
364
+ - Mostly Node-compatible
365
+ - Bun's fast `Bun.hash` can optionally be used for sticky assignment (gated behind a check)
366
+
367
+ ### AWS Lambda@Edge
368
+
369
+ - Node-based but runs at CloudFront edge
370
+ - Limited execution time (low milliseconds)
371
+ - We fit within the constraints
372
+
373
+ ---
374
+
375
+ ## Implementation priority
376
+
377
+ Based on framework popularity and user demand, we'll implement SSR support in this order:
378
+
379
+ 1. **Next.js App Router** (Phase 1) — largest audience
380
+ 2. **Next.js Pages Router** (Phase 1) — still widely used
381
+ 3. **React Native** (Phase 1) — primary audience for our debug overlay killer feature
382
+ 4. **Remix** (Phase 2)
383
+ 5. **SvelteKit** (Phase 3)
384
+ 6. **SolidStart** (Phase 3)
385
+ 7. **Nuxt** (Phase 3)
386
+ 8. **Astro** (Phase 3)
387
+
388
+ ---
389
+
390
+ ## See also
391
+
392
+ - [`API.md`](../../API.md#variantlabnext) — the SSR API surface
393
+ - [`docs/features/hmac-signing.md`](../features/hmac-signing.md) — Web Crypto API usage
394
+ - [`ARCHITECTURE.md`](../../ARCHITECTURE.md#runtime-data-flow) — engine data flow
@@ -0,0 +1,238 @@
1
+ # Naming rationale
2
+
3
+ Why `variantlab`, what we considered, and what still needs validation.
4
+
5
+ ## Table of contents
6
+
7
+ - [Current pick](#current-pick)
8
+ - [Requirements](#requirements)
9
+ - [Candidates considered](#candidates-considered)
10
+ - [Why variantlab won](#why-variantlab-won)
11
+ - [Risks](#risks)
12
+ - [Final validation checklist](#final-validation-checklist)
13
+
14
+ ---
15
+
16
+ ## Current pick
17
+
18
+ **`variantlab`** — npm scope `@variantlab/*`, domain `variantlab.dev`.
19
+
20
+ The name captures three things:
21
+
22
+ 1. **Variant** — the unit of work. Not "flags", not "experiments", not "tests". A variant is a thing a user sees.
23
+ 2. **Lab** — a place for controlled experimentation. Evokes the scientific rigor we want to bring to UX.
24
+ 3. **Neutral** — works for feature flags, A/B tests, gradual rollouts, and UX experiments alike.
25
+
26
+ ---
27
+
28
+ ## Requirements
29
+
30
+ Before deciding, we listed what the name had to support:
31
+
32
+ 1. **Short enough to type** — under 12 characters ideally
33
+ 2. **Available on npm** — the `@name/*` scope must be free
34
+ 3. **Available domain** — `.dev` or `.io` at minimum
35
+ 4. **Available on GitHub** — organization name free
36
+ 5. **Pronounceable** — non-native English speakers can say it
37
+ 6. **Not a trademark** — no existing company with a clashing name
38
+ 7. **Memorable** — sticks in the head
39
+ 8. **Searchable** — has unique search results, not drowned by a common word
40
+ 9. **Extensible** — `@name/core`, `@name/react`, etc. all read well
41
+ 10. **Neutral connotation** — no religion, no politics, no culture-specific meanings
42
+
43
+ ---
44
+
45
+ ## Candidates considered
46
+
47
+ We went through ~30 names. Here are the serious contenders:
48
+
49
+ ### `variantlab`
50
+
51
+ **Pros**: Captures the concept, neutral, scientific, 10 chars, `@variantlab/react` reads well.
52
+ **Cons**: Slightly generic, may be confused with lab equipment brands.
53
+ **npm `@variantlab` scope**: needs to be claimed.
54
+ **variantlab.dev**: needs to be registered.
55
+
56
+ ### `flagkit`
57
+
58
+ **Pros**: Very short (7 chars), memorable, "kit" implies bundled tools.
59
+ **Cons**: "flag" has political/territorial connotations that could be misread. Narrows the concept to feature flags when we want to cover A/B testing, gradual rollout, and UI experiments.
60
+ **Verdict**: Too narrow.
61
+
62
+ ### `shift`
63
+
64
+ **Pros**: 5 chars, bold, evokes "shifting variants".
65
+ **Cons**: Almost certainly taken on npm. Overloaded meaning (keyboard shift, shift left, etc.).
66
+ **Verdict**: Too common.
67
+
68
+ ### `omniflag`
69
+
70
+ **Pros**: "Omni" captures universal framework support.
71
+ **Cons**: 8 chars + "flag" issue. Sounds like an enterprise brand.
72
+ **Verdict**: Too corporate.
73
+
74
+ ### `polyvariant`
75
+
76
+ **Pros**: Scientific, captures multi-variant testing, unique.
77
+ **Cons**: 11 chars + hard to spell + "poly" sounds prefix-y.
78
+ **Verdict**: Too academic.
79
+
80
+ ### `labkit`
81
+
82
+ **Pros**: Short, evokes tooling.
83
+ **Cons**: Almost certainly taken (common laboratory supply brand).
84
+ **Verdict**: Trademark risk.
85
+
86
+ ### `flaglab`
87
+
88
+ **Pros**: Combines "flag" and "lab".
89
+ **Cons**: Narrow (see flag issue), and npm scope likely taken.
90
+ **Verdict**: Ruled out.
91
+
92
+ ### `expt`
93
+
94
+ **Pros**: 4 chars, techy.
95
+ **Cons**: Looks like a typo. Hard to pronounce.
96
+ **Verdict**: Too cute.
97
+
98
+ ### `variox`
99
+
100
+ **Pros**: Unique, 6 chars, no existing trademark.
101
+ **Cons**: Invented word, not immediately meaningful.
102
+ **Verdict**: Backup option.
103
+
104
+ ### `hypothesize`
105
+
106
+ **Pros**: Scientific, unique.
107
+ **Cons**: 11 chars, hard to type repeatedly.
108
+ **Verdict**: Too long.
109
+
110
+ ### `tryout`
111
+
112
+ **Pros**: Short, friendly, captures "trying variants".
113
+ **Cons**: Lacks technical gravitas, probably taken.
114
+ **Verdict**: Too casual.
115
+
116
+ ### `abx`
117
+
118
+ **Pros**: 3 chars, technical.
119
+ **Cons**: Mysterious, not memorable, possibly medical connotation (antibiotic).
120
+ **Verdict**: Too cryptic.
121
+
122
+ ### `testlab`
123
+
124
+ **Pros**: Self-explanatory.
125
+ **Cons**: Generic, "test" is overloaded with unit testing. Probably taken.
126
+ **Verdict**: Too generic.
127
+
128
+ ### `switchboard`
129
+
130
+ **Pros**: Evokes a control panel.
131
+ **Cons**: 11 chars, dated metaphor.
132
+ **Verdict**: Too long.
133
+
134
+ ### `stageset`
135
+
136
+ **Pros**: Evokes staging variants.
137
+ **Cons**: Unclear meaning, 8 chars.
138
+ **Verdict**: Confusing.
139
+
140
+ ### `exposure`
141
+
142
+ **Pros**: Technical term from experimentation (exposure events).
143
+ **Cons**: 8 chars, also has negative connotations (exposure to risk, exposure of data).
144
+ **Verdict**: Mixed signal.
145
+
146
+ ---
147
+
148
+ ## Why variantlab won
149
+
150
+ Looking at the shortlist:
151
+
152
+ | Name | Length | Meaningful | Neutral | Extensible | Available? |
153
+ |---|---:|:-:|:-:|:-:|:-:|
154
+ | variantlab | 10 | ✅ | ✅ | ✅ | TBD |
155
+ | polyvariant | 11 | ✅ | ✅ | ✅ | Likely |
156
+ | variox | 6 | Partial | ✅ | ✅ | Likely |
157
+ | flagkit | 7 | Narrow | Neutral | ✅ | TBD |
158
+ | shift | 5 | Partial | ✅ | ❌ | Unlikely |
159
+
160
+ `variantlab` edges out `polyvariant` on pronounceability and `variox` on meaning. It beats `flagkit` by not pigeonholing us into feature flags.
161
+
162
+ The package-family reads nicely:
163
+
164
+ - `@variantlab/core`
165
+ - `@variantlab/react`
166
+ - `@variantlab/react-native`
167
+ - `@variantlab/next`
168
+ - `@variantlab/cli`
169
+ - `@variantlab/devtools`
170
+
171
+ The tagline writes itself: *"Every framework. Zero lock-in. One lab."*
172
+
173
+ ---
174
+
175
+ ## Risks
176
+
177
+ ### Risk 1 — npm scope taken
178
+
179
+ We need to verify `@variantlab` is free on npm before committing. If it's taken but inactive, we might be able to request it via npm support. If actively used, we need a backup.
180
+
181
+ **Backup plan**: If `@variantlab` is unavailable, fall back to `@variox/*` or `@lab-variant/*`.
182
+
183
+ ### Risk 2 — trademark conflict
184
+
185
+ "Variant Lab" may exist as a company name in biotech or marketing. We need a trademark check before launching publicly.
186
+
187
+ **Backup plan**: Add a distinctive suffix if needed (`variantlab.dev` domain clarifies context).
188
+
189
+ ### Risk 3 — SEO
190
+
191
+ "variant" and "lab" are both common words. Initial Google searches may be noisy. We plan to claim the name via:
192
+
193
+ - GitHub organization
194
+ - npm scope
195
+ - `.dev` domain
196
+ - Twitter / Mastodon / Bluesky handles
197
+ - Product Hunt launch
198
+
199
+ ### Risk 4 — Misinterpretation
200
+
201
+ Some readers might assume "lab" means unstable or experimental (like Chrome flags). We mitigate by emphasizing "stable once shipped" in the tagline and README.
202
+
203
+ ---
204
+
205
+ ## Final validation checklist
206
+
207
+ Before we commit to the name in code:
208
+
209
+ - [ ] Verify `@variantlab` is free on npm
210
+ - [ ] Register `variantlab.dev`
211
+ - [ ] Create GitHub organization `variantlab`
212
+ - [ ] Reserve handles on Twitter, Bluesky, Mastodon, LinkedIn
213
+ - [ ] Run a trademark search in US, EU, and India
214
+ - [ ] Socialize the name in a GitHub discussion for community feedback
215
+ - [ ] Verify no existing OSS project uses the name actively
216
+
217
+ If any of these fail, we fall back to the shortlist in order: `polyvariant` → `variox` → a new brainstorm.
218
+
219
+ ---
220
+
221
+ ## Fallback: invent a word
222
+
223
+ If all pre-existing candidates fail, we invent one. Requirements for an invented name:
224
+
225
+ - 5-8 characters
226
+ - Pronounceable in English, Spanish, and Hindi
227
+ - No existing meaning in any major language (verified via Wiktionary)
228
+ - Clean trademark search
229
+ - `.dev` domain available
230
+ - Short npm scope available
231
+
232
+ Candidates if we go this route: `varex`, `expio`, `experi`, `splitly`, `sprou`.
233
+
234
+ ---
235
+
236
+ ## Decision
237
+
238
+ **Go with `variantlab` pending the validation checklist.** If blocked, revisit this document.