@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,430 @@
1
+ # Architecture
2
+
3
+ This document describes the monorepo layout, build tooling, runtime architecture, data flow, and size budgets for variantlab.
4
+
5
+ ## Table of contents
6
+
7
+ - [Design goals](#design-goals)
8
+ - [Monorepo layout](#monorepo-layout)
9
+ - [Package boundaries](#package-boundaries)
10
+ - [Runtime data flow](#runtime-data-flow)
11
+ - [Build tooling](#build-tooling)
12
+ - [Size budgets](#size-budgets)
13
+ - [Dependency policy](#dependency-policy)
14
+ - [Versioning and release strategy](#versioning-and-release-strategy)
15
+ - [Testing strategy](#testing-strategy)
16
+ - [CI/CD pipeline](#cicd-pipeline)
17
+
18
+ ---
19
+
20
+ ## Design goals
21
+
22
+ 1. **Core runs anywhere.** Any ECMAScript 2020 environment — Node 18+, Deno, Bun, browsers, React Native Hermes, Cloudflare Workers, Vercel Edge, AWS Lambda@Edge. Zero platform APIs in core.
23
+ 2. **Adapters are trivially small.** Each framework adapter should be < 200 source LOC and < 2 KB gzipped.
24
+ 3. **Tree-shakeable everything.** Every export lives in its own module file. Unused code is eliminated at build time.
25
+ 4. **No implicit IO.** Core never reads from disk, network, or global storage on its own. All IO happens through injected adapters (Storage, Fetcher, Telemetry).
26
+ 5. **Deterministic at hash boundaries.** User bucketing uses a stable hash. The same `userId` + experiment produces the same variant across every machine, every runtime, every language.
27
+ 6. **Forward-compatible config schema.** `experiments.json` has a `version` field. The engine refuses to load a config from a newer major version, warns on a newer minor, and upgrades older configs in memory.
28
+
29
+ ---
30
+
31
+ ## Monorepo layout
32
+
33
+ ```
34
+ variantlab/
35
+ ├── README.md
36
+ ├── ARCHITECTURE.md
37
+ ├── API.md
38
+ ├── SECURITY.md
39
+ ├── ROADMAP.md
40
+ ├── LICENSE
41
+ ├── CONTRIBUTING.md
42
+ ├── experiments.schema.json
43
+ ├── package.json # pnpm workspace root
44
+ ├── pnpm-workspace.yaml
45
+ ├── tsconfig.base.json
46
+ ├── .changeset/ # changesets for versioning
47
+ ├── .github/
48
+ │ └── workflows/
49
+ │ ├── ci.yml # lint, test, size-limit
50
+ │ ├── release.yml # changesets publish
51
+ │ ├── sigstore.yml # sigstore signing
52
+ │ └── compat-matrix.yml # test against Node 18/20/22, React 18/19
53
+ ├── docs/
54
+ │ ├── research/
55
+ │ ├── design/
56
+ │ ├── features/
57
+ │ ├── phases/
58
+ │ └── adapters/
59
+ ├── packages/
60
+ │ ├── core/ # @variantlab/core
61
+ │ │ ├── src/
62
+ │ │ │ ├── engine.ts
63
+ │ │ │ ├── types.ts
64
+ │ │ │ ├── storage.ts
65
+ │ │ │ ├── targeting.ts
66
+ │ │ │ ├── assignment.ts
67
+ │ │ │ ├── hash.ts
68
+ │ │ │ ├── schema.ts
69
+ │ │ │ ├── crypto.ts
70
+ │ │ │ ├── errors.ts
71
+ │ │ │ └── index.ts
72
+ │ │ ├── test/
73
+ │ │ ├── package.json
74
+ │ │ └── tsup.config.ts
75
+ │ ├── react/ # @variantlab/react
76
+ │ ├── react-native/ # @variantlab/react-native
77
+ │ ├── next/ # @variantlab/next
78
+ │ ├── remix/ # @variantlab/remix (phase 2)
79
+ │ ├── vue/ # @variantlab/vue (phase 2)
80
+ │ ├── vanilla/ # @variantlab/vanilla (phase 2)
81
+ │ ├── svelte/ # @variantlab/svelte (phase 3)
82
+ │ ├── solid/ # @variantlab/solid (phase 3)
83
+ │ ├── astro/ # @variantlab/astro (phase 3)
84
+ │ ├── nuxt/ # @variantlab/nuxt (phase 3)
85
+ │ ├── storybook/ # @variantlab/storybook (phase 3)
86
+ │ ├── eslint-plugin/ # @variantlab/eslint-plugin (phase 3)
87
+ │ ├── test-utils/ # @variantlab/test-utils (phase 3)
88
+ │ ├── devtools/ # @variantlab/devtools (phase 2, browser ext)
89
+ │ └── cli/ # @variantlab/cli
90
+ ├── apps/
91
+ │ ├── docs/ # Astro Starlight docs site
92
+ │ ├── playground/ # browser sandbox (paste config, see it work)
93
+ │ └── examples/
94
+ │ ├── next-app-router/
95
+ │ ├── next-pages/
96
+ │ ├── expo-router/
97
+ │ ├── react-native-cli/
98
+ │ ├── remix/
99
+ │ ├── vite-react/
100
+ │ ├── nuxt/
101
+ │ ├── sveltekit/
102
+ │ ├── solid-start/
103
+ │ └── astro/
104
+ └── tools/
105
+ ├── size-limit.config.js
106
+ └── scripts/
107
+ ├── check-deps.js # verify 0 runtime deps
108
+ └── check-bundle-size.js
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Package boundaries
114
+
115
+ ### `@variantlab/core`
116
+
117
+ The engine. Zero dependencies. Exports:
118
+
119
+ - `VariantEngine` class — the runtime state machine
120
+ - `createEngine(config, options)` factory
121
+ - `Storage` interface — pluggable persistence
122
+ - `Fetcher` interface — optional remote config loader
123
+ - `Telemetry` interface — optional event sink
124
+ - `Targeting` utilities — predicate evaluation
125
+ - `Assignment` strategies — default, random, sticky-hash, weighted
126
+ - Type exports: `Experiment`, `Variant`, `VariantContext`, `ExperimentsConfig`
127
+
128
+ **Allowed globals**: `crypto` (Web Crypto API), `Date`, `Math`. Nothing else. No `document`, no `window`, no `localStorage`, no `fetch`, no `process`.
129
+
130
+ **Runtime target**: ES2020. No `??=`, no top-level await, no decorators.
131
+
132
+ ### `@variantlab/react`
133
+
134
+ React bindings. Depends on `@variantlab/core` and nothing else (React is a peer dep). Exports:
135
+
136
+ - `<VariantLabProvider config={...}>` — context provider
137
+ - `<Variant experimentId="...">` — render-prop switcher
138
+ - `<VariantValue experimentId="...">` — render-prop for value experiments
139
+ - `useVariant(id)` — hook returning the current variant ID
140
+ - `useVariantValue<T>(id)` — hook returning the variant's value
141
+ - `useExperiment(id)` — hook returning `{ variant, value, track }`
142
+ - `useSetVariant()` — hook to imperatively override a variant (dev only by default)
143
+ - `<VariantDebugOverlay />` — floating debug picker (tree-shaken in production builds)
144
+ - `<VariantErrorBoundary experimentId="...">` — error boundary that reports crashes to the engine
145
+
146
+ **React version**: 18.2+ and 19. Uses `useSyncExternalStore` for concurrent-mode safety.
147
+
148
+ ### `@variantlab/react-native`
149
+
150
+ React Native + Expo bindings. Depends on `@variantlab/core`, peer-depends on `react-native` and optionally `@react-native-async-storage/async-storage`, `react-native-mmkv`, `expo-secure-store`. Exports the same surface as `@variantlab/react` plus:
151
+
152
+ - Default `AsyncStorageAdapter`, `MMKVStorageAdapter`, `SecureStoreAdapter`
153
+ - `<VariantDebugOverlay>` with RN-native UI (floating button, bottom sheet, QR modal, shake-to-open)
154
+ - `useRouteAwareExperiments()` — filters experiments by current Expo Router / React Navigation route
155
+ - Deep-link handler for `myapp://variantlab?set=...`
156
+
157
+ ### `@variantlab/next`
158
+
159
+ Next.js 14 and 15 bindings. Supports both App Router and Pages Router. Exports:
160
+
161
+ - `createVariantLabServer(config)` — SSR-aware engine factory
162
+ - `<VariantLabProvider>` — client component version (re-exports from `@variantlab/react`)
163
+ - `getVariantSSR(experimentId, req)` — server helper for App Router loaders and Pages `getServerSideProps`
164
+ - `variantLabMiddleware(config)` — Next.js middleware that sets a sticky cookie
165
+ - React Server Component support for reading variants on the server
166
+
167
+ ### `@variantlab/cli`
168
+
169
+ Command-line tool. Dev dependency only, never ships to production. Exports binary `variantlab`:
170
+
171
+ - `variantlab init` — scaffold `experiments.json` + install recommended adapter
172
+ - `variantlab generate` — codegen `.d.ts` from `experiments.json`
173
+ - `variantlab validate` — validate config against schema + check for orphaned IDs
174
+ - `variantlab scaffold <experiment-id>` — scaffold boilerplate for a new experiment
175
+
176
+ **Runtime**: Node 18+. The CLI is allowed dependencies (`commander`, `chalk`) — they do not affect the runtime packages.
177
+
178
+ ---
179
+
180
+ ## Runtime data flow
181
+
182
+ ```
183
+ ┌─────────────────────────────────────────────────────────────────┐
184
+ │ Application code (framework) │
185
+ │ │
186
+ │ useVariant("x") <Variant experimentId="x"> track(...) │
187
+ └──────────┬──────────────────────┬──────────────────────┬────────┘
188
+ │ │ │
189
+ │ │ │
190
+ ┌──────────▼──────────────────────▼──────────────────────▼────────┐
191
+ │ Framework adapter │
192
+ │ (@variantlab/react, /next, ...) │
193
+ │ │
194
+ │ React Context │ Hooks │ SSR helpers │ Debug overlay │
195
+ └──────────┬──────────────────────────────────────────────────────┘
196
+
197
+ │ subscribe / getVariant / setVariant / trackEvent
198
+
199
+ ┌──────────▼──────────────────────────────────────────────────────┐
200
+ │ @variantlab/core │
201
+ │ │
202
+ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
203
+ │ │ Engine │──│ Targeting │──│ Assignment │──│ Schema │ │
204
+ │ │ │ │ │ │ │ │ validator│ │
205
+ │ └─────┬──────┘ └────────────┘ └────────────┘ └──────────┘ │
206
+ │ │ │
207
+ │ │ │
208
+ │ ┌─────▼──────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
209
+ │ │ Storage │ │ Fetcher │ │ Telemetry │ │ Crypto │ │
210
+ │ │ (injected) │ │ (injected) │ │ (injected) │ │ (WebAPI) │ │
211
+ │ └────────────┘ └────────────┘ └────────────┘ └──────────┘ │
212
+ └─────────────────────────────────────────────────────────────────┘
213
+ ```
214
+
215
+ ### Resolve variant (hot path)
216
+
217
+ Called on every `useVariant()` read. Must be O(1).
218
+
219
+ 1. Adapter calls `engine.getVariant(experimentId, context)`.
220
+ 2. Engine checks in-memory override map (dev/debug overrides win).
221
+ 3. Engine checks Storage for a persisted assignment.
222
+ 4. If none, engine evaluates targeting predicates against `context`.
223
+ 5. If targeting passes, engine runs the assignment strategy (sticky-hash/weighted/default).
224
+ 6. Engine writes the result to Storage and memoizes.
225
+ 7. Engine emits an `onAssignment` event to Telemetry (first time only per session).
226
+ 8. Returns variant ID.
227
+
228
+ ### Load config (cold start)
229
+
230
+ 1. App mounts provider with inline config or async `Fetcher`.
231
+ 2. Engine validates config against `experiments.schema.json` (hand-rolled validator, no zod).
232
+ 3. If HMAC signature is present, engine verifies via Web Crypto `crypto.subtle.verify`.
233
+ 4. Engine hydrates Storage — reads all previously persisted assignments.
234
+ 5. Engine emits `onReady` event.
235
+
236
+ ### Override flow (dev / QA)
237
+
238
+ 1. User taps a variant in `<VariantDebugOverlay>`, or deep link fires, or QR is scanned.
239
+ 2. Adapter calls `engine.setVariant(experimentId, variantId)`.
240
+ 3. Engine writes override to Storage with priority flag.
241
+ 4. Engine emits `onVariantChanged`.
242
+ 5. All subscribed components re-render via `useSyncExternalStore`.
243
+
244
+ ### Crash rollback flow
245
+
246
+ 1. `<VariantErrorBoundary experimentId="x">` catches an error.
247
+ 2. Adapter calls `engine.reportCrash(experimentId, error)`.
248
+ 3. Engine increments a crash counter in Storage scoped to `(experimentId, variantId, sessionId)`.
249
+ 4. If counter exceeds `rollbackThreshold` within `rollbackWindow`, engine:
250
+ - Clears the persisted assignment
251
+ - Forces the user to the experiment's `default` variant
252
+ - Marks this variant as "quarantined" for the session
253
+ - Emits `onRollback` event
254
+
255
+ Full details in [`docs/features/crash-rollback.md`](./docs/features/crash-rollback.md).
256
+
257
+ ---
258
+
259
+ ## Build tooling
260
+
261
+ | Tool | Purpose | Why |
262
+ |---|---|---|
263
+ | **pnpm** | Package manager + workspace | Fast, disk-efficient, strict hoisting |
264
+ | **tsup** | Bundle libraries | Fast (esbuild), dual ESM+CJS, `.d.ts` generation |
265
+ | **TypeScript 5.6+** | Type checking | Latest language features, strict mode |
266
+ | **Vitest** | Unit + integration tests | Vite-native, fast, TypeScript-first, works in Node + browser |
267
+ | **Playwright** | E2E tests in example apps | Cross-browser, cross-platform |
268
+ | **size-limit** | Bundle size enforcement | CI-blocking on budget violations |
269
+ | **Changesets** | Versioning + changelog | Per-package semver with provenance |
270
+ | **Biome** | Lint + format | 30x faster than ESLint/Prettier, single binary |
271
+ | **Astro Starlight** | Docs site | Fast, SEO-friendly, native MDX |
272
+ | **sigstore** | Release signing | Supply-chain integrity |
273
+
274
+ ### Why not Turbopack / Nx / Rush?
275
+
276
+ - **Turbopack** — not stable for libraries yet
277
+ - **Nx** — too heavy for our use case; we don't need task graphs across dozens of apps
278
+ - **Rush** — unnecessary ceremony for a small monorepo
279
+ - **Bun workspaces** — too young, not yet compatible with `changesets`
280
+
281
+ pnpm + tsup + changesets is the sweet spot for publishing libraries in 2025.
282
+
283
+ ### Why Biome over ESLint + Prettier?
284
+
285
+ - One binary, one config, 30x faster
286
+ - Zero JS plugin runtime (everything is Rust-native)
287
+ - Reduces CI minutes significantly in a monorepo of 10+ packages
288
+
289
+ ---
290
+
291
+ ## Size budgets
292
+
293
+ Enforced in CI via `size-limit`. PRs that exceed the budget are blocked.
294
+
295
+ | Package | Budget (gzipped) | Notes |
296
+ |---|---:|---|
297
+ | `@variantlab/core` | **3.0 KB** | Zero dependencies, pure TS |
298
+ | `@variantlab/react` | **1.5 KB** | Excludes core |
299
+ | `@variantlab/react-native` | **4.0 KB** | Includes debug overlay (tree-shakable in production) |
300
+ | `@variantlab/next` | **2.0 KB** | Excludes core, excludes React |
301
+ | `@variantlab/remix` | **1.5 KB** | |
302
+ | `@variantlab/vue` | **1.5 KB** | |
303
+ | `@variantlab/svelte` | **1.0 KB** | Svelte compiles away most of the runtime |
304
+ | `@variantlab/solid` | **1.0 KB** | |
305
+ | `@variantlab/vanilla` | **0.5 KB** | Just the hook + engine re-export |
306
+
307
+ **Debug overlay is always tree-shaken in production builds.** We use a dev-only import pattern:
308
+
309
+ ```ts
310
+ // @variantlab/react-native exports:
311
+ export { VariantDebugOverlay } from "./debug/overlay";
312
+
313
+ // Production usage — bundler drops the whole module:
314
+ import { VariantDebugOverlay } from "@variantlab/react-native";
315
+ {process.env.NODE_ENV !== "production" && <VariantDebugOverlay />}
316
+ ```
317
+
318
+ Additional strategy: the overlay lives in its own entry point `@variantlab/react-native/debug` so users can import it only in dev.
319
+
320
+ ---
321
+
322
+ ## Dependency policy
323
+
324
+ ### Runtime dependencies
325
+
326
+ - **Core package**: **zero** runtime dependencies, forever. Enforced by `tools/scripts/check-deps.js` in CI.
327
+ - **Adapter packages**: may list `@variantlab/core` as the only runtime dep. Everything else is peer.
328
+ - **Peer dependencies**: framework itself (React, Vue, etc.) and optional integrations (`react-native-mmkv`, `@react-native-async-storage/async-storage`, etc.).
329
+
330
+ ### Dev dependencies
331
+
332
+ - Build tooling (`tsup`, `typescript`, `biome`) is shared at the workspace root.
333
+ - Per-package dev deps are allowed when framework-specific (e.g., `@testing-library/react` in `react`).
334
+
335
+ ### Why this matters
336
+
337
+ Every runtime dependency is a supply-chain attack vector, a potential version conflict, and added bundle size. By refusing runtime deps we:
338
+
339
+ - Reduce audit surface to zero for `@variantlab/core`
340
+ - Eliminate dependency version conflicts
341
+ - Enable safe use in any JavaScript runtime
342
+ - Prevent transitive bloat
343
+
344
+ See [`docs/research/bundle-size-analysis.md`](./docs/research/bundle-size-analysis.md) and [`SECURITY.md`](./SECURITY.md).
345
+
346
+ ---
347
+
348
+ ## Versioning and release strategy
349
+
350
+ - **Semver strict.** Breaking API changes require a major version bump in every affected package.
351
+ - **Changesets** manages per-package versioning. Each PR with a public API change must include a changeset.
352
+ - **Lock-step releases** for `@variantlab/core` and all adapters during 0.x, so users don't mismatch versions.
353
+ - **Provenance**: every npm publish is signed via `npm publish --provenance` and logged to the public Sigstore transparency log.
354
+ - **SBOM**: a CycloneDX SBOM is generated on release and attached to the GitHub release.
355
+
356
+ ### Release cadence
357
+
358
+ - **Patch**: as needed, within 48 hours of a verified bug fix
359
+ - **Minor**: every 2-4 weeks in the 0.x phase, then monthly post-1.0
360
+ - **Major**: only with a 30-day RFC period and a migration guide
361
+
362
+ ---
363
+
364
+ ## Testing strategy
365
+
366
+ | Layer | Tool | Coverage target |
367
+ |---|---|---|
368
+ | Unit (core engine) | Vitest | 95%+ |
369
+ | Unit (adapters) | Vitest + framework testing libs | 90%+ |
370
+ | Integration (example apps) | Playwright | Smoke tests pass |
371
+ | Property-based (hash, assignment) | fast-check (dev only) | Invariants hold |
372
+ | Fuzz (schema validator) | Custom fuzzer | No crashes on malformed input |
373
+ | Compat matrix | GitHub Actions | Node 18/20/22, React 18/19, RN 0.74+ |
374
+ | Bundle size | size-limit | All packages under budget |
375
+ | Type coverage | `tsc --strict` + `typescript-coverage-report` | 100% |
376
+
377
+ **Test philosophy**: every bug fix must come with a regression test. Every public API must have an integration test in an example app.
378
+
379
+ ---
380
+
381
+ ## CI/CD pipeline
382
+
383
+ ### `ci.yml` (on every PR)
384
+
385
+ 1. Install pnpm + dependencies
386
+ 2. Lint + format check (Biome)
387
+ 3. Type check (`tsc --noEmit`)
388
+ 4. Unit tests (Vitest)
389
+ 5. Build all packages (tsup)
390
+ 6. Check bundle sizes (size-limit)
391
+ 7. Check zero-dep policy (custom script)
392
+ 8. Run example app smoke tests (Playwright)
393
+ 9. Generate coverage report
394
+
395
+ ### `release.yml` (on merge to main)
396
+
397
+ 1. Detect pending changesets
398
+ 2. Open or update "Version Packages" PR
399
+ 3. On merge, publish with `npm publish --provenance`
400
+ 4. Sign release with sigstore
401
+ 5. Generate SBOM
402
+ 6. Create GitHub release with changelog
403
+ 7. Update docs site
404
+
405
+ ### `compat-matrix.yml` (nightly)
406
+
407
+ Tests against:
408
+
409
+ - Node 18, 20, 22
410
+ - React 18.2, 18.3, 19.0
411
+ - React Native 0.74, 0.75, 0.76
412
+ - Next.js 14, 15
413
+ - Vue 3.4, 3.5
414
+ - Svelte 4, 5
415
+ - Solid 1.8, 1.9
416
+
417
+ Fails the build if any combination breaks. Prevents silent regressions.
418
+
419
+ ### `sigstore.yml` (on release tag)
420
+
421
+ Signs all published packages and updates provenance attestations.
422
+
423
+ ---
424
+
425
+ ## See also
426
+
427
+ - [`API.md`](./API.md) — complete TypeScript API surface
428
+ - [`SECURITY.md`](./SECURITY.md) — threat model and mitigations
429
+ - [`docs/research/bundle-size-analysis.md`](./docs/research/bundle-size-analysis.md) — how we hit the size budgets
430
+ - [`docs/design/api-philosophy.md`](./docs/design/api-philosophy.md) — API design decisions
@@ -0,0 +1,264 @@
1
+ # Contributing to variantlab
2
+
3
+ Thank you for considering a contribution. variantlab is currently in Phase 0 (Foundation) — we are actively seeking contributors who want to shape the API before a single line of code is written.
4
+
5
+ ## Table of contents
6
+
7
+ - [Current phase](#current-phase)
8
+ - [Ways to contribute](#ways-to-contribute)
9
+ - [Code of conduct](#code-of-conduct)
10
+ - [Before you start coding](#before-you-start-coding)
11
+ - [Development setup](#development-setup)
12
+ - [Pull request process](#pull-request-process)
13
+ - [Commit conventions](#commit-conventions)
14
+ - [Changesets](#changesets)
15
+ - [Documentation changes](#documentation-changes)
16
+ - [Security disclosures](#security-disclosures)
17
+
18
+ ---
19
+
20
+ ## Current phase
21
+
22
+ **We are in Phase 0: Foundation.** This means:
23
+
24
+ - **No production code exists yet.** If you open a PR adding a `src/` file, we will close it with a pointer to this document.
25
+ - All work is on documentation, design, and API surface.
26
+ - The most valuable contributions right now are reviews, critiques, and proposals on the existing docs.
27
+
28
+ When Phase 1 begins, this document will be updated to describe the coding workflow.
29
+
30
+ See [`ROADMAP.md`](./ROADMAP.md) for the current phase and [`docs/phases/phase-0-foundation.md`](./docs/phases/phase-0-foundation.md) for detailed Phase 0 work.
31
+
32
+ ---
33
+
34
+ ## Ways to contribute
35
+
36
+ ### 1. Review the API surface
37
+
38
+ Read [`API.md`](./API.md) and open a GitHub discussion with:
39
+
40
+ - Parts that feel awkward
41
+ - Missing functionality
42
+ - Naming concerns
43
+ - Type-safety gaps
44
+ - Comparisons to how other tools solve the same problem
45
+
46
+ ### 2. Review the threat model
47
+
48
+ Read [`SECURITY.md`](./SECURITY.md) and open a discussion if you spot:
49
+
50
+ - Missing threats
51
+ - Weak mitigations
52
+ - Better alternatives to the proposed designs
53
+ - Privacy concerns we missed
54
+
55
+ ### 3. Propose a framework adapter
56
+
57
+ If your favorite framework is not yet planned, open a discussion with:
58
+
59
+ - Framework name and version
60
+ - How experiments would be consumed (hooks, components, composables, signals)
61
+ - SSR considerations
62
+ - Sample code using the planned API
63
+ - Estimated bundle size
64
+
65
+ We will evaluate and potentially promote it to a future phase.
66
+
67
+ ### 4. Contribute research
68
+
69
+ Add new content to `docs/research/`:
70
+
71
+ - Additional competitor analyses
72
+ - Framework-specific SSR quirks we missed
73
+ - Novel debug overlay patterns from other tools
74
+ - Security research papers relevant to client-side experimentation
75
+
76
+ ### 5. Test the proposed APIs by hand
77
+
78
+ The best feedback comes from trying to write real application code against the proposed API. Open a discussion with a code sample showing what works and what doesn't.
79
+
80
+ ---
81
+
82
+ ## Code of conduct
83
+
84
+ We follow a simple principle: **be kind, be specific, be useful**. Disrespectful, condescending, or harassing behavior will not be tolerated. We follow the [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
85
+
86
+ Report violations to `conduct@variantlab.dev`.
87
+
88
+ ---
89
+
90
+ ## Before you start coding
91
+
92
+ **Phase 0 has no coding.** Once Phase 1 begins:
93
+
94
+ 1. Check the [issue tracker](https://github.com/variantlab/variantlab/issues) for an open issue matching your idea, or create one
95
+ 2. Wait for a maintainer to label it `accepted` before you start work
96
+ 3. Comment on the issue to claim it
97
+ 4. Open a draft PR early so maintainers can guide you
98
+
99
+ This process prevents wasted effort on changes we can't merge.
100
+
101
+ ---
102
+
103
+ ## Development setup
104
+
105
+ *(Phase 1+ only — ignored during Phase 0.)*
106
+
107
+ ### Prerequisites
108
+
109
+ - Node 18.17+ (we test 18, 20, 22)
110
+ - pnpm 9+
111
+ - Git
112
+
113
+ ### Clone and install
114
+
115
+ ```bash
116
+ git clone https://github.com/variantlab/variantlab.git
117
+ cd variantlab
118
+ pnpm install
119
+ ```
120
+
121
+ ### Build everything
122
+
123
+ ```bash
124
+ pnpm build
125
+ ```
126
+
127
+ ### Run tests
128
+
129
+ ```bash
130
+ pnpm test # all packages
131
+ pnpm test --filter=@variantlab/core # one package
132
+ pnpm test:watch # watch mode
133
+ ```
134
+
135
+ ### Check bundle sizes
136
+
137
+ ```bash
138
+ pnpm size
139
+ ```
140
+
141
+ ### Lint + format
142
+
143
+ ```bash
144
+ pnpm check # biome check
145
+ pnpm fix # biome check --apply
146
+ ```
147
+
148
+ ### Run example app
149
+
150
+ ```bash
151
+ pnpm --filter=example-expo-router start
152
+ ```
153
+
154
+ ---
155
+
156
+ ## Pull request process
157
+
158
+ 1. **Fork** the repo and create a feature branch from `main`
159
+ 2. **Write tests** for your change. Every bug fix needs a regression test. Every feature needs at least one integration test.
160
+ 3. **Run the full test suite locally** before pushing: `pnpm build && pnpm test && pnpm size && pnpm check`
161
+ 4. **Add a changeset** if your PR touches public APIs: `pnpm changeset`
162
+ 5. **Update documentation** — relevant `API.md`, `README.md`, `docs/` files
163
+ 6. **Open a PR** with:
164
+ - A clear title (matches a commit convention — see below)
165
+ - A description explaining *what* and *why*
166
+ - Links to related issues/discussions
167
+ - Screenshots or GIFs for UI changes
168
+ - A checklist of changed packages
169
+ 7. **Respond to review feedback** promptly
170
+ 8. **Rebase on main** (do not merge main into your branch)
171
+ 9. Once approved, a maintainer will merge via squash
172
+
173
+ ### What we look for in review
174
+
175
+ - **API consistency** — does this match patterns elsewhere in variantlab?
176
+ - **Bundle size impact** — is the change within budget?
177
+ - **Type safety** — are the new types as strict as possible?
178
+ - **Test quality** — are the tests isolated, deterministic, fast?
179
+ - **Documentation** — is the change documented where users would look?
180
+ - **Security implications** — does this change any assumption in `SECURITY.md`?
181
+ - **SSR correctness** — does this change break any SSR guarantee?
182
+
183
+ ---
184
+
185
+ ## Commit conventions
186
+
187
+ We follow [Conventional Commits](https://www.conventionalcommits.org/):
188
+
189
+ ```
190
+ <type>(<scope>): <description>
191
+ ```
192
+
193
+ Types:
194
+
195
+ - `feat` — new feature
196
+ - `fix` — bug fix
197
+ - `docs` — documentation changes
198
+ - `refactor` — code change that neither adds a feature nor fixes a bug
199
+ - `perf` — performance improvement
200
+ - `test` — adding or updating tests
201
+ - `chore` — build, CI, tooling
202
+ - `security` — security fix (always shipped in the next patch)
203
+
204
+ Scopes:
205
+
206
+ - `core`, `react`, `react-native`, `next`, `remix`, `vue`, `svelte`, `solid`, `astro`, `nuxt`, `cli`, `devtools`, `docs`, `ci`
207
+
208
+ Examples:
209
+
210
+ ```
211
+ feat(react): add useRouteExperiments hook
212
+ fix(core): handle missing userId in stickyHash
213
+ docs(api): document VariantErrorBoundary props
214
+ security(core): prevent prototype pollution in schema validator
215
+ ```
216
+
217
+ ---
218
+
219
+ ## Changesets
220
+
221
+ Every PR that changes public APIs must include a changeset. Run:
222
+
223
+ ```bash
224
+ pnpm changeset
225
+ ```
226
+
227
+ Follow the prompts:
228
+
229
+ 1. Select changed packages (space to toggle, enter to confirm)
230
+ 2. Select bump type (patch, minor, major)
231
+ 3. Write a description (will appear in CHANGELOG)
232
+
233
+ Commit the generated file with your PR.
234
+
235
+ PRs that only change docs, tests, or internal tooling do not need changesets.
236
+
237
+ ---
238
+
239
+ ## Documentation changes
240
+
241
+ Documentation lives in two places:
242
+
243
+ - **Root markdown files** (`README.md`, `API.md`, etc.) — authoritative reference
244
+ - **Docs site** (`apps/docs/`) — long-form guides, tutorials, recipes
245
+
246
+ Changes to the API must first land in `API.md`. Changes to features must update the relevant `docs/features/*.md` spec. Changes to architecture must update `ARCHITECTURE.md`.
247
+
248
+ We prefer small, focused doc PRs over large reorganizations.
249
+
250
+ ---
251
+
252
+ ## Security disclosures
253
+
254
+ **Do not open public issues for security vulnerabilities.** See [`SECURITY.md`](./SECURITY.md) for the private reporting process.
255
+
256
+ ---
257
+
258
+ ## Questions?
259
+
260
+ Open a GitHub discussion or reach out on our community channel (once established). We read every message.
261
+
262
+ ---
263
+
264
+ Thank you for helping build variantlab. Every contribution — code, docs, reviews, ideas — makes the project better.