# OpenSpec Change: enterprise-react-forms-v2 - **Change ID**: `enterprise-react-forms-v2` - **Package**: `@suaveplan/react-forms` - **Category**: react - **Target path**: `packages/forms/react-forms` - **Status**: PROPOSED - **Date**: 2026-05-05 - **Supersedes**: `openspec/changes/enterprise-react-forms/` - **Master design**: `openspec/cross-cutting-designs/2026-05-05-react-ui-react-i18n-react-forms.md` (§9, §11.2) - **Prereqs (one SHIPPED, one in-flight)**: - `enterprise-react-i18n` shipped 2026-05-05 — archived at `openspec/archive/2026-05-05-enterprise-react-i18n/`; capability spec at `openspec/specs/react-i18n/spec.md`. This change consumes `TranslationKey` for every `FieldConfig.label` / `placeholder` / `helperText` / `errorMessage`, `useTranslation` for label resolution at render time, `useErrorMessage` for `SuaveplanError`-coded validation failures, and the `ContributedTranslationKeys` augmentation pattern for default recipe button labels (`react-forms.wizard.next` etc.). - `enterprise-react-ui-v2` (sibling, in-flight) — provides `Field` / `FieldGroup` / `FieldSet` / `InputGroup` / `Textarea` / `Checkbox` / `Switch` / `RadioGroup` / `Toggle` / `ToggleGroup` primitives the factory and recipes render onto. Wave 5b waits for Wave 3b before adding `@suaveplan/react-ui` as a peer dep (gate enforced via `scripts/lint/check-no-backwards-deps.ts` per-tier hierarchy). --- ## Why The original `enterprise-react-forms` proposal bundled product-scope domain forms (`BankAccountForm`, `UserProfileForm`, `FriendRequestForm`, `GroupCreateForm`, `MessageComposeForm`, `ChangePasswordForm`, `TwoFactorForm`, `NotificationSettingsForm`, `PrivacySettingsForm`, `SiteConfigForm`, `SystemSettingsForm`, `NotificationPreferencesForm`) into the library. That conflates two layers: the **library** (primitives + composition machinery) and the **consumer's product** (the actual forms a particular app needs). The result is a package that nobody outside Suaveplan can adopt without inheriting our entity model, and that we ourselves cannot evolve without coordinating with every domain at once. The user has approved a primitives + factory + recipes architecture instead. `@suaveplan/react-forms` v2 ships: 1. **Primitives** that wrap react-hook-form + react-ui controls into a tested, accessible, i18n'd surface. 2. **A `createDomainForm` factory** that takes a Zod schema + a typed field config and returns a working form component — without hiding react-hook-form. Three escape hatches (`children` render-prop, `` static accessor, `useDomainForm()` hook) keep the abstraction transparent. 3. **Four recipes in v1** — `wizard`, `repeating-fieldset`, `conditional`, `async-search` — covering the multi-step, dynamic-array, dependent-field, and prefilled-edit patterns that every real app needs. 4. **A validation library** — synchronous validators (`required`, `minLength`, `maxLength`, `email`, `url`, `pattern`, `phone`, `postcode`, `creditCard`, `password-strength`, `iban`, `bic`, `custom`), async validators (`asyncEmailUnique`, `asyncUsernameAvailable`), and composers (`AND` / `OR` / `sequence`). Domain-specific forms move out to consumer apps and to the example `@suaveplan/react-ui-pack-auth` (a separate change), which proves the factory contract end-to-end on register / login / password-reset / 2FA / magic-link. The branded-`TranslationKey` discipline from §11.2 of the master design is the second non-negotiable: every user-facing string in a `FieldConfig` is typed as `TranslationKey`, never `string`. Validators return `messageKey: TranslationKey`. The CI gate (`scripts/lint/check-no-hardcoded-strings.ts`) catches literal regressions before they ship. --- ## Current-state audit (genesis) The package does not exist on `genesis`. `STATUS.md` lists `react-forms` under `Pending`. There are no consumers waiting on a specific public surface yet — the example pack (`react-ui-pack-auth`) is the first consumer and will track this proposal. --- ## Available workspace deps on genesis | Dep | Version source | Role | |---|---|---| | `@suaveplan/react-i18n` | `workspace:^` | `TranslationKey`, `useTranslation`, ``, locale glue | | `@suaveplan/react-ui` | `workspace:^` | `Field`, `FieldGroup`, `Input`, `Select`, `Combobox`, `Calendar`, `Button`, etc. | | `@suaveplan/api-schemas` | `workspace:^` | Shared request / response Zod schemas the factory consumes via `schema:` | | `@suaveplan/error` | `workspace:^` | `defineCodes("REACT_FORMS", [...])` registry; `ReactFormsError` | | `@suaveplan/timing` | `workspace:^` | Debounce / dedup for async field validators | | `@suaveplan/types` | `workspace:^` | `Clock`, `LoggerLike`, `StorageLike` (used by form persistence recipes) | | `react-hook-form` | `catalog:forms` | Form state core; surfaced (never hidden) by the factory | | `@hookform/resolvers` | `catalog:forms` | Zod resolver bridge into RHF | | `zod` | `catalog:forms` | Schema spine | | `date-fns` | `catalog:datetime` | Date / datetime field formatters | | `@opentelemetry/api` | `catalog:otel` | `form.submit.duration_ms`, `form.validation.duration_ms`, `form.async-validator.duration_ms`, `form.error_total` | External peer: `react@^18`. No new deps need to be added to the root catalog — every entry above already exists. --- ## Public API surface ### `primitives/` `Field` (re-export from `@suaveplan/react-ui`, single source), `FieldGroup`, `AccessibleDatePicker`, `DateTimePicker`, `MoneyInput`, `SearchableSelect` (async), `TagInput`, `FileUploadField` (defers to `@suaveplan/react-file-upload` when present, falls back to native ``), `RichTextField` (defers to `@suaveplan/react-rich-text`), `CodeInput` (OTP / 2FA, wraps `InputOTP`). Each primitive is a separate subpath export so consumers can tree-shake — e.g. `import { MoneyInput } from '@suaveplan/react-forms/primitives/money-input/money-input'`. ### `core/` - `ApiClientContext` — `` + `useApiClient()`. The factory's `onSubmit` receives `api` from this context. Implementation-agnostic (consumer brings their own client). - `AuthContext` — `` + `useAuth()`. Surfaces `signIn` / `signOut` / `currentUser` so registration forms can auto-sign-in on success. - `ValidationEngine` — combines a Zod schema, per-field async validators, cross-field rules, and a server-error mapper into a single resolver compatible with react-hook-form. - `useFormValidation(schema, options)` — hook returning a memoised resolver from a `ValidationEngine`. - `useDomainForm(config)` — the hook the factory wraps. Standalone-callable for consumers building bespoke forms that don't fit the factory's layout shape. ### `factory/` - `createDomainForm(config)` — returns `ComponentType` plus a `.Field` static accessor and `.useFormContext` re-export. - Field-kind registry typed via `FieldConfigByKind`; misconfigurations fail at module-evaluation time via Zod-validated `FieldConfig` schemas. - Layout dispatcher (`vertical` / `horizontal` / `grid` / custom render fn). ### `recipes/` - `createWizardForm({ steps, onComplete })` — multi-step form with per-step Zod sub-schema, navigation guards, and a `useWizardState()` hook. - `createRepeatingFieldset({ minRows, maxRows, fieldset })` — dynamic-array rows backed by RHF's `useFieldArray`. - `createConditionalField({ when, render })` — declarative show/hide based on other field values; participates in validation as a function of visibility. - `createAsyncSearchForm({ load, schema, fields })` — async-prefilled forms (load entity by id, edit, save). ### `validation/` - `validators/` — `required`, `minLength`, `maxLength`, `email`, `url`, `pattern`, `phone`, `postcode`, `creditCard`, `password-strength`, `iban`, `bic`, `custom`. - `async/` — `asyncEmailUnique`, `asyncUsernameAvailable`. Each accepts `{ debounceMs, signal, dedupKey }`. - `compose/` — `and(...validators)`, `or(...validators)`, `sequence(...validators)` (short-circuits on first failure). All validators return `{ valid: true } | { valid: false; messageKey: TranslationKey; messageArgs?: Record }`. The `messageKey` field is typed as `TranslationKey`, not `string` — passing a literal misses the translation pipeline and fails the CI no-hardcoded-strings gate. --- ## Design decisions ### 1. Five-layer surface `primitives/ → core/ → factory/ → recipes/ → validation/`. Each layer compiles, tests, and ships independently; later layers consume earlier ones, never the reverse. Ports the master-design §9.1 layout verbatim. ### 2. The factory surfaces RHF — three escape hatches `createDomainForm` is the 80%-case ergonomics layer. The other 20% of forms need direct access to the RHF context, partial layouts, or a completely custom shell. The three hatches: - **`children` render-prop** — passing `children` with the render-prop signature gives the consumer raw `useFormContext()` access plus pre-rendered field elements keyed by name. - **`` static accessor** — for partial layouts where the consumer wants the factory's layout for some fields but inlines others into a custom JSX shape. - **`useDomainForm()` hook** — the same hook the factory wraps, exported standalone. Consumers build their own component shell with full control over markup. The factory never hides RHF; it only declares it. Consumers can always drop one layer down without losing their config. ### 3. `FieldConfig` strings are `TranslationKey`, never `string` Every user-facing text property in `FieldConfig` is typed as `TranslationKey` (per master design §9.5.1 and §11.2). This is enforced at the type level: ```ts export interface FieldConfigBase { label: TranslationKey; placeholder?: TranslationKey; helperText?: TranslationKey; errorMessage?: TranslationKey | { [code: string]: TranslationKey }; } const RegisterForm = createDomainForm({ schema: RegisterRequestSchema, fields: { email: { kind: "email", label: "auth.email.label" }, // typechecks name: { kind: "text", label: "Full name" satisfies string }, // TS error }, // ... }); ``` The same constraint applies to `submit.label`, `submit.cancelLabel`, recipe step titles, and conditional-field gate labels. ### 4. `kind: 'custom'` is a first-class field kind Captcha, signature pad, file dropzone with consumer-specific upload protocol — these are not escape hatches. They are first-class custom fields with full factory integration: ```ts captcha: { kind: "custom", label: "auth.captcha.label", render: ({ field, fieldState }) => , } ``` `kind: 'custom'` participates in layout, validation, error messaging, and submit wiring identically to built-in kinds. The factory exposes `field` (RHF `ControllerRenderProps`) and `fieldState` (RHF `ControllerFieldState`) to the render function. ### 5. All four recipes ship in v1 Splitting recipes across releases creates the "Phase 2 is unwritten" failure mode (`openspec/AGENTS.md` §2b). `wizard`, `repeating-fieldset`, `conditional`, `async-search` all ship in the v1 archive of this change. The factory's internal hook architecture is designed for all four from day one. ### 6. `ValidationEngine` semantics The engine layers, in order, on every form mutation: 1. **Zod schema** — the spine. Synchronous; runs first. 2. **Cross-field rules** — e.g. `passwordConfirm matches password`. Synchronous; runs after the schema. Has access to the full form values object. 3. **Per-field async validators** — debounced via `@suaveplan/timing` (`debounce({ leading: false, trailing: true, maxWait })`), deduped by `(field, value)` key, cancelled via `AbortSignal` when the form unmounts or when a newer value supersedes the in-flight check. 4. **Server-error mapping** — when `onSubmit` throws a `ReactFormsError(REACT_FORMS.SERVER_VALIDATION)` carrying `{ field, code, messageArgs }`, the engine sets the corresponding RHF field error with the translated message resolved via `t(code, messageArgs)`. `AbortSignal` threads through every async path. Switching to a different form mid-validation cancels in-flight checks instead of leaking promises. ### 7. Validator return shape is a discriminated union `{ valid: true } | { valid: false; messageKey: TranslationKey; messageArgs?: Record }`. Plain-string returns are a type error. The engine resolves `messageKey` against the active i18n instance at error-display time, so the same validator works in every locale with no additional wiring. ### 8. OTel-first observability Every form lifecycle event emits structured telemetry (see § Telemetry below). Counter / histogram names are stable contracts — consumers can build dashboards against them. ### 9. Server Actions are out of scope `onSubmit` stays a user-defined async function. RSC / Server Actions integration is a Wave 8+ follow-up — the surface is too unstable in React 19 to bake in now. --- ## What changes ### ADDED ```text packages/forms/react-forms/ ├── README.md ├── package.json ├── src/ │ ├── index.ts # explicit named exports only │ ├── error-codes.ts # defineCodes("REACT_FORMS", [...]) │ ├── primitives/ │ │ ├── field/ # re-export from react-ui │ │ ├── accessible-date-picker/ │ │ ├── date-time-picker/ │ │ ├── money-input/ │ │ ├── searchable-select/ │ │ ├── tag-input/ │ │ ├── file-upload-field/ │ │ ├── rich-text-field/ │ │ └── code-input/ │ ├── core/ │ │ ├── api-client-context/ │ │ ├── auth-context/ │ │ ├── validation-engine/ │ │ ├── use-form-validation/ │ │ └── use-domain-form/ │ ├── factory/ │ │ ├── create-domain-form/ │ │ ├── field-config/ # FieldConfig types + Zod schemas per kind │ │ └── layout/ # vertical / horizontal / grid dispatchers │ ├── recipes/ │ │ ├── wizard/ │ │ ├── repeating-fieldset/ │ │ ├── conditional/ │ │ └── async-search/ │ └── validation/ │ ├── validators/ # required, email, url, ... │ ├── async/ # asyncEmailUnique, asyncUsernameAvailable │ └── compose/ # and / or / sequence ├── e2e/ │ ├── factory.spec.ts │ ├── recipes.spec.ts │ └── a11y.spec.ts ├── tsconfig.json ├── tsconfig.lib.json ├── vite.config.ts └── vitest.config.ts ``` ### REMOVED (vs the legacy `enterprise-react-forms` proposal) Every domain form below is dropped from the package. Justification (applies uniformly): **product scope; belongs in consumer apps; replaced by the `createDomainForm` factory and the example `@suaveplan/react-ui-pack-auth` pack.** - `auth/forms/` — `SignInForm`, `SignUpForm`, `ForgotPasswordForm`, `ResetPasswordForm` - `bank/forms/` — `BankAccountForm`, `BankTransactionForm` - `user/forms/` — `UserCreateForm`, `UserEditForm`, `OptimizedUserCreateForm` - `friend/` — `FriendRequestForm` - `group/forms/` — `GroupCreateForm`, `GroupEditForm` - `message/forms/` — `MessageCreateForm`, `MessageEditForm` - `notification/forms/` and `notifications/forms/` — `NotificationCreateForm`, `NotificationEditForm` (duplicate domains in upstream collapsed) - `security/` — `ChangePasswordForm`, `TwoFactorForm` (re-emerge in `react-ui-pack-auth`) - `settings/forms/` — `SettingCreateForm`, `SettingEditForm`, `NotificationSettingsForm`, `PrivacySettingsForm` - `site/forms/` — `SiteCreateForm`, `SiteEditForm`, `SiteConfigForm` - `system/forms/` — `SystemCreateForm`, `SystemEditForm`, `SystemSettingsForm` - `demo/DemoApp.tsx` — example app, not library code Also dropped from upstream: `core/components/FormProgressBar`, `FormSection`, `FormSkeleton`, `FormWizard`, `FormProvider` — superseded by the recipes (`wizard` recipe covers `FormWizard`; `react-ui` Skeleton covers `FormSkeleton`; layout is the factory's job). `shared/components/VirtualizedList`, `LazyFormSection`, `OptimizedFormField` — performance scaffolding belongs in `react-ui` or in consumer apps. `core/storage/` form persistence — moves to a `recipes/persistent-form` follow-up if demand surfaces. `security/csrf`, `security/rate-limiting`, `security/sanitization` — cross-cutting concerns that belong in `@suaveplan/api-client` or the consumer's transport layer, not in a forms package. --- ## Parity Checklist | Capability | Upstream | Migrated | Notes | |---|---|---|---| | `Field` primitive | ✅ | ✅ | Re-exported from `@suaveplan/react-ui` to keep one source of truth | | `AccessibleDatePicker` | ✅ | ✅ | Calendar + ARIA wired via react-ui `Calendar` | | `DateTimePicker` | ✅ | ✅ | Time-of-day component composes date + time inputs | | `MoneyInput` | ⬆️ | ⬆️ | Enhanced: locale-aware via `@suaveplan/i18n` formatters; upstream was hardcoded GBP | | `SearchableSelect` (async) | ✅ | ⬆️ | Enhanced: debounce + dedup + AbortSignal via `@suaveplan/timing` | | `TagInput` | ✅ | ✅ | | | `FileUploadField` | ✅ | ✅ | Defers to `@suaveplan/react-file-upload` when present, native `` fallback | | `RichTextField` | ✅ | ✅ | Defers to `@suaveplan/react-rich-text` | | `CodeInput` (OTP) | ✅ | ✅ | Wraps `react-ui` `InputOTP` | | `ApiClientContext` | ✅ | ✅ | | | `AuthContext` | ✅ | ✅ | | | `TranslationContext` | ✅ | 🚫 | Replaced by `@suaveplan/react-i18n`'s `TranslationProvider`; forms package consumes that, doesn't redefine it | | `useFormValidation` | ✅ | ✅ | Now returns a `ValidationEngine`-backed resolver | | `ValidationEngine` | ✅ | ⬆️ | Enhanced: cross-field rules + AbortSignal-cancellable async + server-error mapping | | `useFormSubmission` | ✅ | 🚫 | Replaced by the factory's `submit.onSubmit` callback + `useDomainForm()`'s `submit` return | | `useFormPersistence` | ✅ | 🚫 | Deferred to a follow-up `recipes/persistent-form` (can be built on top of `useDomainForm` + `StorageLike` from `@suaveplan/types`) | | `FormProvider` / `FormStateContext` | ✅ | 🚫 | RHF's `` is sufficient; the factory wraps it | | `FormProgressBar` | ✅ | 🚫 | Wizard recipe owns progress UI; consumers compose `react-ui` `Progress` if needed | | `FormSection` | ✅ | 🚫 | Layout concern; the factory's layout dispatcher handles sections via `FieldGroup` | | `FormSkeleton` | ✅ | 🚫 | `react-ui` Skeleton is the canonical source | | `FormWizard` | ✅ | ⬆️ | Replaced by `createWizardForm` recipe (typed steps, sub-schemas, navigation guards) | | `useDebounce` / `useDebouncedValidation` | ✅ | 🚫 | Replaced by `@suaveplan/timing`'s `debounce` directly inside the validation engine | | `useLocalStorage` | ✅ | 🚫 | Out of scope; `StorageLike` from `@suaveplan/types` covers it generically | | `useMemoryMonitor` / `usePerformanceMonitor` / `useRenderTracker` | ✅ | 🚫 | Dev-only telemetry; replaced by OTel histograms (see § Telemetry) | | `useSchemaValidation` | ✅ | 🚫 | Folded into `ValidationEngine` | | `usePagination` | ✅ | 🚫 | Out of scope for a forms package; `@suaveplan/pagination` covers this | | `VirtualizedList` | ✅ | 🚫 | Belongs in `react-ui` if needed; not a forms concern | | `LazyFormSection` / `OptimizedFormField` | ✅ | 🚫 | Premature optimisation; React 18 concurrent rendering covers the same need | | `accessibility/` components + hooks | ✅ | ⬆️ | Subsumed by `react-ui`'s WCAG 2.2 AA baseline; forms inherit by composition | | `core/i18n/` (custom translation context) | ✅ | 🚫 | Replaced by `@suaveplan/react-i18n` | | `validation/` library | ✅ | ⬆️ | Enhanced: discriminated-union returns, `messageKey: TranslationKey` typing, async validators with debounce + dedup, compose helpers | | `security/csrf`, `security/rate-limiting`, `security/sanitization` | ✅ | 🚫 | Cross-cutting transport concerns; belong in `@suaveplan/api-client`, not a forms package | | `shared/state/` (form persistence) | ✅ | 🚫 | Deferred to `recipes/persistent-form` follow-up | | `integration/` (mocks) | ✅ | 🚫 | Genesis uses `@suaveplan/testing` + real DI adapters per CLAUDE.md "no mocks" rule | | **Domain forms** (auth, bank, user, friend, group, message, notification, settings, site, system) | ✅ | 🚫 | Product scope; replaced by `createDomainForm` factory. Auth set re-emerges in `react-ui-pack-auth`. | | `createDomainForm` factory | ❌ | ✅ | New: declarative form assembly with three escape hatches | | Wizard recipe | ❌ | ✅ | New: typed multi-step factory | | Repeating-fieldset recipe | ❌ | ✅ | New: dynamic-array fieldsets via `useFieldArray` | | Conditional-field recipe | ❌ | ✅ | New: declarative show/hide with validation gating | | Async-search recipe | ❌ | ✅ | New: load-by-id then edit | | Branded `TranslationKey` typing for FieldConfig | ❌ | ✅ | New: type-level enforcement of i18n discipline | | OTel telemetry | ❌ | ✅ | New: form lifecycle counters + histograms | --- ## Field kinds shipped with v1 `text`, `email`, `password` (with optional `strengthMeter: boolean`), `number`, `currency`, `date`, `dateTime`, `select`, `multi-select`, `combobox` (async), `checkbox`, `radio`, `toggle`, `textarea`, `rich-text`, `file`, `tag-input`, `otp`, `custom`. Each kind has a sibling Zod schema that validates its `FieldConfig` shape at factory-define time. A misconfigured field (e.g. a `select` without `options`) throws `ReactFormsError(REACT_FORMS.FIELD_CONFIG_INVALID)` at module evaluation, not on first render. --- ## Recipes - **`createWizardForm({ steps, onComplete })`** — multi-step factory. Each step has its own Zod sub-schema; navigating forward validates the current step before advancing; navigating backward preserves entered data. Exposes `useWizardState()` for custom progress UIs. - **`createRepeatingFieldset({ name, minRows, maxRows, fieldset })`** — dynamic-array fieldsets backed by RHF's `useFieldArray`. `fieldset` is itself a `FieldConfig` map, so rows nest factory-typed fields with full validation. - **`createConditionalField({ when, render })`** — declarative show/hide. `when` is a predicate over the current form values; hidden fields are excluded from validation (and from the submitted payload) so optional sections don't pollute the schema. - **`createAsyncSearchForm({ load, schema, fields })`** — async-prefilled forms. `load(id, signal)` fetches the entity; the form renders in a loading state until the promise resolves, then mounts with `defaultValues` populated. --- ## Validation library ### Synchronous validators `required`, `minLength(min)`, `maxLength(max)`, `email`, `url`, `pattern(regex)`, `phone({ region })`, `postcode({ region })`, `creditCard` (Luhn), `password-strength({ minScore })`, `iban`, `bic`, `custom(predicate, messageKey)`. ### Async validators `asyncEmailUnique({ check, debounceMs, dedupKey })`, `asyncUsernameAvailable({ check, debounceMs, dedupKey })`. Each accepts an injected `check(value, signal): Promise` so the validator is transport-agnostic and testable without network mocks. ### Composers `and(...validators)` — all must pass. `or(...validators)` — at least one must pass. `sequence(...validators)` — short-circuits on first failure (cheap-first ordering). All return the discriminated union shape; all accept `AbortSignal` where async; all surface `messageKey: TranslationKey`. --- ## Telemetry Every form lifecycle event emits OTel data via `@suaveplan/types`'s `LoggerLike` and `@opentelemetry/api`: | Instrument | Kind | Attributes | Description | |---|---|---|---| | `form.submit.duration_ms` | Histogram | `form_id` | End-to-end submit latency | | `form.validation.duration_ms` | Histogram | `form_id`, `phase` (schema / cross-field / async) | Per-phase validation latency | | `form.async-validator.duration_ms` | Histogram | `form_id`, `validator` (`asyncEmailUnique` / `asyncUsernameAvailable` / consumer-named) | Per-validator async latency | | `form.error_total` | Counter | `form_id`, `kind` (`schema` / `cross-field` / `async` / `submit` / `server`) | Error counter by failure class | | `form.field.touched_total` | Counter | `form_id`, `field` | First-touch counter for field-level analytics | Every histogram/counter name is a stable contract — see § Migration in the spec for the rule when these need to change. --- ## Out of scope - **Server Actions / RSC integration** — `onSubmit` stays user-defined. Targets a follow-up once React 19 server-rendering APIs stabilise. - **Server-rendered form recipes** — same reason. - **Domain-specific forms** — moved to consumer apps and the example `@suaveplan/react-ui-pack-auth` pack. - **Form persistence (`useFormPersistence`)** — deferred to `recipes/persistent-form` follow-up. - **CSRF / rate-limiting / sanitisation primitives** — belong in `@suaveplan/api-client` and transport layers. - **Performance hooks** (`useMemoryMonitor`, `usePerformanceMonitor`, `useRenderTracker`) — replaced by OTel-shipped telemetry. --- ## Source mapping | Legacy requirement (`enterprise-react-forms`) | Migrated artefact in v2 | Justification if dropped | |---|---|---| | `BankAccountForm`, `BankTransactionForm` | `createDomainForm` factory | Product scope | | `UserCreateForm`, `UserEditForm`, `OptimizedUserCreateForm` | `createDomainForm` + `createAsyncSearchForm` recipe | Product scope | | `FriendRequestForm` | `createDomainForm` factory | Product scope | | `GroupCreateForm`, `GroupEditForm` | `createDomainForm` + `createAsyncSearchForm` recipe | Product scope | | `MessageCreateForm`, `MessageEditForm` | `createDomainForm` + `createAsyncSearchForm` recipe | Product scope | | `NotificationCreateForm`, `NotificationEditForm`, `NotificationPreferencesForm` | `createDomainForm` factory | Product scope | | `ChangePasswordForm` | `react-ui-pack-auth` (separate change) | Belongs in the auth pack | | `TwoFactorForm` | `react-ui-pack-auth` (separate change) | Belongs in the auth pack | | `SignInForm`, `SignUpForm`, `ForgotPasswordForm`, `ResetPasswordForm` | `react-ui-pack-auth` (separate change) | Belongs in the auth pack | | `SettingCreateForm`, `SettingEditForm`, `NotificationSettingsForm`, `PrivacySettingsForm` | `createDomainForm` factory | Product scope | | `SiteCreateForm`, `SiteEditForm`, `SiteConfigForm` | `createDomainForm` factory | Product scope | | `SystemCreateForm`, `SystemEditForm`, `SystemSettingsForm` | `createDomainForm` factory | Product scope | | `Field`, `AccessibleDatePicker`, `DateTimePicker`, `MoneyInput`, `SearchableSelect`, `TagInput`, `CodeInput`, `RichTextField`, `FileUploadField` | `primitives/` | Direct port | | `ApiClientContext`, `AuthContext` | `core/api-client-context`, `core/auth-context` | Direct port | | `TranslationContext`, `core/i18n/` | `@suaveplan/react-i18n` | Cross-package consolidation | | `useFormValidation`, `useFormSubmission`, `useFormPersistence` | `useFormValidation` (kept), others 🚫 | Submission folds into the factory; persistence is a follow-up recipe | | `ValidationEngine` | `core/validation-engine` (enhanced) | ⬆️ AbortSignal + cross-field + server mapping | | `validation/validators/`, `validation/async/`, `validation/compose/` | Direct port with `TranslationKey` typing | ⬆️ Discriminated-union returns | | `FormWizard` component | `recipes/wizard/createWizardForm` | Refactored from component to factory | | `FormProgressBar`, `FormSection`, `FormSkeleton`, `FormProvider` | 🚫 | Layout / chrome lives in `react-ui` | | `useDebounce`, `useDebouncedValidation`, `useLocalStorage`, `useMemoryMonitor`, `usePerformanceMonitor`, `useRenderTracker`, `useSchemaValidation`, `usePagination` | 🚫 | Replaced by `@suaveplan/timing`, `@suaveplan/types` `StorageLike`, OTel telemetry, `@suaveplan/pagination` | | `accessibility/` standalone components | 🚫 | Inherited via composition with `@suaveplan/react-ui` (WCAG 2.2 AA baseline) | | `security/csrf`, `security/rate-limiting`, `security/sanitization` | 🚫 | Belongs in transport layer | | `integration/mocks/` | 🚫 | Genesis uses real DI adapters; `@suaveplan/testing` provides the harness | --- ## Verification approach Each requirement in `specs/react-forms/spec.md` carries a `**Verified by:**` clause naming the test files. The pre-publish gate is the standard CLAUDE.md sequence: ```bash bunx turbo lint typecheck test --filter=@suaveplan/react-forms bunx turbo test:coverage --filter=@suaveplan/react-forms # 100/100/100/100 hard gate bunx turbo build --filter=@suaveplan/react-forms bun scripts/lint/check-no-hardcoded-strings.ts packages/forms/react-forms bun scripts/lint/check-translation-keys.ts packages/forms/react-forms bun scripts/lint/check-test-runner-imports.ts packages/forms/react-forms ``` Plus the standard banned-pattern, barrel-shape, missing-`.md`, README, bare-version, and subpath-export scans from `openspec/AGENTS.md` §13. --- ## Migration impact This is a pure additive on `genesis` — no existing package depends on `@suaveplan/react-forms` yet. The example pack `@suaveplan/react-ui-pack-auth` (a separate change) is the first consumer and will be developed against this proposal's surface in Wave 6. For consumers porting from upstream `@suaveplan/react-forms@1.x`: - **Old**: `import { SignInForm } from '@suaveplan/react-forms';` - **New**: `import { LoginForm } from '@suaveplan/react-ui-pack-auth';` OR build with `createDomainForm({ schema: LoginRequestSchema, fields: { ... } })`. A `MIGRATION.md` section in the package README enumerates each upstream form → new factory call. Consumer apps rebuild their domain forms at the call site once. --- ## Impact ### Affected dependents Run `bunx turbo ls --filter=...@suaveplan/react-forms` to enumerate. Genesis state at proposal time: - **No dependents on genesis yet** (verified by `bunx turbo ls --filter=...@suaveplan/react-forms`). The package does not exist; its first consumer (`@suaveplan/react-ui-pack-auth`) is itself a pending change tracking this proposal. - `@suaveplan/react-ui-pack-auth` — [ ] tests run / [x] docs reviewed / [ ] no action needed (reason: separate change tracks this surface; both ship together in Wave 6 of the master design) For foundation-package dependent sweep purposes, this package is **not foundational** — it sits high in the dep graph and has zero downstream consumers at proposal time.