Skip to main content

18 · Engineering practices & delivery

Purpose: Define how the Rasd Forms monorepo is laid out, built, linted, tested, documented, released and kept secure, so every package in the spine (00 §3) ships with the same quality bar and adopters can trust the supply chain. Audience: Rasd core engineers and contributors; platform/DevOps engineers at UN/NGO organisations who audit or vendor the packages; host-app developers who want to run the same test utilities against their own forms.

TL;DR

  • One pnpm 11 + Turborepo 2.10 monorepo: packages/* (the 17 @rasd/* packages), apps/* (docs, playground, Next and Expo examples, Storybook, later the phase-3 license dashboard), tooling/* (shared configs). TypeScript 6 strict + isolatedDeclarations + verbatimModuleSyntax everywhere.
  • ESM-only packages built with tsdown (web/core) and react-native-builder-bob (native); exports maps with a react-native condition; platform split by package, never by file extension. tsup is unmaintained (research/08).
  • Lint = ESLint 9 flat config; format = Biome 2.5. One formatter, one linter, no Prettier.
  • Test pyramid: Vitest 4 + fast-check + fuzzer for @rasd/core; storage conformance suite against memory / Dexie / SQLite; RTL / RNTL 14 with vitest-axe; Playwright projects chromium, chromium-ar-rtl, offline, pwa, perf; Maestro on Expo; Storybook 10 + Chromatic; sync chaos tests. Coverage gates: core ≥ 90 %, renderers ≥ 80 %.
  • CI on GitHub Actions: SHA-pinned actions, Node 22/24 matrix, remote Turbo cache; required checks cover lint, typecheck, unit, contract, size-limit, publint/attw, a11y, E2E smoke, API report, security.
  • Releases via Changesets (fixed group for runtime packages) and npm Trusted Publishing (OIDC) with provenance; canary dist-tag on every merge; CycloneDX SBOM on every GitHub Release.
  • Semver per package; RFD spec MAJOR.MINOR with ≥ 12-month deprecations; support matrix React 19 (18.3 tested), RN ≥ 0.81 New Architecture, Expo ≥ 54, last-2 browsers, Android 7+.
  • Every PR satisfies the review checklist and Definition of Done (§12); ADRs in docs/adr/, RFCs in rfcs/; DCO sign-off and SPDX headers, no CLA.

1. Repository layout

rasd-forms/
├─ pnpm-workspace.yaml # workspaces + catalog: + pnpm 11 security settings (§2.5)
├─ package.json # private root: scripts only
├─ turbo.jsonc # task graph (§2.6)
├─ tsconfig.base.json # shared compiler options (§2.2)
├─ biome.json eslint.config.js .size-limit.js lefthook.yml
├─ .changeset/ # config.json (fixed group) + pending changesets
├─ .github/
│ ├─ workflows/ ci.yml release.yml canary.yml nightly.yml codeql.yml scorecard.yml
│ ├─ ISSUE_TEMPLATE/ bug.yml feature.yml form-compat.yml security.md
│ └─ PULL_REQUEST_TEMPLATE.md CODEOWNERS dependabot.yml renovate.json5
├─ packages/ # one dir per @rasd/* package (spine §3)
│ ├─ core/ react/ builder/ element/ storage/ storage-dexie/ sync/ pwa/
│ │ license/ themes/ xlsform/ server/ cli/ testing/ → tsdown
│ ├─ native/ storage-sqlite/ → bob
│ └─ media/ → tsdown (web) + bob (native entry)
├─ apps/
│ ├─ docs/ Docusaurus 3.10 (en / ar RTL / fr), TypeDoc pages, live playground
│ ├─ playground-web/ Vite + vite-plugin-pwa; Playwright, Lighthouse CI, CSP target
│ ├─ example-next/ Next.js App Router + Serwist; SSR/RSC smoke target
│ ├─ example-expo/ Expo SDK ≥ 54; Maestro flows; Hermes bytecode size tracked
│ ├─ storybook/ Storybook 10 (react-vite + react-native-web-vite), one story set
│ └─ license-dashboard/ customer portal (phase 3, spine §3); private, never published
├─ tooling/ eslint-config/ tsconfig/ tsdown-config/ bob-config/ vitest-config/ scripts/
├─ docs/ this design set + schema/ examples/ research/ adr/
├─ rfcs/ 0000-template.md, accepted/
├─ fixtures/ forms (*.form.json), submissions, storage snapshots per released version
└─ LICENSE LICENSE-COMMERCIAL SECURITY.md CONTRIBUTING.md CODE_OF_CONDUCT.md GOVERNANCE.md

Each package owns src/, src/internal/ (never exported), tests beside sources, package.json, a build config, a .size-limit.js entry, api-extractor.json, generated CHANGELOG.md and a published README.md. Packages import each other only as @rasd/<name> (resolved through the rasd-source condition in dev), never by relative path. The private rasd-source condition is wired once per tool — TypeScript customConditions (§2.2), Vite/Vitest resolve.conditions, Metro unstable_conditionNames in the Expo example (via react-native-monorepo-config), Storybook's Vite config — so in-repo apps and tests run against src/ while consumers always get dist/ (research/08). Apache-2.0 vs FSL per package follows 00 §12.

2. Toolchain

2.1 Versions (pinned via catalog:; facts from research/08)

ConcernChoiceVersion / note
Package managerpnpm11.x (Node ≥ 22); skip 11.0.8 (OIDC publish bug pnpm#11513)
Task runnerTurborepo2.10, remote cache
LanguageTypeScript6.0; TS 7 native preview as an advisory CI job
Web/core buildtsdown (Rolldown)unbundle: true, dts: { isolatedDeclarations: true }; not tsup (unmaintained)
Native buildreact-native-builder-bob≥ 0.40 (ESM-only template); targets module, typescript, codegen
Unit/componentVitest 4 + RTL + vitest-axe + fast-check (web); Jest + @react-native/jest-preset + RNTL 14 (RN)RNTL 14: React ≥ 19, RN ≥ 0.78, async render
E2EPlaywright (web), Maestro (Expo)Detox not used (Expo community-only)
Stories / visualStorybook 10 + ChromaticChromatic free 5 000 snapshots/mo, TurboSnap; Loki fallback
Lint / formatESLint 9 flat + typescript-eslint v8 / Biome 2.5§3.1
DocsDocusaurus 3.10 + TypeDoc 0.28 (typedoc-plugin-markdown)en / ar / fr, future.v4
API guardAPI Extractor run --localverify isolatedDeclarations support (rushstack#4877); fallback TypeDoc-JSON diff
ReleaseChangesets + npm Trusted Publishing (OIDC)npm ≥ 11.5.1, Node ≥ 22.14 runner, id-token: write
Budgets / packaging / securitysize-limit, publint, @arethetypeswrong/cli; Renovate, pnpm audit, Socket, CodeQL, zizmor, Scorecard, pnpm sbom§9, §10
Node enginestooling >=22.13; @rasd/server >=20; runtime packages noneGitHub runners default Node 24

2.2 TypeScript base config

// tsconfig.base.json
{
"compilerOptions": {
"strict": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noPropertyAccessFromIndexSignature": true,
"isolatedModules": true,
"isolatedDeclarations": true,
"verbatimModuleSyntax": true,
"module": "esnext",
"moduleResolution": "bundler", // bob: node16/nodenext break platform resolution
"target": "es2022", // Hermes down-levels via Babel in bob
"lib": ["es2023"], // packages add "dom" explicitly; core/storage never do
"types": [], // TS 6 default; packages opt in ("react", "react-native")
"customConditions": ["rasd-source"], // native packages prepend "react-native"
"jsx": "react-jsx",
"declaration": true, "declarationMap": true, "sourceMap": true,
"skipLibCheck": true
}
}

@rasd/core and @rasd/storage compile without dom so any DOM global fails typecheck; the core suite also runs under Node and under Hermes in the Expo example with identical snapshots (03 §17).

2.3 Build outputs

  • tsdown packages: per-file dist/*.js (unbundle: true) for file-granular tree-shaking; .d.ts from isolated declarations without tsc; platform browser (react, builder, element, storage-dexie, pwa), neutral (core, storage, sync, license, themes, xlsform, testing) or node (server, cli). React Compiler runs at build time (babel-plugin-react-compiler, target: '19', no runtime dep); component suites run compiled and uncompiled.
  • bob packages (native, storage-sqlite, native entry of media): [["module", { "esm": true }], "typescript"]; extension-less specifiers, named exports only, no import.meta, no top-level await, no react-native/Libraries/* deep imports (removed in RN 0.87).
  • @rasd/element: tsdown ESM entry plus Vite library mode iife for rasd-forms.iife.js (bundles React) and rasd-forms.css; SRI hashes in release notes.
  • src/ ships in every tarball so source maps and rasd-source work for consumers debugging Hermes traces.

2.4 package.json contract

// packages/react/package.json (abridged)
{
"name": "@rasd/react",
"license": "FSL-1.1-Apache-2.0",
"type": "module",
"sideEffects": ["./dist/styles.css", "./dist/locales/*.js"],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": { "rasd-source": "./src/index.ts", "types": "./dist/index.d.ts", "browser": "./dist/index.js", "default": "./dist/index.js" },
"./elements/*": { "rasd-source": "./src/elements/*/index.ts", "types": "./dist/elements/*/index.d.ts", "default": "./dist/elements/*/index.js" },
"./locales/*": { "types": "./dist/locales/*.d.ts", "default": "./dist/locales/*.js" },
"./styles.css": "./dist/styles.css",
"./unstable": { "types": "./dist/unstable.d.ts", "default": "./dist/unstable.js" },
"./package.json": "./package.json"
},
"files": ["dist", "src", "!**/__tests__", "LICENSE", "README.md"],
"dependencies": { "@rasd/core": "workspace:^" },
"peerDependencies": {
"react": "^18.3.0 || ^19.0.0", "react-dom": "^18.3.0 || ^19.0.0",
"@rasd/storage": "workspace:^", "@rasd/sync": "workspace:^", "@rasd/license": "workspace:^", "@rasd/media": "workspace:^"
},
"peerDependenciesMeta": { "@rasd/storage": { "optional": true }, "@rasd/sync": { "optional": true }, "@rasd/license": { "optional": true }, "@rasd/media": { "optional": true } },
"publishConfig": { "access": "public", "provenance": true },
"repository": { "type": "git", "url": "git+https://github.com/rasd-forms/rasd-forms.git", "directory": "packages/react" }
}

Native packages use the branch order rasd-source, types, react-native, default (publint: types first within a branch, default last; Metro matches by condition name). Peer ranges: react-native ">=0.81.0 <1.0.0"; optional expo ">=54", expo-sqlite, @op-engineering/op-sqlite, react-native-gesture-handler ^3, react-native-reanimated ^4 (native reorder UI only). ESM-only, no CJS build: dual publishing duplicates React context (bob's dual-package hazard) and Node 20.19+/22.12+ require(esm) covers Node consumers. sideEffects is false except CSS, locale catalogs (they register into the i18n runtime) and the @rasd/pwa worker asset. Adding an exports entry is additive; removing or renaming one is a major.

2.5 Workspace settings

# pnpm-workspace.yaml
packages: ["packages/*", "apps/*", "tooling/*"]
catalog:
react: ^19.2.0
react-dom: ^19.2.0
react-native: 0.85.0 # example app; CI matrix 0.81 / 0.85 / 0.87
expo: ~56.0.0
typescript: ~6.0.0
zod: ^4.0.0
dexie: ^4.4.0
vitest: ^4.0.0
minimumReleaseAge: 4320 # 3 days for third-party packages
minimumReleaseAgeExclude: ["@rasd/*"]
blockExoticSubdeps: true
strictDepBuilds: true
allowBuilds: { esbuild: true, "@swc/core": true, better-sqlite3: true }
trustPolicy: no-downgrade

2.6 Turborepo task graph

// turbo.jsonc
{
"$schema": "https://turborepo.dev/schema.json",
"globalDependencies": ["tsconfig.base.json", "biome.json", "eslint.config.js"],
"globalEnv": ["CI", "RASD_SITE_KEY", "CHROMATIC_PROJECT_TOKEN"],
"tasks": {
"build": { "dependsOn": ["^build"], "outputs": ["dist/**", "lib/**"], "inputs": ["src/**", "package.json", "tsdown.config.ts", "babel.config.js"] },
"typecheck": { "dependsOn": ["^build"], "outputs": [] },
"lint": { "outputs": [] },
"test": { "dependsOn": ["^build"], "outputs": ["coverage/**"] },
"test:contract": { "dependsOn": ["^build"], "outputs": [] },
"api-report": { "dependsOn": ["build"], "outputs": ["etc/*.api.md"] },
"size": { "dependsOn": ["build"], "outputs": [] },
"pack-check": { "dependsOn": ["build"], "outputs": [] }, // publint + attw on `npm pack`
"e2e": { "dependsOn": ["^build"], "outputs": ["playwright-report/**"], "cache": false },
"storybook:build": { "dependsOn": ["^build"], "outputs": ["storybook-static/**"] },
"docs:build": { "dependsOn": ["^build", "api-report"], "outputs": ["build/**"] },
"bench": { "dependsOn": ["build"], "cache": false }
}
}

3. Coding standards

3.1 Lint and format — decision

ESLint 9 flat config lints; Biome 2.5 formats and organises imports; Prettier is not used. Only ESLint carries eslint-plugin-react-hooks v6 (React-Compiler rules), jsx-a11y, react-native-a11y, import-x boundary rules and typescript-eslint's type-aware rules (no-floating-promises, no-misused-promises, switch-exhaustiveness-check) that this codebase depends on; Biome's coverage of those is partial (research/08 §9). Biome formats 10–20× faster than Prettier and its organizeImports replaces import sorting; ESLint stylistic rules are all off so no two tools fight. Pre-commit (lefthook): biome check --staged then eslint --cache on staged files; CI runs both on the tree.

Mandatory rules beyond recommended-type-checked: switch-exhaustiveness-check, no-floating-promises, no-misused-promises, consistent-type-imports, import-x/no-default-export (allowed only in apps/* route files and stories), import-x/no-restricted-paths encoding the dependency rules of 03 §3.1, import-x/no-cycle (+ madge --circular), no-restricted-globals (window, document, navigator, fetch, timers in @rasd/core/@rasd/storage), no-restricted-imports (react-native/Libraries/*, lodash, moment), all react-hooks/*, jsx-a11y/strict, react-native-a11y/all, header/header (SPDX), no-console outside apps/* and the logger, and a custom rule banning physical CSS properties in favour of logical ones.

3.2 Naming and file structure

ThingConventionExample
Fileskebab-case; one exported concept per file; *.test.ts(x) and *.stories.tsx beside sourceform-engine.ts, select-one.tsx
Types / classesPascalCase, no I prefixFormDefinition, StorageAdapter
Functions / varscamelCase; factories createX, hooks useX, guards isX, converters toX/fromXcreateSyncEngine, useField, isRepeatElement
ConstantsUPPER_SNAKE only for true constantsMAX_EXPR_DEPTH
Element types, error codesspine: snake_case types, RASD_* codesselect_multiple, RASD_SYNC_REJECTED
CSSrasd-<Component>__<part>, --rasd-<group>-<key>rasd-SelectOne__option, --rasd-color-primary
Eventslower-case verbs; DOM events rasd:<event>conflict, rasd:finalize
Branches / commitsfeat/…, fix/…; Conventional Commitsfeat(sync): tus adaptive chunking

Barrels: exactly one index.ts per package and per public sub-path; none inside src/internal/; barrels hold export { x } from './x.js' lines only. Named exports only (bob requirement, Metro-safe). Files > 400 lines or functions > 60 lines need a justifying comment or a split.

3.3 Errors and logging

  • Throw RasdError (code, message, details, cause) for anything a host can act on; codes are frozen constants from @rasd/core/errors listed in 17 · API reference. Never throw strings; never swallow — handle, wrap with cause, or route to onError. Async APIs reject, sync APIs throw, UI never crashes (per-element error boundary → placeholder + one onError report).
  • Engines catch every rejection and route it to the host onError hook on <RasdProvider> (03 §10, 17); nothing in @rasd/* leaves a promise unhandled (RN ≥ 0.82 turns unhandled rejections into console.error, which would spam field devices).
  • Logging only through createLogger({ level, sink, redact }) from @rasd/core: console sink at warn in production, debug in dev; the redactor strips bearer tokens, tus URLs, bind.sensitive values and anything under data. No telemetry.

4. TypeScript API design rules

// Branded ids: SubmissionId and AttachmentId cannot be mixed
export type SubmissionId = string & { readonly __brand: 'SubmissionId' };
export type AttachmentId = string & { readonly __brand: 'AttachmentId' };
export const asSubmissionId = (s: string): SubmissionId => s as SubmissionId;

// Discriminated union on `type`; `x:${string}` is the open tail
export type Element = TextElement | NumberElement | SelectOneElement | RepeatElement | /* … */ CustomElement;
export interface CustomElement extends ElementBase { type: `x:${string}`; props: Record<string, unknown> }

// Exhaustive switch with a compile-time guard
export function valueKind(el: Element): ValueKind {
switch (el.type) {
case 'text': case 'barcode': return 'string';
case 'number': case 'rating': case 'range': return 'number';
/* … every literal … */
default: return assertUnknownCustom(el); // 'unknown' for `x:*`, `never` for anything else
}
}
  1. Explicit return type on every export (isolatedDeclarations enforces it). interface for host-extensible shapes (ElementBase, StorageAdapter), type for unions and brands.
  2. Options objects, not positional booleans; readonly in signatures; defaults documented with @default.
  3. import type for anything erased; renderers depend on adapter packages only as type-only optional peers (03 §3.1).
  4. No enums (as const unions), no namespaces, no any in exported signatures (unknown + zod at boundaries).
  5. Public surface = per-package index.ts + spine §11, guarded by an API Extractor .api.md under etc/; a diff requires the api-change label and a matching changeset level. @rasd/*/unstable is exempt; @internal symbols are stripped from .d.ts.
  6. rasd types form.json output is snapshot-tested against toSubmission() for every fixture (FR-150).
  7. Deprecations: @deprecated JSDoc + one-time dev-only console.warn + changelog entry; removal ≥ 12 months later and only in a major.

5. Testing strategy

5.1 The pyramid

LayerPackagesToolProvesGate
Unit + property + fuzzcore, storage, sync, license, xlsform, themes, cliVitest 4, fast-check, grammar fuzzerREL semantics, engine graph, validation, diff/migrate, token state machinePR; core ≥ 90 % lines/branches
Contractstorage-*, sync transports, @rasd/serverrunConformanceSuite() (09 §13); rspConformance({ baseUrl, getAuthToken }) (10 §11) run against @rasd/server and fakeTransport()Every adapter behaves identically; the reference server and the fake agreePR (memory, fake-indexeddb, better-sqlite3, server in Docker); nightly real browsers/emulator
Componentreact, native, builder, elementRTL + vitest-axe (Vitest browser mode where needed), RNTL 14Rendering, a11y wiring, RTL, font scale, keyboard, registry overridesPR; renderers ≥ 80 %
Visualreact, native (RN-web), builderStorybook 10 + Chromatic (TurboSnap)Locale × mode × scale matrix pixel-stablePR (changed stories)
E2E webplayground-web, example-next, elementPlaywright chromium, firefox, webkit, chromium-ar-rtl, offline, pwa, perfOffline capture → sync, SW update safety, install, CSP, two-tab leaderPR smoke; nightly full
E2E nativeexample-expoMaestro (Android emulator in CI; iOS nightly on macOS / EAS Workflows)Same journeys on device, backup exclusion, low-RAM profilenightly + release
Performancecore, react, native, storageVitest bench, Playwright perf (4× CPU throttle), pnpm bench:storage, Hermes bytecode size§10 budgetsPR (bench, size); nightly device
Sync chaossync + storage + testingfault-injecting transport, fake clock, seeded RNGNo loss/duplication under partitions, restarts, 5xx, quota, revocationPR (10 seeds); nightly (100)
SecurityallCodeQL, pnpm audit, Socket, pollution corpus, CSP runSupply chain and sandbox invariantsPR

Flake policy: a test that fails then passes on retry is quarantined the same day (test.fixme + issue) and fixed within two weeks; CI retries: 1 for E2E, 0 for unit. Other runtime packages target ≥ 85 % coverage; the builder ≥ 70 % plus full story coverage.

5.2 Engine and REL: property-based and fuzz tests

import { expect } from 'vitest';
import { fc, test } from '@fast-check/vitest';
import { parseExpression, evaluate, printExpression } from '@rasd/core';
import { relArbitrary, evalContext } from './helpers/rel-arbitrary.js'; // grammar-driven REL generator (${ref}, calls, operators)

test.prop([relArbitrary({ depth: 6 })])('print∘parse is identity on the AST', (src) => {
const ast = parseExpression(src);
expect(parseExpression(printExpression(ast))).toEqual(ast);
});
test.prop([fc.integer(), fc.integer()])('+ matches JS on integers', (a, b) => {
expect(evaluate(parseExpression(`${a} + ${b}`), evalContext())).toBe(a + b);
});
test.prop([fc.integer({ min: 0, max: 20 }), fc.integer({ min: 0, max: 20 })])('${ref} arithmetic reads current values', (adults, children) => {
const ast = parseExpression('${adults} + ${children}');
expect(evaluate(ast, evalContext({ adults, children }))).toBe(adults + children);
});

Required suites: (a) parser round-trip and a precedence oracle against a reference recursive-descent parser (spine 00 §5 precedence table); (b) ODK compatibility corpus (20 §11) — every relevant/constraint/calculation from public XLSForm test forms parses or is flagged, hyphenated aliases (string-length(, selected-at(, count-selected(, indexed-repeat() resolve only in call position, results diffed against pyxform/JavaRosa outputs generated in CI; (c) sandbox invariants — grammar fuzzer (10 000 cases per PR, 100 000 nightly) plus the prototype-pollution corpus from 05, never exceeding ≤ 10 ms / ≤ 100 000 steps / ≤ 8 KiB source; (d) dependency graph — random relevant/calculate edges: incremental recompute equals brute-force, cycles detected at load; (e) diffDefinitionsmigrateSubmission — migrated drafts validate against the new definition and loss equals the dropped keys; (f) definitionHash stable across key order and whitespace.

5.3 Storage contract tests

The conformance suite (@rasd/storage/conformance, ~180 cases, framework-agnostic — 09 §13) is the single definition of adapter behaviour:

import 'fake-indexeddb/auto';
import { runConformanceSuite } from '@rasd/storage/conformance';
import { createDexieStorage } from '@rasd/storage-dexie';

runConformanceSuite(() => createDexieStorage({ namespace: `t-${crypto.randomUUID()}` }), { platform: 'web' });

Matrix: memory (Node); dexie (Vitest + fake-indexeddb/auto on PR; Vitest browser mode on Chromium/Firefox/WebKit incl. private context nightly); sqlite (Vitest + @rasd/storage-sqlite/node = better-sqlite3, plaintext and field-level encryption; expo/op drivers run the same suite inside the Expo example on the emulator nightly). Extra suites: migration fixtures for every released storage version (fixtures/dexie-v<n>.json, fixtures/sqlite-v<n>.db) upgraded to HEAD, crash-mid-chunk migration, checksum tamper, downgrade refusal, wrong-key/AAD-transplant negatives, export→import equality, quota simulation, concurrent patch fuzz with expectedRev.

5.4 Component tests

Web: renderForm() from @rasd/testing (06 §18, 17 §14) with RTL + user-event; every element type is tested for label/description/error wiring (aria-describedby), keyboard operation, dir="rtl" layout, readonly/relevant behaviour, registry override, and vitest-axe zero violations in en, ar, en-XB. Native: RNTL 14 (render awaited), role + accessible-name queries only, accessibilityState asserted, RTL via useDirection() independent of I18nManager, font scale 2.0. Hosts on Jest get the same axe rule set through jest-axe via the @rasd/testing/axe glue. Autosave and debounce use fake timers; Dexie-backed component tests use fake-indexeddb. Custom x: elements registered through defineElement() are covered by a shared contract test (renders with props, honours readonly/relevant, exposes the accessible-props contract, and a throwing component is contained by the per-element boundary — 03 §17).

5.5 End-to-end

Playwright projects: chromium, firefox, webkit; chromium-ar-rtl (locale: 'ar', dir asserted); offline (shell loaded, context.setOffline(true), form filled, finalized, tab killed and resumed, then online sync against @rasd/server running as a Docker service); pwa (waiting → prompt → useRasdBusy() defers reload; navigator.storage.persisted(); install criteria; CSP report-to endpoint fails the run on any report); perf (4× CPU throttle, 3G profile). Two-tab leader election and BroadcastChannel propagation use two contexts. Maestro flows on example-expo: capture-offline.yaml, sync-resume.yaml (kill mid-tus upload), rtl.yaml, low-memory.yaml (1 GB profile), backup-exclusion.yaml (bmgr backupnow → reinstall → no DB).

5.6 Visual regression, a11y and i18n snapshots

One story set drives web and RN-web. Matrix per story: locale en · ar · en-XB · xx-LS × mode light · dark · highContrast × font scale 1.0 · 1.3 · 2.0 (13); Chromatic diffs changed stories only; the Storybook Vitest plugin runs vitest-axe per story. Pseudo-locales catch concatenation and truncation; RTL snapshots catch unmirrored icons and physical CSS.

5.7 Sync chaos tests

import { expect, test } from 'vitest';
import { createSyncEngine } from '@rasd/sync';
import { fakeStorage, fakeClock, fakeTransport, fixtures } from '@rasd/testing';
import { seedFinalizedSubmissions, runChaos } from './helpers/chaos.js'; // repo-local helpers

test('chaos seed 42: no loss, no duplication', async () => {
const clock = fakeClock('2026-08-15T10:00:00Z');
const storage = fakeStorage();
// fakeTransport = in-memory RSP server + fault injection (17 §14); `faultyNetwork()` is the lower-level fetch shim
const transport = fakeTransport({
forms: [fixtures.pdmGfd2026],
faults: { seed: 42, dropRate: 0.2, statuses: [500, 503, 429], latencyMs: [50, 3000], partitionEvery: 5 },
});
const allIds = await seedFinalizedSubmissions(storage, fixtures.pdmGfd2026, { submissions: 500, attachments: 120 });
const engine = createSyncEngine({ storage, getAuthToken: async () => 't', transport, clock: clock.now });

await runChaos({ engine, clock, ticks: 10_000, restarts: 20 }); // kill/restart the engine, advance the fake clock

const acceptedIds = transport.received.map((s) => s.id);
expect(new Set(acceptedIds)).toEqual(new Set(allIds)); // nothing lost
expect(acceptedIds).toHaveLength(allIds.length); // nothing duplicated (idempotency keys)
expect(await storage.submissions.count({ status: 'synced' })).toBe(allIds.length); // every local row reached `synced`
});

Assertions: exactly-once acceptance under drops/5xx/partitions (idempotency keys); backoff schedule matches 10 §4.5 with the seeded RNG; tus resumes from Upload-Offset after restart; rejected never retries and never re-enters queued; device_revoked halts network but not storage; quota errors pause without loss; X-Rasd-License refresh updates license state; single-flight syncNow(); leader hand-off between two tabs; status transitions never skip a state of 00 §6 (finalized → queued → sending → synced, sending → queued on retry, sending → rejected on 4xx). Failing seeds are committed as regression cases.

5.8 @rasd/testing for hosts

Hosts get the same tools (17 §14): renderForm() (web via RTL, platform: 'native' via RNTL; kill()/restart() simulate process death), fakeStorage(), fakeClock(), faultyNetwork(), fakeTransport(), fakeMedia(), fakeLicense(state), form fixtures (fixtures.minimal, fixtures.pdmGfd2026, fixtures.kitchenSink, fixtures.perf500, fixtures.repeats200), propertyEngine() and the @rasd/testing/axe glue; RFD assertions use validateFormDefinition(def) from @rasd/core (00 §11) — Apache-2.0, so agencies can test their own forms in Vitest/Jest without a browser (FR-153).

6. Storybook, docs site, examples

  • Storybook 10 (apps/storybook): @storybook/react-vite and @storybook/react-native-web-vite on the same major, stories under packages/*/src/**/*.stories.tsx, decorators for locale/direction/theme/font-scale/license state; builder stories mount <FormBuilder> with fixture forms and a dnd-adapter fake sensor for deterministic drags; the on-device @storybook/react-native runner is ad hoc, not in CI.
  • Docs (apps/docs, Docusaurus 3.10, future.v4, i18n en + ar RTL + fr, versioned per minor): guides, the spine, TypeDoc-generated API pages, a live playground (@docusaurus/theme-live-codeblock + <rasd-form> from the IIFE build), a compatibility matrix regenerated from the CI matrix JSON, and a security page linking SBOM/provenance. Docs build is a required check when docs/** or the public API changes. Fumadocs/Starlight/Mintlify were rejected: none combines RTL multi-locale, live React demos and $0 (research/08 §8).
  • Example apps are living recipes and CI targets: playground-web (Vite + vite-plugin-pwa 1.3 injectManifest with registerRasdRoutes, Lighthouse CI on a Moto G profile, CSP), example-next (App Router, next/dynamic renderer, Serwist 9), example-expo (SDK ≥ 54, expo-sqlite SQLCipher, EAS Workflows Maestro). Each shows observability wiring (§12.7); none contains secrets — dev origins run in the evaluating license state.

7. CI/CD

7.1 Pipeline

flowchart LR
PR[Pull request] --> L[lint + typecheck + biome]
PR --> U[unit + property + contract]
PR --> C[component + axe + RNTL]
PR --> B[build + publint + attw + size-limit + api-report]
PR --> E["E2E smoke: chromium + chromium-ar-rtl + offline + pwa"]
PR --> S[CodeQL + audit + Socket + zizmor]
L & U & C & B & E & S --> M{required checks green and 1 CODEOWNER review}
M --> MAIN[merge to main]
MAIN --> CAN["canary publish, dist-tag canary"]
MAIN --> VP["Changesets 'Version Packages' PR"]
VP -->|merged| REL["release.yml: OIDC publish + provenance + SBOM + GitHub Release"]
MAIN --> N["nightly: full E2E, Maestro, browsers, chaos x10, benches, TS7 preview"]

7.2 Workflow (abridged)

# .github/workflows/ci.yml
name: ci
on:
pull_request: {}
push:
branches: [main]
permissions:
contents: read # default for every job; widened per job only
concurrency:
group: "ci-${{ github.ref }}"
cancel-in-progress: true
env:
TURBO_TOKEN: "${{ secrets.TURBO_TOKEN }}"
TURBO_TEAM: rasd
DO_NOT_TRACK: "1"
jobs:
check:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
node: [22, 24]
steps:
- uses: actions/checkout@<full-sha> # every action SHA-pinned; Renovate bumps the pins
- uses: pnpm/action-setup@<full-sha>
- uses: actions/setup-node@<full-sha>
with:
node-version: "${{ matrix.node }}"
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm turbo run lint typecheck test test:contract build api-report size pack-check --affected
- uses: codecov/codecov-action@<full-sha>
native:
runs-on: ubuntu-24.04
steps:
# …checkout / pnpm / setup-node / install exactly as in `check` (Node 24)…
- run: pnpm --filter @rasd/native --filter @rasd/storage-sqlite test # Jest + RNTL 14
e2e-smoke:
runs-on: ubuntu-24.04
services:
rsp: # reference RSP server for the offline → sync journey
image: ghcr.io/rasd-forms/server:main
ports: ["8080:8080"]
steps:
# …setup as in `check`…
- run: pnpm exec playwright install --with-deps chromium
- run: pnpm turbo run e2e --filter=playground-web -- --project=chromium --project=chromium-ar-rtl --project=offline --project=pwa
security:
runs-on: ubuntu-24.04
permissions:
contents: read
security-events: write # CodeQL upload only
steps:
# …setup as in `check`…
- run: pnpm audit --audit-level=high
- uses: socket/socket-security-action@<full-sha>
- uses: zizmorcore/zizmor-action@<full-sha>
- uses: github/codeql-action/init@<full-sha>
with:
languages: javascript-typescript
queries: security-extended
- uses: github/codeql-action/analyze@<full-sha>
ts7-preview:
runs-on: ubuntu-24.04
continue-on-error: true # advisory until TS 7 is GA
steps:
# …setup as in `check`…
- run: pnpm dlx @typescript/native-preview -p tsconfig.json --noEmit

Caching: pnpm store keyed on pnpm-lock.yaml; Turborepo remote cache for build/test/typecheck; Playwright browsers cached by version. --affected limits PR work to changed packages plus dependents; main runs everything. Required checks (branch protection, linear history, signed commits): check (22), check (24), native, e2e-smoke, security, chromatic (UI PRs), docs-build (docs/API PRs). Targets: PR ≤ 12 min, nightly ≤ 90 min.

7.3 Release

  • Changesets: fixed group = core, react, native, builder, element, storage, storage-dexie, storage-sqlite, sync, pwa, media, license, themes, testing; xlsform, cli, server independent. Every PR touching packages/* needs a changeset (bot-enforced). changesets/action opens "Version Packages"; merging it triggers release.yml.
  • release.yml: environment: npm (2 required reviewers), permissions: { id-token: write, contents: write }, Node 24, npm ≥ 11.5.1, pnpm changeset publish (shells to npm publish, avoiding the pnpm 11.0.8 OIDC bug); provenance attached automatically (public repo, exact repository.url); each package registered on npmjs.com with one trusted publisher and "Require 2FA and disallow tokens". Post-publish: pnpm sbom (CycloneDX + SPDX) per package attached to the GitHub Release; clean-container npm install smoke; docs version cut.
  • Canary: every main merge runs changeset version --snapshot canary and publishes --tag canary (0.0.0-canary-<date>-<sha>) with provenance; canaries older than 30 days are deprecated.
  • Changelog: @changesets/changelog-github into per-package CHANGELOG.md; release notes grouped Breaking · Features · Fixes · RFD spec · Security; a "Migration" section is mandatory for majors and RFD MINOR additions.
  • Rollback: npm deprecate the bad version and publish a patch; never unpublish.

8. Versioning and compatibility policy

stateDiagram-v2
[*] --> stable
stable --> deprecated: RFC accepted, runtime warning and codemod shipped
deprecated --> removed: next MAJOR, at least 12 months later
removed --> [*]
AxisSchemeGuarantee
@rasd/* packagessemver; runtime packages in a fixed groupPatch never changes engine semantics; minor = additive API / RFD MINOR support; major = removed exports, changed defaults, dropped platforms; ≥ 12-month deprecation warnings
RFD spec (rasd: "1.x")MAJOR.MINORIgnore-and-preserve unknown properties within a major; MINOR = optional properties, new element types, new REL functions; MAJOR ships a converter that round-trips every fixture; JSON Schema $id per version (04)
Storage schemainteger per adapterUpgrades from any release ≤ 12 months old (N-3 minors via fixtures); export works from any version
RSP / RLT/v1; typ: "RLT" + kidAdditive fields only within v1; new keys ship in minors
React^18.3 || ^19 for @rasd/react (19 primary); ^19 for @rasd/native18.3 stays in the CI matrix until 12 months after React 20 GA
React Native / Expo>=0.81 <1, New Architecture only; Expo ≥ 54Matrix RN 0.81 / 0.85 / 0.87, Expo 54 / 56; floors move only in a major with 6 months' notice
Browserslast 2 Chrome/Edge/Firefox/Safari; Android WebView/Chrome ≥ 100; iOS home-screen web appsFeature detection, never UA sniffing; missing features → degraded, never a crash
Nodetooling ≥ 22.13; @rasd/server ≥ 20Runtime packages carry no Node requirement

Breaking-change protocol: RFC (§12.5) → deprecation release with runtime warning and codemod (rasd migrate <from> <to>) where feasible → removal in the next major no earlier than 12 months later; the docs compatibility page lists every deprecation with its removal date.

9. Security tooling

ControlTool / settingCadence
Dependency updatesRenovate (config:best-practices, grouped RN/Expo bumps, minimumReleaseAge: 3 days, weekly lockfile maintenance, automerge patch/minor devDeps) + Dependabot alertscontinuous
Install-time defencepnpm 11 settings in §2.5; --frozen-lockfile everywhereevery install
Vulnerability scanpnpm audit --audit-level=high (fails PR), Socket (typosquat / install-script / obfuscation), OpenSSF Scorecard ≥ 8PR + weekly
Static analysisCodeQL (security-extended), semgrep pack for eval/new Function/innerHTMLPR
SecretsGitHub secret scanning + push protection; gitleaks pre-commit; no .env committedcontinuous
Actions hygienefull-SHA pins, permissions: contents: read default, zizmor, OIDC-only publish, reviewer-gated environments, no pull_request_targetPR
SBOM / provenancepnpm sbom CycloneDX + SPDX on Releases; npm provenance (SLSA build L2 on GitHub-hosted runners)release
DisclosureSECURITY.md: private reporting via GitHub Security Advisories, 72 h acknowledgement, 90-day coordinated disclosure, CVE for confirmed issues, backports to the last two minorsongoing
Fuzz + sandboxREL grammar fuzzer, pollution corpus, CSP-enforced playground runPR / nightly
Vendor packsecurityReport(), MASVS/ASVS mapping, SBOM, provenance, CAIQ v4 self-assessment (16, research/11)per minor

10. Performance budgets

// .size-limit.js (min+gzip; React/RN externalised)
export default [
{ name: '@rasd/core', path: 'packages/core/dist/index.js', limit: '45 kB', import: '*' },
{ name: '@rasd/core REL only', path: 'packages/core/dist/index.js', limit: '10 kB', import: '{ parseExpression, evaluate }' },
{ name: '@rasd/react default renderer', path: 'packages/react/dist/index.js', limit: '90 kB', import: '{ RasdProvider, FormRenderer }', ignore: ['react', 'react-dom'] },
{ name: '@rasd/react styles', path: 'packages/react/dist/styles.css', limit: '12 kB' },
{ name: 'form-runner (core+react+dexie+sync)', path: ['packages/react/dist/index.js', 'packages/storage-dexie/dist/index.js', 'packages/sync/dist/index.js'], limit: '120 kB', ignore: ['react', 'react-dom'] },
{ name: '@rasd/builder dnd-adapter', path: 'packages/builder/dist/internal/dnd-adapter/index.js', limit: '25 kB', ignore: ['react', 'react-dom'] },
{ name: '@rasd/element IIFE (incl. React)', path: 'packages/element/dist/rasd-forms.iife.js', limit: '175 kB' },
{ name: '@rasd/storage', path: 'packages/storage/dist/index.js', limit: '8 kB' },
{ name: '@rasd/storage-dexie (own code)', path: 'packages/storage-dexie/dist/index.js', limit: '12 kB', ignore: ['dexie'] },
{ name: '@rasd/storage-sqlite', path: 'packages/storage-sqlite/lib/module/index.js', limit: '15 kB', ignore: ['expo-sqlite', '@op-engineering/op-sqlite'] },
{ name: '@rasd/license', path: 'packages/license/dist/index.js', limit: '8 kB' }
];

Budgets are the spine's numbers (00 §12) plus the storage figures from 09 §14; raising any limit requires an RFC-lite note in the PR and a changeset entry, never a silent config edit.

Runtime budgets (02 NFR-001…008, 06 §10, 07 §6, 09 §14): keystroke → paint ≤ 16 ms mid-range / ≤ 50 ms low-end reference on a 500-question form; page switch ≤ 100 ms; form open ≤ 1.5 s for 300 elements; add repeat row 200 ≤ 50 ms; dataset search over 50 000 rows ≤ 150 ms after debounce; autosave patch p95 ≤ 50 ms on device (NFR-006; the adapter-level targets in 09 §14 are tighter: ≤ 20 ms Dexie on a Moto G, ≤ 15 ms SQLite); heap ≤ 150 MB with 30 thumbnails; every chunk < 2 MB uncompressed (Workbox precache default, research/05). Enforcement: size-limit on PR (comment + fail); Vitest bench on engine hot paths with a 15 % regression threshold against the main baseline; Playwright perf on PR; nightly device runs (Moto G-class emulator, iPhone SE simulator) publish to the compatibility page and a > 20 % regression opens an issue; example-expo Hermes bytecode size tracked per release.

11. Accessibility and i18n gates

  • A11y (PR-blocking): vitest-axe 0 violations for every story in en, ar, en-XB at font scale 2.0; @axe-core/playwright on example apps; jsx-a11y/strict and react-native-a11y/all; keyboard-only Playwright test per element type; every builder drag has a tested non-drag equivalent (WCAG 2.2 SC 2.5.7, research/03); touch targets ≥ 48 px asserted from layout; focus ring 2 px / 3:1 checked by rasd theme check.
  • i18n (PR-blocking): rasd i18n check — all library UI strings from the catalog, ICU subset only, keys complete in en/ar/fr, no JSX string concatenation, pseudo-locale build passes, RTL snapshots stable, no physical CSS, Intl only via runtime helpers (13).
  • Manual (per minor): screen-reader matrix (NVDA + Chrome, VoiceOver iOS/macOS, TalkBack) on the reference form; native Arabic review of new strings.

12. Process

12.1 Code review checklist

  • One concern per PR; description states why, links issue/RFC, lists behaviour and RFD/API impact.
  • Spine conformance: names, shapes, codes, status values match 00; no new vocabulary without a spine update.
  • Types: explicit return types, no any, exhaustive switches, branded ids at boundaries; .api.md diff reviewed and semver-labelled.
  • Offline-first: no network await on the render path; storage writes transactional; failures surface as RasdError codes.
  • Data safety: no path drops draft data; migrations reversible or export-safe; bind.sensitive respected in logs.
  • Tests: unit for logic, contract for adapters, story + axe for UI, regression test for every bug fix, chaos case if sync touched.
  • Bundle: size-limit unchanged or justified; heavy code lazy; new dependency reviewed under the deps label (license, size, maintenance, install scripts).
  • i18n/RTL/a11y gates green; strings in catalog; logical CSS.
  • Security: no eval/innerHTML, inputs validated at boundaries, secrets never logged; threat-model note for new surfaces.
  • Docs: JSDoc on public symbols, docs page/recipe updated, changeset present at the right level.

12.2 Definition of Done

Merged with the checklist satisfied; coverage at or above the package gate; story in the locale × mode × scale matrix; E2E or Maestro flow if a user journey from 01 is touched; docs and example app updated; size and perf budgets green; a11y/i18n gates green; changeset written; ADR recorded for architectural choices; unstable paths promoted or removed.

12.3 Contribution guide

CONTRIBUTING.md covers prerequisites (Node 22.13+, pnpm 11, JDK 17 + Android SDK for native), pnpm i && pnpm turbo build, pnpm dev (playground + Storybook), running each test layer locally, Conventional Commits + DCO, PR flow (draft → checks → CODEOWNER review → squash-merge), good first issue labelling, translation PRs via the ar/fr catalog template, and the rule that changes to docs/00 need two maintainers.

12.4 Issue templates

bug.yml (package, version, platform, minimal RFD or playground link, expected/actual, redacted logs), feature.yml (problem, proposed API, spine impact, alternatives), form-compat.yml (XLSForm/Kobo/ODK form that misbehaves, with expected ODK behaviour), security.md (redirects to private advisories). Triage SLA: labelled within 2 business days; severity:data-loss jumps the queue and gets a patch release.

12.5 RFC and ADR process

RFC (rfcs/NNNN-title.md) for anything changing the RFD spec, REL, RSP, RLT, storage schema, public API shape or support matrix: summary, motivation, detailed design, drawbacks, alternatives, migration, unresolved questions; ≥ 7-day comment window; accepted by two maintainers; linked from the implementing changeset. ADR (docs/adr/ADR-NNN-title.md, MADR format; initial set = 03 §16) for architectural decisions; ADRs are immutable once accepted — supersede, never edit.

12.6 Licensing headers, DCO, CLA

Every source file starts with // SPDX-License-Identifier: Apache-2.0 (core packages) or // SPDX-License-Identifier: FSL-1.1-Apache-2.0 (gated packages), enforced by eslint-plugin-header per package; root LICENSE + LICENSE-COMMERCIAL, per-package LICENSE copied at build. DCO sign-off (git commit -s) is required, no CLA — a CLA deters agency contributors and FSL already provides the Apache conversion. license-checker-rseidelsohn runs in release CI with an allow-list (MIT, Apache-2.0, BSD-2/3, ISC, 0BSD, OFL for fonts); copyleft dependencies are rejected in runtime packages.

12.7 Observability in examples

The library ships no telemetry; the examples show how a host wires the provider's onError / logger hooks to Sentry through createSentryScrubber() (16 §12), renders useSync() status (last sync time, pending counts) and storage.estimate() in a diagnostics panel, logs at warn through the redacting logger, and tags errors with formId, formVersion, definitionHash, @rasd/* version and license state. Web Vitals + Lighthouse CI run on the playground; the Expo example wires crash reporting behind an opt-in flag.

13. Failure modes of the delivery pipeline

FailureDetectionResponse
Compromised or typosquatted dependency (Shai-Hulud-class worm, install script)Socket + pnpm audit on PR; minimumReleaseAge holds new versions ≥ 3 days; allowBuilds blocks unknown install scripts; Renovate diffPR blocked; lockfile pinned; incident note in SECURITY.md history; if a published @rasd/* version pulled a bad transitive, npm deprecate + patch release within 24 h
Publish token or OIDC misconfiguration (pnpm 11.0.8 bug, wrong repository.url, missing id-token: write)release.yml fails at changeset publish; provenance check in the post-publish smoke (npm view --json dist.attestations)Fix the workflow, re-run; never fall back to a long-lived npm token; a version that reached npm without provenance is deprecated and re-released as a patch
Partial publish of the fixed group (some packages at N, others at N-1)post-publish npm install smoke in a clean container resolves the whole group at one versionRe-run publish (idempotent per package); until green, canary tag is left untouched and the release notes are held
Flaky test masks a real regressionflake quarantine rule (§5.1), retry budget 1 for E2E / 0 for unit, seeded RNG everywheretest.fixme + issue same day, fix ≤ 2 weeks; a quarantined test cannot stay quarantined across a release
Turborepo remote-cache poisoning or stale hash (input not declared)globalDependencies/inputs reviewed in PR; nightly --force run compares with cached resultsCache-key rotation (TURBO_TEAM namespace bump); missing inputs added to turbo.jsonc; the nightly diff fails the build
Size or perf budget regression slips through (new dependency, lost tree-shaking)size-limit PR comment + fail; sideEffects and publint checks; Vitest bench 15 % thresholdLazy-load or replace the dependency; budgets are never raised in the same PR that breaks them (§10)
Public API changed without reviewAPI Extractor .api.md diff without the api-change label; changeset level lower than the diffCI fails; reviewer applies label + changeset; unstable paths are exempt by design
Storage schema migration breaks an older on-device databasemigration fixtures per released version (§5.3) upgraded to HEAD on every PR; downgrade refusal testPR blocked; a shipped bad migration gets a patch that repairs forward — never a downgrade — and export() must keep working throughout (09 §6)
Chromatic / third-party CI service outagerequired check reports neutral after 30 min timeoutUI PRs wait or a maintainer runs loki locally and attaches the diff; never bypass the check by removing it from branch protection
Secret leaked into a log, fixture or storysecret scanning + push protection; gitleaks pre-commit; redacting logger tests (grep over 10 k fault-injection log lines)Rotate immediately, purge from history, advisory if it touched a customer secret
Docs and code drift (API page shows removed prop)docs:build depends on api-report; TypeDoc build fails on missing symbols; link checkerDocs PR blocked; the compatibility page is regenerated from CI JSON, not edited by hand

14. Acceptance criteria

  • Fresh clone: pnpm i --frozen-lockfile && pnpm turbo build test passes on Node 22 and 24 in ≤ 15 min cold, ≤ 3 min warm.
  • Every package passes publint and attw --pack; exports order verified (types first in branch, default last, react-native present for native packages).
  • size-limit budgets in §10 pass; builder and heavy elements are separate chunks.
  • Coverage gates hold: core ≥ 90 %, renderers ≥ 80 %, other runtime packages ≥ 85 %.
  • Storage conformance passes for memory, dexie (fake + real browsers) and sqlite (better-sqlite3 + expo/op on emulator).
  • Sync chaos suite passes 10 seeds on PR and 100 nightly with zero loss/duplication.
  • Playwright chromium, chromium-ar-rtl, offline, pwa pass on PR; firefox, webkit, perf and Maestro flows nightly.
  • Every story is axe-clean across the locale × mode × scale matrix; Chromatic has no unapproved diffs.
  • A release yields provenance-attested packages, SBOMs, generated changelogs and a docs version; canary publishes on every main merge.
  • Renovate, CodeQL, secret scanning, Scorecard and zizmor active; all Actions SHA-pinned; SECURITY.md published.
  • .api.md reports exist for every package with a public surface and CI fails on undeclared changes.
  • Every "PR blocked" row of §13 has been exercised by a deliberately failing drill PR (budget breach, undeclared API change, unsigned action, broken migration fixture) at least once per quarter, and the release runbook covers the OIDC and partial-publish rows.
  • Code samples in this document (package.json, tsconfig.base.json, turbo.jsonc, .size-limit.js, ci.yml) are kept in tooling/ as the real files and this document links to them; drift fails docs:build.

Open questions

  • Keep the ^18.3 || ^19 peer range for @rasd/react at launch, or declare ^19 only (RNTL 14 and the React Compiler target push toward 19) and treat 18.3 as best-effort?
  • If API Extractor's isolatedDeclarations incompatibility persists, standardise on a TypeDoc-JSON diff for the API report?
  • Chromatic's free tier will be exceeded by the full 4 × 3 × 3 story matrix — run it nightly only, or self-host Loki?
  • Turborepo remote cache: Vercel-hosted (simplest) or self-hosted (supply-chain optics for UN reviewers)?
  • Do gated (FSL) packages stay on the public npm scope with provenance (recommended) or move to a private registry if the builder ever becomes source-closed?
  • Nightly device benchmarks on a device farm (Firebase Test Lab / AWS Device Farm) rather than emulators, given the 2 GB-RAM Android target?

00 · Decisions & conventions · 02 · Requirements · 03 · Architecture · 04 · Form schema spec · 05 · Logic & expressions · 06 · Renderer React · 07 · Renderer native · 08 · Builder · 09 · Offline storage · 10 · Sync protocol · 11 · PWA & embedding · 12 · Theming · 13 · i18n, RTL & accessibility · 14 · Media & field capture · 15 · Licensing & billing · 16 · Security & data protection · 17 · API reference · 19 · Roadmap & work breakdown · 20 · Interoperability · 21 · Getting started · JSON Schema: form · JSON Schema: theme · Research: 08 · Library engineering, 03 · Drag-and-drop builder, 05 · PWA & embedding, 11 · Security threat model, 12 · Versioning & migration